Skip to content

Commit a6d05b0

Browse files
committed
Increment schema to latest 5
html-oriented manifest format ----------------------------- In the new schema format, which is defined in ember-fastboot/fastboot@3fd5bc9 the manifest is written into HTML and later extracted by fastboot on server side instead of previously reading from dist/package.json Note: The new schema in fastboot does not handle fastboot config https://github.com/ember-fastboot/ember-cli-fastboot/tree/e4d0b7c7bcdf82def0dc8726835b49d707673f41#providing-additional-config This commit changes to read Fastboot.config from dist/package.json instead of ignoring it Allow to require module path from whitelisted dependency ------------------------------------------------------- Incrementing schema to 5 also included the changes in schema 4 strictWhitelist See ember-fastboot/fastboot#200 Revert back to put config in dist/package.json add data-fastboot-ignore to unexpected files properly ignore files that should not execute in fastboot
1 parent 909c141 commit a6d05b0

File tree

10 files changed

+231
-70
lines changed

10 files changed

+231
-70
lines changed

packages/ember-cli-fastboot/index.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const chalk = require('chalk');
1111

1212
const fastbootAppBoot = require('./lib/utilities/fastboot-app-boot');
1313
const FastBootConfig = require('./lib/broccoli/fastboot-config');
14+
const HTMLWriter = require('./lib/broccoli/html-writer');
1415
const fastbootAppFactoryModule = require('./lib/utilities/fastboot-app-factory-module');
1516
const migrateInitializers = require('./lib/build-utilities/migrate-initializers');
1617
const SilentError = require('silent-error');
@@ -219,6 +220,7 @@ module.exports = {
219220
return finalFastbootTree;
220221
},
221222

223+
// Note: this hook is ignored when built with embroider
222224
treeForPublic(tree) {
223225
let fastbootTree = this._getFastbootTree();
224226
let trees = [];
@@ -229,7 +231,7 @@ module.exports = {
229231

230232
let newTree = new MergeTrees(trees);
231233

232-
let fastbootConfigTree = this._buildFastbootConfigTree(newTree);
234+
let fastbootConfigTree = (this._fastbootConfigTree = this._buildFastbootConfigTree(newTree));
233235

234236
// Merge the package.json with the existing tree
235237
return new MergeTrees([newTree, fastbootConfigTree], { overwrite: true });
@@ -306,6 +308,29 @@ module.exports = {
306308
});
307309
},
308310

