Skip to content

Commit c15a633

Browse files
author
Dobromir Hristov
authored
feat: add static-hostable support, by allowing changing the baseUrl at runtime (#24)
1 parent f5bce67 commit c15a633

19 files changed

+253
-33
lines changed

app/index.html

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
1818
<link rel="mask-icon" href="<%= BASE_URL %>favicon.svg" color="#333333">
1919
<title><%= process.env.VUE_APP_TITLE %></title>
20+
<script type="application/javascript">var baseUrl = "<%= BASE_URL %>"</script>
2021
</head>
2122
<body data-color-scheme="auto">
2223
<noscript><%= require('@/assets/global-elements/noscript.html') %></noscript>

app/main.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
1010

11+
import '../webpack-asset-path';
1112
import Vue from 'vue';
1213
import Router from 'vue-router';
1314
import App from '@/App.vue';

bin/baseUrlPlaceholder.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* This source file is part of the Swift.org open source project
3+
*
4+
* Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
* Licensed under Apache License v2.0 with Runtime Library Exception
6+
*
7+
* See https://swift.org/LICENSE.txt for license information
8+
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
module.exports = '{{BASE_PATH}}';

bin/transformIndex.js

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* This source file is part of the Swift.org open source project
3+
*
4+
* Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
* Licensed under Apache License v2.0 with Runtime Library Exception
6+
*
7+
* See https://swift.org/LICENSE.txt for license information
8+
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
/**
12+
* This file is a build-time node script, that replaces all references
13+
* of the `BASE_URL_PLACEHOLDER` in the `index.html` file. If it finds references, it stores a
14+
* raw copy of the file as `index-template.html`, along with the replaced, ready to serve version
15+
* as `index.html`.
16+
*
17+
* To create a build with a custom base path, just set a `BASE_URL` in your env, and it will be
18+
* respected in the build, while still creating an `index-template.html` file.
19+
*
20+
* This process is part of the docc static-hostable transformation.
21+
*/
22+
const fs = require('fs');
23+
const path = require('path');
24+
const BASE_URL_PLACEHOLDER = require('./baseUrlPlaceholder');
25+
26+
const indexFile = path.join(__dirname, '../dist/index.html');
27+
const templateFile = path.resolve(__dirname, '../dist/index-template.html');
28+
const baseUrl = process.env.BASE_URL || '/';
29+
30+
try {
31+
// read the template file
32+
const data = fs.readFileSync(indexFile, 'utf8');
33+
34+
if (!data.includes(BASE_URL_PLACEHOLDER)) {
35+
// stop if the placeholder is not found
36+
return;
37+
}
38+
39+
// copy it to a new file
40+
fs.writeFileSync(templateFile, data, 'utf8');
41+
42+
// do the replacement
43+
const result = data.replace(new RegExp(`${BASE_URL_PLACEHOLDER}/`, 'g'), baseUrl);
44+
45+
// replace the file
46+
fs.writeFileSync(indexFile, result, 'utf8');
47+
} catch (err) {
48+
console.error(err);
49+
throw new Error('index.html template processing could not finish.');
50+
}

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"private": true,
66
"scripts": {
77
"serve": "vue-cli-service serve",
8-
"build": "vue-cli-service build",
8+
"build": "vue-cli-service build && node ./bin/transformIndex.js",
99
"test": "npm run test:unit && npm run lint && npm run test:license",
1010
"test:license": "./bin/check-source",
1111
"test:unit": "vue-cli-service test:unit",
@@ -17,7 +17,8 @@
1717
"files": [
1818
"src",
1919
"index.js",
20-
"test-utils.js"
20+
"test-utils.js",
21+
"webpack-asset-path.js"
2122
],
2223
"dependencies": {
2324
"core-js": "^3.8.2",

src/components/ImageAsset.vue

+3-2
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,18 @@
4141
import imageAsset from 'docc-render/mixins/imageAsset';
4242
import AppStore from 'docc-render/stores/AppStore';
4343
import ColorScheme from 'docc-render/constants/ColorScheme';
44+
import { normalizeAssetUrl } from 'docc-render/utils/assets';
4445

4546
function constructAttributes(sources) {
4647
if (!sources.length) {
4748
return null;
4849
}
49-
const srcSet = sources.map(s => `${s.src} ${s.density}`).join(', ');
50+
const srcSet = sources.map(s => `${normalizeAssetUrl(s.src)} ${s.density}`).join(', ');
5051
const defaultSource = sources[0];
5152

5253
const attrs = {
5354
srcSet,
54-
src: defaultSource.src,
55+
src: normalizeAssetUrl(defaultSource.src),
5556
};
5657

5758
// All the variants should have the same size, so use the size of the first

src/components/Tutorial/Hero.vue

+3-2
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import LinkableElement from 'docc-render/components/LinkableElement.vue';
7373

7474
import GenericModal from 'docc-render/components/GenericModal.vue';
7575
import PlayIcon from 'theme/components/Icons/PlayIcon.vue';
76+
import { normalizeAssetUrl } from 'docc-render/utils/assets';
7677
import HeroMetadata from './HeroMetadata.vue';
7778

7879
export default {
@@ -139,10 +140,10 @@ export default {
139140
variant.traits.includes('light')
140141
));
141142

142-
return (lightVariant || {}).url;
143+
return lightVariant ? normalizeAssetUrl(lightVariant.url) : '';
143144
},
144145
projectFilesUrl() {
145-
return this.projectFiles ? this.references[this.projectFiles].url : null;
146+
return this.projectFiles ? normalizeAssetUrl(this.references[this.projectFiles].url) : null;
146147
},
147148
bgStyle() {
148149
return {

src/components/VideoAsset.vue

+6-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<video
1313
:controls="showsControls"
1414
:autoplay="autoplays"
15-
:poster="defaultPosterAttributes.url"
15+
:poster="normalizeAssetUrl(defaultPosterAttributes.url)"
1616
muted
1717
playsinline
1818
@playing="$emit('playing')"
@@ -24,12 +24,12 @@
2424
is handled with JavaScript media query listeners unlike the `<source>`
2525
based implementation being used for image assets.
2626
-->
27-
<source :src="videoAttributes.url">
27+
<source :src="normalizeAssetUrl(videoAttributes.url)">
2828
</video>
2929
</template>
3030

3131
<script>
32-
import { separateVariantsByAppearance } from 'docc-render/utils/assets';
32+
import { separateVariantsByAppearance, normalizeAssetUrl } from 'docc-render/utils/assets';
3333
import AppStore from 'docc-render/stores/AppStore';
3434
import ColorScheme from 'docc-render/constants/ColorScheme';
3535

@@ -116,5 +116,8 @@ export default {
116116
: defaultVideoAttributes
117117
),
118118
},
119+
methods: {
120+
normalizeAssetUrl,
121+
},
119122
};
120123
</script>

src/setup-utils/SwiftDocCRenderRouter.js

+7-5
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,18 @@
99
*/
1010

1111
import Router from 'vue-router';
12-
import { saveScrollOnReload, restoreScrollOnReload, scrollBehavior } from 'docc-render/utils/router-utils';
12+
import {
13+
saveScrollOnReload,
14+
restoreScrollOnReload,
15+
scrollBehavior,
16+
} from 'docc-render/utils/router-utils';
1317
import routes from 'docc-render/routes';
18+
import { baseUrl } from 'docc-render/utils/theme-settings';
1419

1520
export default function createRouterInstance(routerConfig = {}) {
1621
const router = new Router({
1722
mode: 'history',
18-
// This needs to be explicitly set to "/" like this even when the base URL
19-
// is `/tutorials/`. Otherwise, the router would be mistakenly routing things
20-
// to redundant paths like `/tutorials/tutorials/...` on the website.
21-
base: '/',
23+
base: baseUrl,
2224
scrollBehavior,
2325
...routerConfig,
2426
routes: routerConfig.routes || routes,

src/utils/__mocks__/theme-settings.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
1010

11+
const baseUrl = '';
1112
const getSetting = jest.fn(() => ({}));
12-
// eslint-disable-next-line import/prefer-default-export
13-
export { getSetting };
13+
export { baseUrl, getSetting };

src/utils/assets.js

+25
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
/**
1212
* Utility functions for working with Assets
1313
*/
14+
import { baseUrl } from 'docc-render/utils/theme-settings';
1415

1516
/**
1617
* Separate array of variants by light/dark mode
@@ -50,3 +51,27 @@ export function extractDensities(variants) {
5051
return list;
5152
}, []);
5253
}
54+
55+
/**
56+
* Joins two URL paths, normalizing slashes, so we dont have double slashes.
57+
* Does not work with actual URLs.
58+
* @param {Array} parts - list of paths to join.
59+
* @return {String}
60+
*/
61+
export function pathJoin(parts) {
62+
const separator = '/';
63+
const replace = new RegExp(`${separator}+`, 'g');
64+
return parts.join(separator).replace(replace, separator);
65+
}
66+
67+
/**
68+
* Normalizes asset urls, by prefixing the baseUrl path to them.
69+
* @param {String} url
70+
* @return {String}
71+
*/
72+
export function normalizeAssetUrl(url) {
73+
if (!url || typeof url !== 'string' || url.startsWith(baseUrl) || !url.startsWith('/')) {
74+
return url;
75+
}
76+
return pathJoin([baseUrl, url]);
77+
}

src/utils/data.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
1010

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

1416
export class FetchError extends Error {
1517
constructor(route) {
@@ -39,7 +41,7 @@ export async function fetchData(path, params = {}) {
3941
url.search = queryString;
4042
}
4143

42-
const response = await fetch(url);
44+
const response = await fetch(url.href);
4345
if (isBadResponse(response)) {
4446
throw response;
4547
}
@@ -51,7 +53,7 @@ export async function fetchData(path, params = {}) {
5153

5254
function createDataPath(path) {
5355
const dataPath = path.replace(/\/$/, '');
54-
return `${process.env.BASE_URL}data${dataPath}.json`;
56+
return `${pathJoin([baseUrl, 'data', dataPath])}.json`;
5557
}
5658

5759
export async function fetchDataForRouteEnter(to, from, next) {

src/utils/theme-settings.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@ export const themeSettingsState = {
1919
theme: {},
2020
features: {},
2121
};
22+
export const { baseUrl } = window;
2223

2324
/**
2425
* Method to fetch the theme settings and store in local module state.
2526
* Method is called before Vue boots in `main.js`.
2627
* @return {Promise<{}>}
2728
*/
2829
export async function fetchThemeSettings() {
29-
const url = new URL(`${process.env.BASE_URL}theme-settings.json`, window.location.href);
30+
const url = new URL(`${baseUrl}theme-settings.json`, window.location.href);
3031
return fetch(url.href)
3132
.then(r => r.json())
3233
.catch(() => ({}));

tests/unit/setup-utils/SwiftDocCRenderRouter.spec.js

+6
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,17 @@ import Router from 'vue-router';
1313
import SwiftDocCRenderRouter from 'docc-render/setup-utils/SwiftDocCRenderRouter';
1414
import { FetchError } from 'docc-render/utils/data';
1515

16+
jest.mock('docc-render/utils/theme-settings', () => ({
17+
baseUrl: '/',
18+
}));
19+
1620
const mockInstance = {
1721
onError: jest.fn(),
1822
onReady: jest.fn(),
1923
replace: jest.fn(),
24+
beforeEach: jest.fn(),
2025
};
26+
2127
jest.mock('vue-router', () => jest.fn(() => (mockInstance)));
2228
jest.mock('docc-render/utils/router-utils', () => ({
2329
restoreScrollOnReload: jest.fn(),

tests/unit/utils/assets.spec.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* This source file is part of the Swift.org open source project
3+
*
4+
* Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
* Licensed under Apache License v2.0 with Runtime Library Exception
6+
*
7+
* See https://swift.org/LICENSE.txt for license information
8+
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import { normalizeAssetUrl, pathJoin } from 'docc-render/utils/assets';
12+
13+
const mockBaseUrl = jest.fn().mockReturnValue('/');
14+
15+
jest.mock('@/utils/theme-settings', () => ({
16+
get baseUrl() { return mockBaseUrl(); },
17+
}));
18+
19+
describe('assets', () => {
20+
describe('pathJoin', () => {
21+
it.each([
22+
[['foo', 'bar'], 'foo/bar'],
23+
[['foo/', 'bar'], 'foo/bar'],
24+
[['foo', '/bar'], 'foo/bar'],
25+
[['foo/', '/bar'], 'foo/bar'],
26+
[['foo/', 'bar/'], 'foo/bar/'],
27+
[['foo/', '/bar/'], 'foo/bar/'],
28+
[['/foo', '/bar'], '/foo/bar'],
29+
[['/foo', 'bar/'], '/foo/bar/'],
30+
[['/foo/', 'bar/'], '/foo/bar/'],
31+
[['/foo/', '/bar/'], '/foo/bar/'],
32+
])('joins params %s into %s', (params, expected) => {
33+
expect(pathJoin(params)).toEqual(expected);
34+
});
35+
});
36+
describe('normalizeAssetUrl', () => {
37+
it('works correctly if baseurl is just a slash', () => {
38+
mockBaseUrl.mockReturnValue('/');
39+
expect(normalizeAssetUrl('/foo')).toBe('/foo');
40+
});
41+
42+
it('works when both have slashes leading', () => {
43+
mockBaseUrl.mockReturnValue('/base/');
44+
expect(normalizeAssetUrl('/foo')).toBe('/base/foo');
45+
});
46+
47+
it('does not change, if passed a url', () => {
48+
expect(normalizeAssetUrl('https://foo.com')).toBe('https://foo.com');
49+
expect(normalizeAssetUrl('http://foo.com')).toBe('http://foo.com');
50+
});
51+
52+
it('does not change, if path is relative', () => {
53+
mockBaseUrl.mockReturnValue('/base');
54+
expect(normalizeAssetUrl('foo/bar')).toBe('foo/bar');
55+
});
56+
57+
it('does not change, if the path is already prefixed', () => {
58+
mockBaseUrl.mockReturnValue('/base');
59+
expect(normalizeAssetUrl('/base/foo')).toBe('/base/foo');
60+
});
61+
62+
it('returns empty, if nothing passed', () => {
63+
expect(normalizeAssetUrl('')).toBe('');
64+
expect(normalizeAssetUrl(undefined)).toBe(undefined);
65+
expect(normalizeAssetUrl(null)).toBe(null);
66+
});
67+
});
68+
});

0 commit comments

Comments
 (0)