Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add static-hostable support, by allowing changing the baseUrl at runtime #24

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link rel="mask-icon" href="<%= BASE_URL %>favicon.svg" color="#333333">
<title><%= process.env.VUE_APP_TITLE %></title>
<script type="application/javascript">var baseUrl = "<%= BASE_URL %>"</script>
</head>
<body data-color-scheme="auto">
<noscript><%= require('@/assets/global-elements/noscript.html') %></noscript>
Expand Down
1 change: 1 addition & 0 deletions app/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import '../webpack-asset-path';
import Vue from 'vue';
import Router from 'vue-router';
import App from '@/App.vue';
Expand Down
11 changes: 11 additions & 0 deletions bin/baseUrlPlaceholder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* This source file is part of the Swift.org open source project
*
* Copyright (c) 2021 Apple Inc. and the Swift project authors
* Licensed under Apache License v2.0 with Runtime Library Exception
*
* See https://swift.org/LICENSE.txt for license information
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

module.exports = '{{BASE_PATH}}';
50 changes: 50 additions & 0 deletions bin/transformIndex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* This source file is part of the Swift.org open source project
*
* Copyright (c) 2021 Apple Inc. and the Swift project authors
* Licensed under Apache License v2.0 with Runtime Library Exception
*
* See https://swift.org/LICENSE.txt for license information
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

/**
* This file is a build-time node script, that replaces all references
* of the `BASE_URL_PLACEHOLDER` in the `index.html` file. If it finds references, it stores a
* raw copy of the file as `index-template.html`, along with the replaced, ready to serve version
* as `index.html`.
*
* To create a build with a custom base path, just set a `BASE_URL` in your env, and it will be
* respected in the build, while still creating an `index-template.html` file.
*
* This process is part of the docc static-hostable transformation.
*/
const fs = require('fs');
const path = require('path');
const BASE_URL_PLACEHOLDER = require('./baseUrlPlaceholder');

const indexFile = path.join(__dirname, '../dist/index.html');
const templateFile = path.resolve(__dirname, '../dist/index-template.html');
const baseUrl = process.env.BASE_URL || '/';

