diff --git a/package.json b/package.json index 1ed4fd5d3..e64370353 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,16 @@ "packages/*", "test-packages/*" ], + "scripts": { + "test": "npm-run-all test:*", + "test:ember-cli-fastboot": "yarn workspace ember-cli-fastboot test:ember", + "test:fastboot": "yarn workspace fastboot test", + "test:fastboot-express-middleware": "yarn workspace fastboot-express-middleware test", + "test:fastboot-app-server": "yarn workspace fastboot-app-server test:mocha", + "test:integration": "yarn workspace integration-tests test" + }, "devDependencies": { + "npm-run-all": "^4.1.5", "release-it": "^14.2.2", "release-it-lerna-changelog": "^3.1.0", "release-it-yarn-workspaces": "^2.0.0" diff --git a/packages/ember-cli-fastboot/.editorconfig b/packages/ember-cli-fastboot/.editorconfig deleted file mode 100644 index 219985c22..000000000 --- a/packages/ember-cli-fastboot/.editorconfig +++ /dev/null @@ -1,20 +0,0 @@ -# EditorConfig helps developers define and maintain consistent -# coding styles between different editors and IDEs -# editorconfig.org - -root = true - - -[*] -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true -indent_style = space -indent_size = 2 - -[*.hbs] -insert_final_newline = false - -[*.{diff,md}] -trim_trailing_whitespace = false diff --git a/packages/ember-cli-fastboot/index.js b/packages/ember-cli-fastboot/index.js index 10ee7b75b..58ad197a0 100644 --- a/packages/ember-cli-fastboot/index.js +++ b/packages/ember-cli-fastboot/index.js @@ -11,6 +11,7 @@ const chalk = require('chalk'); const fastbootAppBoot = require('./lib/utilities/fastboot-app-boot'); const FastBootConfig = require('./lib/broccoli/fastboot-config'); +const HTMLWriter = require('./lib/broccoli/html-writer'); const fastbootAppFactoryModule = require('./lib/utilities/fastboot-app-factory-module'); const migrateInitializers = require('./lib/build-utilities/migrate-initializers'); const SilentError = require('silent-error'); @@ -177,7 +178,6 @@ module.exports = { */ _getFastbootTree() { const appName = this._name; - const isModuleUnification = this._isModuleUnification(); let fastbootTrees = []; @@ -222,6 +222,7 @@ module.exports = { return finalFastbootTree; }, + // Note: this hook is ignored when built with embroider treeForPublic(tree) { let fastbootTree = this._getFastbootTree(); let trees = []; @@ -232,7 +233,7 @@ module.exports = { let newTree = new MergeTrees(trees); - let fastbootConfigTree = this._buildFastbootConfigTree(newTree); + let fastbootConfigTree = (this._fastbootConfigTree = this._buildFastbootConfigTree(newTree)); // Merge the package.json with the existing tree return new MergeTrees([newTree, fastbootConfigTree], {overwrite: true}); @@ -309,6 +310,28 @@ module.exports = { }); }, + /** + * Write fastboot-script tags to the html file + */ + postprocessTree(type, tree) { + this._super(...arguments); + if (type === 'all') { + let { fastbootConfig, appName, manifest } = this._fastbootConfigTree; + let fastbootHTMLTree = new HTMLWriter(tree, { + annotation: 'FastBoot HTML Writer', + fastbootConfig, + appName, + manifest, + appJsPath: this.app.options.outputPaths.app.js, + }); + + // Merge the package.json with the existing tree + return new MergeTrees([tree, fastbootHTMLTree], { overwrite: true }); + } + + return tree; + }, + serverMiddleware(options) { let emberCliVersion = this._getEmberCliVersion(); let app = options.app; diff --git a/packages/ember-cli-fastboot/lib/broccoli/fastboot-config.js b/packages/ember-cli-fastboot/lib/broccoli/fastboot-config.js index d66c80a29..1f107be41 100644 --- a/packages/ember-cli-fastboot/lib/broccoli/fastboot-config.js +++ b/packages/ember-cli-fastboot/lib/broccoli/fastboot-config.js @@ -1,17 +1,17 @@ /* eslint-env node */ 'use strict'; -const fs = require('fs'); -const fmt = require('util').format; -const uniq = require('ember-cli-lodash-subset').uniq; -const merge = require('ember-cli-lodash-subset').merge; +const fs = require('fs'); +const fmt = require('util').format; +const uniq = require('ember-cli-lodash-subset').uniq; +const merge = require('ember-cli-lodash-subset').merge; const md5Hex = require('md5-hex'); -const path = require('path'); +const path = require('path'); const Plugin = require('broccoli-plugin'); const stringify = require('json-stable-stringify'); -const LATEST_SCHEMA_VERSION = 3; +const LATEST_SCHEMA_VERSION = 5; module.exports = class FastBootConfig extends Plugin { constructor(inputNode, options) { @@ -37,25 +37,29 @@ module.exports = class FastBootConfig extends Plugin { } else { this.htmlFile = 'index.html'; } - + + this.prepareConfig(); + this.prepareDependencies(); } - /** * The main hook called by Broccoli Plugin. Used to build or * rebuild the tree. In this case, we generate the configuration * and write it to `package.json`. */ build() { - this.buildConfig(); - this.buildDependencies(); - this.buildManifest(); this.buildHostWhitelist(); - let outputPath = path.join(this.outputPath, 'package.json'); this.writeFileIfContentChanged(outputPath, this.toJSONString()); } + get manifest() { + if (!this._manifest) { + this._manifest = this.buildManifest(); + } + return this._manifest; + } + writeFileIfContentChanged(outputPath, content) { let previous = this._fileToChecksumMap[outputPath]; let next = md5Hex(content); @@ -66,11 +70,11 @@ module.exports = class FastBootConfig extends Plugin { } } - buildConfig() { + prepareConfig() { // we only walk the host app's addons to grab the config since ideally // addons that have dependency on other addons would never define // this advance hook. - this.project.addons.forEach((addon) => { + this.project.addons.forEach(addon => { if (addon.fastbootConfigTree) { let configFromAddon = addon.fastbootConfigTree(); @@ -83,7 +87,7 @@ module.exports = class FastBootConfig extends Plugin { }); } - buildDependencies() { + prepareDependencies() { let dependencies = {}; let moduleWhitelist = []; let ui = this.ui; @@ -97,7 +101,10 @@ module.exports = class FastBootConfig extends Plugin { if (dep in dependencies) { version = dependencies[dep]; - ui.writeLine(fmt("Duplicate FastBoot dependency %s. Versions may mismatch. Using range %s.", dep, version), ui.WARNING); + ui.writeLine( + fmt('Duplicate FastBoot dependency %s. Versions may mismatch. Using range %s.', dep, version), + ui.WARNING + ); return; } @@ -129,7 +136,7 @@ module.exports = class FastBootConfig extends Plugin { } updateFastBootManifest(manifest) { - this.project.addons.forEach(addon =>{ + this.project.addons.forEach(addon => { if (addon.updateFastBootManifest) { manifest = addon.updateFastBootManifest(manifest); @@ -157,7 +164,7 @@ module.exports = class FastBootConfig extends Plugin { htmlFile: this.htmlFile }; - this.manifest = this.updateFastBootManifest(manifest); + return this.updateFastBootManifest(manifest); } buildHostWhitelist() { @@ -167,17 +174,21 @@ module.exports = class FastBootConfig extends Plugin { } toJSONString() { - return stringify({ - dependencies: this.dependencies, - fastboot: { - moduleWhitelist: this.moduleWhitelist, - schemaVersion: LATEST_SCHEMA_VERSION, - manifest: this.manifest, - hostWhitelist: this.normalizeHostWhitelist(), - config: this.fastbootConfig, - appName: this.appName, - } - }, null, 2); + return stringify( + { + name: this.appName, + dependencies: this.dependencies, + fastboot: { + moduleWhitelist: this.moduleWhitelist, + schemaVersion: LATEST_SCHEMA_VERSION, + hostWhitelist: this.normalizeHostWhitelist(), + config: this.fastbootConfig, + htmlEntrypoint: this.manifest.htmlFile + } + }, + null, + 2 + ); } normalizeHostWhitelist() { @@ -194,7 +205,7 @@ module.exports = class FastBootConfig extends Plugin { } }); } -} +}; function eachAddonPackage(project, cb) { project.addons.map(addon => cb(addon.pkg)); @@ -207,7 +218,11 @@ function getFastBootDependencies(pkg) { } if (addon.fastBootDependencies) { - throw new SilentError('ember-addon.fastBootDependencies has been replaced with ember-addon.fastbootDependencies [addon: ' + pkg.name + ']') + throw new SilentError( + 'ember-addon.fastBootDependencies has been replaced with ember-addon.fastbootDependencies [addon: ' + + pkg.name + + ']' + ); } return addon.fastbootDependencies; diff --git a/packages/ember-cli-fastboot/lib/broccoli/html-writer.js b/packages/ember-cli-fastboot/lib/broccoli/html-writer.js new file mode 100644 index 000000000..375e9b1a5 --- /dev/null +++ b/packages/ember-cli-fastboot/lib/broccoli/html-writer.js @@ -0,0 +1,94 @@ +'use strict'; + +const Filter = require('broccoli-persistent-filter'); +const { JSDOM } = require('jsdom'); + +module.exports = class BasePageWriter extends Filter { + constructor(inputNodes, { annotation, fastbootConfig, appName, manifest, appJsPath }) { + super(inputNodes, { + annotation, + extensions: ['html'], + targetExtension: 'html', + }); + this._manifest = manifest; + this._rootURL = getRootURL(fastbootConfig, appName); + this._appJsPath = appJsPath; + } + + getDestFilePath() { + let filteredRelativePath = super.getDestFilePath(...arguments); + + return filteredRelativePath === this._manifest.htmlFile ? filteredRelativePath : null; + } + + processString(content) { + let dom = new JSDOM(content); + let scriptTags = dom.window.document.querySelectorAll('script'); + + // In fastboot-config.js the paths are transformed with stripLeadingSlash + // do we need to concat rootURL here? + let rootURL = this._rootURL; + + let scriptSrcs = []; + for (let element of scriptTags) { + scriptSrcs.push(urlWithin(element.getAttribute('src'), rootURL)); + } + + let fastbootScripts = this._manifest.vendorFiles + .concat(this._manifest.appFiles) + .map(src => urlWithin(src, rootURL)) + .filter(src => !scriptSrcs.includes(src)); + + let appJsTag = findAppJsTag(scriptTags, this._appJsPath, rootURL); + let range = new NodeRange(appJsTag); + + for (let src of fastbootScripts) { + range.insertAsScriptTag(src); + } + + return dom.serialize(); + } +}; + +function getRootURL(appName, config) { + let rootURL = (config[appName] && config[appName].rootURL) || '/'; + if (!rootURL.endsWith('/')) { + rootURL = rootURL + '/'; + } + return rootURL; +} + +function urlWithin(candidate, root) { + let candidateURL = new URL(candidate, 'http://_the_current_origin_'); + let rootURL = new URL(root, 'http://_the_current_origin_'); + if (candidateURL.href.startsWith(rootURL.href)) { + return candidateURL.href.slice(rootURL.href.length); + } +} + +function findAppJsTag(scriptTags, appJsPath, rootURL) { + appJsPath = urlWithin(appJsPath, rootURL); + for (let e of scriptTags) { + if (urlWithin(e.getAttribute('src'), rootURL) === appJsPath) { + return e; + } + } +} + +class NodeRange { + constructor(initial) { + this.start = initial.ownerDocument.createTextNode(''); + initial.parentElement.insertBefore(this.start, initial); + this.end = initial; + } + + insertAsScriptTag(src) { + let newTag = this.end.ownerDocument.createElement('fastboot-script'); + newTag.setAttribute('src', src); + this.insertNode(newTag); + } + + insertNode(node) { + this.end.parentElement.insertBefore(node, this.end); + } +} diff --git a/packages/ember-cli-fastboot/package.json b/packages/ember-cli-fastboot/package.json index 196f539ce..d92781393 100644 --- a/packages/ember-cli-fastboot/package.json +++ b/packages/ember-cli-fastboot/package.json @@ -37,6 +37,7 @@ "fastboot-express-middleware": "3.2.0-beta.2", "fastboot-transform": "^0.1.3", "fs-extra": "^7.0.0", + "jsdom": "^16.2.2", "json-stable-stringify": "^1.0.1", "md5-hex": "^2.0.0", "recast": "^0.19.1", diff --git a/packages/fastboot/src/fastboot-schema.js b/packages/fastboot/src/fastboot-schema.js index 91fcede9c..c513f9c83 100644 --- a/packages/fastboot/src/fastboot-schema.js +++ b/packages/fastboot/src/fastboot-schema.js @@ -79,7 +79,8 @@ function loadConfig(distPath) { ({ appName, config, html, scripts } = loadManifest(distPath, pkg.fastboot, schemaVersion)); } else { appName = pkg.name; - ({ config, html, scripts } = htmlEntrypoint(appName, distPath, pkg.fastboot.htmlEntrypoint)); + config = pkg.fastboot.config; + ({ html, scripts } = htmlEntrypoint(appName, distPath, pkg.fastboot.htmlEntrypoint, config)); } let sandboxRequire = buildWhitelistedRequire( diff --git a/packages/fastboot/src/html-entrypoint.js b/packages/fastboot/src/html-entrypoint.js index 8e2a2213d..f33ffaa7d 100644 --- a/packages/fastboot/src/html-entrypoint.js +++ b/packages/fastboot/src/html-entrypoint.js @@ -4,21 +4,11 @@ const { JSDOM } = require('jsdom'); const fs = require('fs'); const path = require('path'); -function htmlEntrypoint(appName, distPath, htmlPath) { +function htmlEntrypoint(appName, distPath, htmlPath, config) { let html = fs.readFileSync(path.join(distPath, htmlPath), 'utf8'); let dom = new JSDOM(html); let scripts = []; - let config = {}; - for (let element of dom.window.document.querySelectorAll('meta')) { - let name = element.getAttribute('name'); - if (name && name.endsWith('/config/environment')) { - let content = JSON.parse(decodeURIComponent(element.getAttribute('content'))); - content.APP = Object.assign({ autoboot: false }, content.APP); - config[name.slice(0, -1 * '/config/environment'.length)] = content; - } - } - let rootURL = getRootURL(appName, config); for (let element of dom.window.document.querySelectorAll('script,fastboot-script')) { @@ -34,7 +24,7 @@ function htmlEntrypoint(appName, distPath, htmlPath) { } } - return { config, html: dom.serialize(), scripts }; + return { html: dom.serialize(), scripts }; } function extractSrc(element) {