311+
/**
312+
* Write fastboot-script tags to the html file
313+
*/
314+
postprocessTree(type, tree) {
315+
this._super(...arguments);
316+
if (type === 'all') {
317+
let { fastbootConfig, appName, manifest } = this._fastbootConfigTree;
318+
let fastbootHTMLTree = new HTMLWriter(tree, {
319+
annotation: 'FastBoot HTML Writer',
320+
fastbootConfig,
321+
appName,
322+
manifest,
323+
appJsPath: this.app.options.outputPaths.app.js,
324+
outputPaths: this.app.options.outputPaths,
325+
});
326+
327+
// Merge the package.json with the existing tree
328+
return new MergeTrees([tree, fastbootHTMLTree], { overwrite: true });
329+
}
330+
331+
return tree;
332+
},
333+
309334
serverMiddleware(options) {
310335
let emberCliVersion = this._getEmberCliVersion();
311336
let app = options.app;

packages/ember-cli-fastboot/lib/broccoli/fastboot-config.js

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
/* eslint-env node */
22
'use strict';
33

4-
const fs = require('fs');
5-
const fmt = require('util').format;
6-
const uniq = require('ember-cli-lodash-subset').uniq;
7-
const merge = require('ember-cli-lodash-subset').merge;
4+
const fs = require('fs');
5+
const fmt = require('util').format;
6+
const uniq = require('ember-cli-lodash-subset').uniq;
7+
const merge = require('ember-cli-lodash-subset').merge;
88
const md5Hex = require('md5-hex');
9-
const path = require('path');
9+
const path = require('path');
1010
const Plugin = require('broccoli-plugin');
1111

1212
const stringify = require('json-stable-stringify');
1313

14-
const LATEST_SCHEMA_VERSION = 3;
14+
const LATEST_SCHEMA_VERSION = 5;
1515

1616
module.exports = class FastBootConfig extends Plugin {
1717
constructor(inputNode, options) {
@@ -38,24 +38,28 @@ module.exports = class FastBootConfig extends Plugin {
3838
this.htmlFile = 'index.html';
3939
}
4040

41+
this.prepareConfig();
42+
this.prepareDependencies();
4143
}
4244

43-
4445
/**
4546
* The main hook called by Broccoli Plugin. Used to build or
4647
* rebuild the tree. In this case, we generate the configuration
4748
* and write it to `package.json`.
4849
*/
4950
build() {
50-
this.buildConfig();
51-
this.buildDependencies();
52-
this.buildManifest();
5351
this.buildHostWhitelist();
54-
5552
let outputPath = path.join(this.outputPath, 'package.json');
5653
this.writeFileIfContentChanged(outputPath, this.toJSONString());
5754
}
5855

56+
get manifest() {
57+
if (!this._manifest) {
58+
this._manifest = this.buildManifest();
59+
}
60+
return this._manifest;
61+
}
62+
5963
writeFileIfContentChanged(outputPath, content) {
6064
let previous = this._fileToChecksumMap[outputPath];
6165
let next = md5Hex(content);
@@ -66,11 +70,11 @@ module.exports = class FastBootConfig extends Plugin {
6670
}
6771
}
6872

69-
buildConfig() {
73+
prepareConfig() {
7074
// we only walk the host app's addons to grab the config since ideally
7175
// addons that have dependency on other addons would never define
7276
// this advance hook.
73-
this.project.addons.forEach((addon) => {
77+
this.project.addons.forEach(addon => {
7478
if (addon.fastbootConfigTree) {
7579
let configFromAddon = addon.fastbootConfigTree();
7680

@@ -83,7 +87,7 @@ module.exports = class FastBootConfig extends Plugin {
8387
});
8488
}
8589

86-
buildDependencies() {
90+
prepareDependencies() {
8791
let dependencies = {};
8892
let moduleWhitelist = [];
8993
let ui = this.ui;
@@ -97,7 +101,10 @@ module.exports = class FastBootConfig extends Plugin {
97101

98102
if (dep in dependencies) {
99103
version = dependencies[dep];
100-
ui.writeLine(fmt("Duplicate FastBoot dependency %s. Versions may mismatch. Using range %s.", dep, version), ui.WARNING);
104+
ui.writeLine(
105+
fmt('Duplicate FastBoot dependency %s. Versions may mismatch. Using range %s.', dep, version),
106+
ui.WARNING
107+
);
101108
return;
102109
}
103110

@@ -129,7 +136,7 @@ module.exports = class FastBootConfig extends Plugin {
129136
}
130137

131138
updateFastBootManifest(manifest) {
132-
this.project.addons.forEach(addon =>{
139+
this.project.addons.forEach(addon => {
133140
if (addon.updateFastBootManifest) {
134141
manifest = addon.updateFastBootManifest(manifest);
135142

@@ -157,7 +164,7 @@ module.exports = class FastBootConfig extends Plugin {
157164
htmlFile: this.htmlFile
158165
};
159166

160-
this.manifest = this.updateFastBootManifest(manifest);
167+
return this.updateFastBootManifest(manifest);
161168
}
162169

163170
buildHostWhitelist() {
@@ -167,17 +174,21 @@ module.exports = class FastBootConfig extends Plugin {
167174
}
168175

169176
toJSONString() {
170-
return stringify({
171-
dependencies: this.dependencies,
172-
fastboot: {
173-
moduleWhitelist: this.moduleWhitelist,
174-
schemaVersion: LATEST_SCHEMA_VERSION,
175-
manifest: this.manifest,
176-
hostWhitelist: this.normalizeHostWhitelist(),
177-
config: this.fastbootConfig,
178-
appName: this.appName,
179-
}
180-
}, null, 2);
177+
return stringify(
178+
{
179+
name: this.appName,
180+
dependencies: this.dependencies,
181+
fastboot: {
182+
moduleWhitelist: this.moduleWhitelist,
183+
schemaVersion: LATEST_SCHEMA_VERSION,
184+
hostWhitelist: this.normalizeHostWhitelist(),
185+
config: this.fastbootConfig,
186+
htmlEntrypoint: this.manifest.htmlFile
187+
}
188+
},
189+
null,
190+
2
191+
);
181192
}
182193

183194
normalizeHostWhitelist() {
@@ -194,7 +205,7 @@ module.exports = class FastBootConfig extends Plugin {
194205
}
195206
});
196207
}
197-
}
208+
};
198209

199210
function eachAddonPackage(project, cb) {
200211
project.addons.map(addon => cb(addon.pkg));
@@ -207,7 +218,11 @@ function getFastBootDependencies(pkg) {
207218
}
208219

209220
if (addon.fastBootDependencies) {
210-
throw new SilentError('ember-addon.fastBootDependencies has been replaced with ember-addon.fastbootDependencies [addon: ' + pkg.name + ']')
221+
throw new SilentError(
222+
'ember-addon.fastBootDependencies has been replaced with ember-addon.fastbootDependencies [addon: ' +
223+
pkg.name +
224+
']'
225+
);
211226
}
212227

213228
return addon.fastbootDependencies;
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
'use strict';
2+
3+
const Filter = require('broccoli-persistent-filter');
4+
const { JSDOM } = require('jsdom');
5+
6+
module.exports = class BasePageWriter extends Filter {
7+
constructor(inputNodes, { annotation, fastbootConfig, appName, manifest, outputPaths }) {
8+
super(inputNodes, {
9+
annotation,
10+
extensions: ['html'],
11+
targetExtension: 'html',
12+
});
13+
this._manifest = manifest;
14+
this._rootURL = getRootURL(fastbootConfig, appName);
15+
this._appJsPath = outputPaths.app.js;
16+
this._expectedFiles = expectedFiles(outputPaths);
17+
}
18+
19+
getDestFilePath() {
20+
let filteredRelativePath = super.getDestFilePath(...arguments);
21+
22+
return filteredRelativePath === this._manifest.htmlFile ? filteredRelativePath : null;
23+
}
24+
25+
processString(content) {
26+
let dom = new JSDOM(content);
27+
let scriptTags = dom.window.document.querySelectorAll('script');
28+
29+
this._ignoreUnexpectedScripts(scriptTags);
30+
31+
let fastbootScripts = this._findFastbootScriptToInsert(scriptTags);
32+
let appJsTag = findAppJsTag(scriptTags, this._appJsPath, this._rootURL);
33+
insertFastbootScriptsBeforeAppJsTags(fastbootScripts, appJsTag);
34+
35+
return dom.serialize();
36+
}
37+
38+
_findFastbootScriptToInsert(scriptTags) {
39+
let rootURL = this._rootURL;
40+
let scriptSrcs = [];
41+
for (let element of scriptTags) {
42+
scriptSrcs.push(urlWithin(element.getAttribute('src'), rootURL));
43+
}
44+
45+
return this._manifest.vendorFiles
46+
.concat(this._manifest.appFiles)
47+
.map(src => urlWithin(src, rootURL))
48+
.filter(src => !scriptSrcs.includes(src));
49+
}
50+
51+
_ignoreUnexpectedScripts(scriptTags) {
52+
let expectedFiles = this._expectedFiles;
53+
let rootURL = this._rootURL;
54+
for (let element of scriptTags) {
55+
if (!expectedFiles.includes(urlWithin(element.getAttribute('src'), rootURL))) {
56+
element.setAttribute('data-fastboot-ignore', '');
57+
}
58+
}
59+
}
60+
};
61+
62+
function expectedFiles(outputPaths) {
63+
function stripLeadingSlash(filePath) {
64+
return filePath.replace(/^\//, '');
65+
}
66+
67+
let appFilePath = stripLeadingSlash(outputPaths.app.js);
68+
let appFastbootFilePath = appFilePath.replace(/\.js$/, '') + '-fastboot.js';
69+
let vendorFilePath = stripLeadingSlash(outputPaths.vendor.js);
70+
return [appFilePath, appFastbootFilePath, vendorFilePath];
71+
}
72+
73+
function getRootURL(appName, config) {
74+
let rootURL = (config[appName] && config[appName].rootURL) || '/';
75+
if (!rootURL.endsWith('/')) {
76+
rootURL = rootURL + '/';
77+
}
78+
return rootURL;
79+
}
80+
81+
function urlWithin(candidate, root) {
82+
let candidateURL = new URL(candidate, 'http://_the_current_origin_');
83+
let rootURL = new URL(root, 'http://_the_current_origin_');
84+
if (candidateURL.href.startsWith(rootURL.href)) {
85+
return candidateURL.href.slice(rootURL.href.length);
86+
}
87+
}
88+
89+
function findAppJsTag(scriptTags, appJsPath, rootURL) {
90+
appJsPath = urlWithin(appJsPath, rootURL);
91+
for (let e of scriptTags) {
92+
if (urlWithin(e.getAttribute('src'), rootURL) === appJsPath) {
93+
return e;
94+
}
95+
}
96+
}
97+
98+
function insertFastbootScriptsBeforeAppJsTags(fastbootScripts, appJsTag) {
99+
let range = new NodeRange(appJsTag);
100+
101+
for (let src of fastbootScripts) {
102+
range.insertAsScriptTag(src);
103+
}
104+
}
105+
106+
class NodeRange {
107+
constructor(initial) {
108+
this.start = initial.ownerDocument.createTextNode('');
109+
initial.parentElement.insertBefore(this.start, initial);
110+
this.end = initial;
111+
}
112+
113+
insertAsScriptTag(src) {
114+
let newTag = this.end.ownerDocument.createElement('fastboot-script');
115+
newTag.setAttribute('src', src);
116+
this.insertNode(newTag);
117+
this.insertNode(this.end.ownerDocument.createTextNode('\n'));
118+
}
119+
120+
insertNode(node) {
121+
this.end.parentElement.insertBefore(node, this.end);
122+
}
123+
}

packages/ember-cli-fastboot/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"fastboot-express-middleware": "3.2.0-beta.2",
4444
"fastboot-transform": "^0.1.3",
4545
"fs-extra": "^7.0.0",
46+
"jsdom": "^16.2.2",
4647
"json-stable-stringify": "^1.0.1",
4748
"md5-hex": "^2.0.0",
4849
"recast": "^0.19.1",

packages/fastboot/src/fastboot-schema.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ function loadConfig(distPath) {
7979
({ appName, config, html, scripts } = loadManifest(distPath, pkg.fastboot, schemaVersion));
8080
} else {
8181
appName = pkg.name;
82-
({ config, html, scripts } = htmlEntrypoint(appName, distPath, pkg.fastboot.htmlEntrypoint));
82+
config = pkg.fastboot.config;
83+
({ html, scripts } = htmlEntrypoint(appName, distPath, pkg.fastboot.htmlEntrypoint, config));
8384
}
8485

8586
let sandboxRequire = buildWhitelistedRequire(

packages/fastboot/src/html-entrypoint.js

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,11 @@ const { JSDOM } = require('jsdom');
44
const fs = require('fs');
55
const path = require('path');
66

7-
function htmlEntrypoint(appName, distPath, htmlPath) {
7+
function htmlEntrypoint(appName, distPath, htmlPath, config = {}) {
88
let html = fs.readFileSync(path.join(distPath, htmlPath), 'utf8');
99
let dom = new JSDOM(html);
1010
let scripts = [];
1111

12-
let config = {};
13-
for (let element of dom.window.document.querySelectorAll('meta')) {
14-
let name = element.getAttribute('name');
15-
if (name && name.endsWith('/config/environment')) {
16-
let content = JSON.parse(decodeURIComponent(element.getAttribute('content')));
17-
content.APP = Object.assign({ autoboot: false }, content.APP);
18-
config[name.slice(0, -1 * '/config/environment'.length)] = content;
19-
}
20-
}
21-
2212
let rootURL = getRootURL(appName, config);
2313

2414
for (let element of dom.window.document.querySelectorAll('script,fastboot-script')) {
@@ -34,7 +24,7 @@ function htmlEntrypoint(appName, distPath, htmlPath) {
3424
}
3525
}
3626

37-
return { config, html: dom.serialize(), scripts };
27+
return { html: dom.serialize(), scripts };
3828
}
3929

4030
function extractSrc(element) {

0 commit comments

Comments
 (0)