try {
// read the template file
const data = fs.readFileSync(indexFile, 'utf8');

if (!data.includes(BASE_URL_PLACEHOLDER)) {
// stop if the placeholder is not found
return;
}

// copy it to a new file
fs.writeFileSync(templateFile, data, 'utf8');

// do the replacement
const result = data.replace(new RegExp(`${BASE_URL_PLACEHOLDER}/`, 'g'), baseUrl);

// replace the file
fs.writeFileSync(indexFile, result, 'utf8');
} catch (err) {
console.error(err);
throw new Error('index.html template processing could not finish.');
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"build": "vue-cli-service build && node ./bin/transformIndex.js",
"test": "npm run test:unit && npm run lint && npm run test:license",
"test:license": "./bin/check-source",
"test:unit": "vue-cli-service test:unit",
Expand All @@ -17,7 +17,8 @@
"files": [
"src",
"index.js",
"test-utils.js"
"test-utils.js",
"webpack-asset-path.js"
],
"dependencies": {
"core-js": "^3.8.2",
Expand Down
5 changes: 3 additions & 2 deletions src/components/ImageAsset.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,18 @@
import imageAsset from 'docc-render/mixins/imageAsset';
import AppStore from 'docc-render/stores/AppStore';
import ColorScheme from 'docc-render/constants/ColorScheme';
import { normalizeAssetUrl } from 'docc-render/utils/assets';

function constructAttributes(sources) {
if (!sources.length) {
return null;
}
const srcSet = sources.map(s => `${s.src} ${s.density}`).join(', ');
const srcSet = sources.map(s => `${normalizeAssetUrl(s.src)} ${s.density}`).join(', ');
const defaultSource = sources[0];

const attrs = {
srcSet,
src: defaultSource.src,
src: normalizeAssetUrl(defaultSource.src),
};

// All the variants should have the same size, so use the size of the first
Expand Down
5 changes: 3 additions & 2 deletions src/components/Tutorial/Hero.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import LinkableElement from 'docc-render/components/LinkableElement.vue';

import GenericModal from 'docc-render/components/GenericModal.vue';
import PlayIcon from 'theme/components/Icons/PlayIcon.vue';
import { normalizeAssetUrl } from 'docc-render/utils/assets';
import HeroMetadata from './HeroMetadata.vue';

export default {
Expand Down Expand Up @@ -139,10 +140,10 @@ export default {
variant.traits.includes('light')
));

return (lightVariant || {}).url;
return lightVariant ? normalizeAssetUrl(lightVariant.url) : '';
},
projectFilesUrl() {
return this.projectFiles ? this.references[this.projectFiles].url : null;
return this.projectFiles ? normalizeAssetUrl(this.references[this.projectFiles].url) : null;
},
bgStyle() {
return {
Expand Down
9 changes: 6 additions & 3 deletions src/components/VideoAsset.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<video
:controls="showsControls"
:autoplay="autoplays"
:poster="defaultPosterAttributes.url"
:poster="normalizeAssetUrl(defaultPosterAttributes.url)"
muted
playsinline
@playing="$emit('playing')"
Expand All @@ -24,12 +24,12 @@
is handled with JavaScript media query listeners unlike the `<source>`
based implementation being used for image assets.
-->
<source :src="videoAttributes.url">
<source :src="normalizeAssetUrl(videoAttributes.url)">
</video>
</template>

<script>
import { separateVariantsByAppearance } from 'docc-render/utils/assets';
import { separateVariantsByAppearance, normalizeAssetUrl } from 'docc-render/utils/assets';
import AppStore from 'docc-render/stores/AppStore';
import ColorScheme from 'docc-render/constants/ColorScheme';

Expand Down Expand Up @@ -116,5 +116,8 @@ export default {
: defaultVideoAttributes
),
},
methods: {
normalizeAssetUrl,
},
};
</script>
12 changes: 7 additions & 5 deletions src/setup-utils/SwiftDocCRenderRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@
*/

import Router from 'vue-router';
import { saveScrollOnReload, restoreScrollOnReload, scrollBehavior } from 'docc-render/utils/router-utils';
import {
saveScrollOnReload,
restoreScrollOnReload,
scrollBehavior,
} from 'docc-render/utils/router-utils';
import routes from 'docc-render/routes';
import { baseUrl } from 'docc-render/utils/theme-settings';

export default function createRouterInstance(routerConfig = {}) {
const router = new Router({
mode: 'history',
// This needs to be explicitly set to "/" like this even when the base URL
// is `/tutorials/`. Otherwise, the router would be mistakenly routing things
// to redundant paths like `/tutorials/tutorials/...` on the website.
base: '/',
base: baseUrl,
scrollBehavior,
...routerConfig,
routes: routerConfig.routes || routes,
Expand Down
4 changes: 2 additions & 2 deletions src/utils/__mocks__/theme-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

const baseUrl = '';
const getSetting = jest.fn(() => ({}));
// eslint-disable-next-line import/prefer-default-export
export { getSetting };
export { baseUrl, getSetting };
25 changes: 25 additions & 0 deletions src/utils/assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
/**
* Utility functions for working with Assets
*/
import { baseUrl } from 'docc-render/utils/theme-settings';

/**
* Separate array of variants by light/dark mode
Expand Down Expand Up @@ -50,3 +51,27 @@ export function extractDensities(variants) {
return list;
}, []);
}

/**
* Joins two URL paths, normalizing slashes, so we dont have double slashes.
* Does not work with actual URLs.
* @param {Array} parts - list of paths to join.
* @return {String}
*/
export function pathJoin(parts) {
const separator = '/';
const replace = new RegExp(`${separator}+`, 'g');
return parts.join(separator).replace(replace, separator);
}

/**
* Normalizes asset urls, by prefixing the baseUrl path to them.
* @param {String} url
* @return {String}
*/
export function normalizeAssetUrl(url) {
if (!url || typeof url !== 'string' || url.startsWith(baseUrl) || !url.startsWith('/')) {
return url;
}
return pathJoin([baseUrl, url]);
}
6 changes: 4 additions & 2 deletions src/utils/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import { pathJoin } from 'docc-render/utils/assets';
import { queryStringForParams, areEquivalentLocations } from 'docc-render/utils/url-helper';
import emitWarningForSchemaVersionMismatch from 'docc-render/utils/schema-version-check';
import { baseUrl } from 'docc-render/utils/theme-settings';

export class FetchError extends Error {
constructor(route) {
Expand Down Expand Up @@ -39,7 +41,7 @@ export async function fetchData(path, params = {}) {
url.search = queryString;
}

const response = await fetch(url);
const response = await fetch(url.href);
if (isBadResponse(response)) {
throw response;
}
Expand All @@ -51,7 +53,7 @@ export async function fetchData(path, params = {}) {

function createDataPath(path) {
const dataPath = path.replace(/\/$/, '');
return `${process.env.BASE_URL}data${dataPath}.json`;
return `${pathJoin([baseUrl, 'data', dataPath])}.json`;
}

export async function fetchDataForRouteEnter(to, from, next) {
Expand Down
3 changes: 2 additions & 1 deletion src/utils/theme-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ export const themeSettingsState = {
theme: {},
features: {},
};
export const { baseUrl } = window;

/**
* Method to fetch the theme settings and store in local module state.
* Method is called before Vue boots in `main.js`.
* @return {Promise<{}>}
*/
export async function fetchThemeSettings() {
const url = new URL(`${process.env.BASE_URL}theme-settings.json`, window.location.href);
const url = new URL(`${baseUrl}theme-settings.json`, window.location.href);
return fetch(url.href)
.then(r => r.json())
.catch(() => ({}));
Expand Down
6 changes: 6 additions & 0 deletions tests/unit/setup-utils/SwiftDocCRenderRouter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@ import Router from 'vue-router';
import SwiftDocCRenderRouter from 'docc-render/setup-utils/SwiftDocCRenderRouter';
import { FetchError } from 'docc-render/utils/data';

jest.mock('docc-render/utils/theme-settings', () => ({
baseUrl: '/',
}));

const mockInstance = {
onError: jest.fn(),
onReady: jest.fn(),
replace: jest.fn(),
beforeEach: jest.fn(),
};

jest.mock('vue-router', () => jest.fn(() => (mockInstance)));
jest.mock('docc-render/utils/router-utils', () => ({
restoreScrollOnReload: jest.fn(),
Expand Down
68 changes: 68 additions & 0 deletions tests/unit/utils/assets.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* This source file is part of the Swift.org open source project
*
* Copyright (c) 2021 Apple Inc. and the Swift project authors
* Licensed under Apache License v2.0 with Runtime Library Exception
*
* See https://swift.org/LICENSE.txt for license information
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import { normalizeAssetUrl, pathJoin } from 'docc-render/utils/assets';

const mockBaseUrl = jest.fn().mockReturnValue('/');

jest.mock('@/utils/theme-settings', () => ({
get baseUrl() { return mockBaseUrl(); },
}));

describe('assets', () => {
describe('pathJoin', () => {
it.each([
[['foo', 'bar'], 'foo/bar'],
[['foo/', 'bar'], 'foo/bar'],
[['foo', '/bar'], 'foo/bar'],
[['foo/', '/bar'], 'foo/bar'],
[['foo/', 'bar/'], 'foo/bar/'],
[['foo/', '/bar/'], 'foo/bar/'],
[['/foo', '/bar'], '/foo/bar'],
[['/foo', 'bar/'], '/foo/bar/'],
[['/foo/', 'bar/'], '/foo/bar/'],
[['/foo/', '/bar/'], '/foo/bar/'],
])('joins params %s into %s', (params, expected) => {
expect(pathJoin(params)).toEqual(expected);
});
});
describe('normalizeAssetUrl', () => {
it('works correctly if baseurl is just a slash', () => {
mockBaseUrl.mockReturnValue('/');
expect(normalizeAssetUrl('/foo')).toBe('/foo');
});

it('works when both have slashes leading', () => {
mockBaseUrl.mockReturnValue('/base/');
expect(normalizeAssetUrl('/foo')).toBe('/base/foo');
});

it('does not change, if passed a url', () => {
expect(normalizeAssetUrl('https://foo.com')).toBe('https://foo.com');
expect(normalizeAssetUrl('http://foo.com')).toBe('http://foo.com');
});

it('does not change, if path is relative', () => {
mockBaseUrl.mockReturnValue('/base');
expect(normalizeAssetUrl('foo/bar')).toBe('foo/bar');
});

it('does not change, if the path is already prefixed', () => {
mockBaseUrl.mockReturnValue('/base');
expect(normalizeAssetUrl('/base/foo')).toBe('/base/foo');
});

it('returns empty, if nothing passed', () => {
expect(normalizeAssetUrl('')).toBe('');
expect(normalizeAssetUrl(undefined)).toBe(undefined);
expect(normalizeAssetUrl(null)).toBe(null);
});
});
});
Loading