UNPKG

ember-introjs

Version:
363 lines (299 loc) 10.9 kB
/* eslint-env node */ 'use strict'; const path = require('path'); const fs = require('fs'); const MergeTrees = require('broccoli-merge-trees'); const FastBootExpressMiddleware = require('fastboot-express-middleware'); const FastBoot = require('fastboot'); const chalk = require('chalk'); const fastbootAppModule = require('./lib/utilities/fastboot-app-module'); const FastBootConfig = require('./lib/broccoli/fastboot-config'); const migrateInitializers = require('./lib/build-utilities/migrate-initializers'); const Concat = require('broccoli-concat'); const Funnel = require('broccoli-funnel'); const p = require('ember-cli-preprocess-registry/preprocessors'); const fastbootTransform = require('fastboot-transform'); const existsSync = fs.existsSync; let checker; function getVersionChecker(context) { if (!checker) { const VersionChecker = require('ember-cli-version-checker'); checker = new VersionChecker(context); } return checker; } /* * Main entrypoint for the Ember CLI addon. */ module.exports = { name: 'ember-cli-fastboot', init() { this._super.init && this._super.init.apply(this, arguments); }, /** * Called at the start of the build process to let the addon know it will be * used. Sets the auto run on app to be false so that we create and route app * automatically only in browser. * * See: https://ember-cli.com/user-guide/#integration */ included(app) { // set autoRun to false since we will conditionally include creating app when app files // is eval'd in app-boot app.options.autoRun = false; if (app.options.fingerprint) { // set generateAssetMap to be true so that manifest files can be correctly written // in package.json app.options.fingerprint.generateAssetMap = true; } // get the app registry object and app name so that we can build the fastboot // tree this._appRegistry = app.registry; this._name = app.name; migrateInitializers(this.project); }, /** * Registers the fastboot shim that allows apps and addons to wrap non-compatible * libraries in Node with a FastBoot check using `app.import`. * */ importTransforms() { return { fastbootShim: fastbootTransform } }, /** * Inserts placeholders into index.html that are used by the FastBoot server * to insert the rendered content into the right spot. Also injects a module * for FastBoot application boot. */ contentFor(type, config, contents) { if (type === 'body') { return "<!-- EMBER_CLI_FASTBOOT_BODY -->"; } if (type === 'head') { return "<!-- EMBER_CLI_FASTBOOT_TITLE --><!-- EMBER_CLI_FASTBOOT_HEAD -->"; } if (type === 'app-boot') { return fastbootAppModule(config.modulePrefix, JSON.stringify(config.APP || {})); } // if the fastboot addon is installed, we overwrite the config-module so that the config can be read // from meta tag/directly for browser build and from Fastboot config for fastboot target. if (type === 'config-module') { const originalContents = contents.join(''); const appConfigModule = `${config.modulePrefix}`; contents.splice(0, contents.length); contents.push( 'if (typeof FastBoot !== \'undefined\') {', 'return FastBoot.config(\'' + appConfigModule + '\');', '} else {', originalContents, '}' ); return; } }, treeForFastBoot(tree) { let fastbootHtmlBarsTree; // check the ember version and conditionally patch the DOM api if (this._getEmberVersion().lt('2.10.0-alpha.1')) { fastbootHtmlBarsTree = this.treeGenerator(path.resolve(__dirname, 'fastboot-app-lt-2-9')); return tree ? new MergeTrees([tree, fastbootHtmlBarsTree]) : fastbootHtmlBarsTree; } return tree; }, _processAddons(addons, fastbootTrees) { addons.forEach((addon) => { this._processAddon(addon, fastbootTrees); }); }, _processAddon(addon, fastbootTrees) { // walk through each addon and grab its fastboot tree const currentAddonFastbootPath = path.join(addon.root, 'fastboot'); let fastbootTree; if (existsSync(currentAddonFastbootPath)) { fastbootTree = this.treeGenerator(currentAddonFastbootPath); } // invoke addToFastBootTree for every addon if (addon.treeForFastBoot) { let additionalFastBootTree = addon.treeForFastBoot(fastbootTree); if (additionalFastBootTree) { fastbootTrees.push(additionalFastBootTree); } } else if (fastbootTree !== undefined) { fastbootTrees.push(fastbootTree); } this._processAddons(addon.addons, fastbootTrees); }, /** * Function that builds the fastboot tree from all fastboot complaint addons * and project and transpiles it into appname-fastboot.js */ _getFastbootTree() { const appName = this._name; let fastbootTrees = []; this._processAddons(this.project.addons, fastbootTrees); // check the parent containing the fastboot directory const projectFastbootPath = path.join(this.project.root, 'fastboot'); if (existsSync(projectFastbootPath)) { let fastbootTree = this.treeGenerator(projectFastbootPath); fastbootTrees.push(fastbootTree); } // transpile the fastboot JS tree let mergedFastBootTree = new MergeTrees(fastbootTrees, { overwrite: true }); let funneledFastbootTrees = new Funnel(mergedFastBootTree, { destDir: appName }); const processExtraTree = p.preprocessJs(funneledFastbootTrees, '/', this._name, { registry: this._appRegistry }); function stripLeadingSlash(filePath) { return filePath.replace(/^\//, ''); } let appFilePath = stripLeadingSlash(this.app.options.outputPaths.app.js); let finalFastbootTree = new Concat(processExtraTree, { outputFile: appFilePath.replace(/\.js$/, '-fastboot.js') }); return finalFastbootTree; }, treeForPublic(tree) { let fastbootTree = this._getFastbootTree(); let trees = []; if (tree) { trees.push(tree); } trees.push(fastbootTree); let newTree = new MergeTrees(trees); return newTree; }, /** * After the entire Broccoli tree has been built for the `dist` directory, * adds the `fastboot-config.json` file to the root. * */ postprocessTree(type, tree) { if (type === 'all') { let fastbootConfigTree = this._buildFastbootConfigTree(tree); // Merge the package.json with the existing tree return new MergeTrees([tree, fastbootConfigTree], {overwrite: true}); } return tree; }, /** * Need to handroll our own clone algorithm since JSON.stringy changes regex * to empty objects which breaks hostWhiteList property of fastboot. * * @param {Object} config */ _cloneConfigObject(config) { if (config === null || typeof config !== 'object') { return config; } if (config instanceof Array) { let copy = []; for (let i=0; i< config.length; i++) { copy[i] = this._cloneConfigObject(config[i]); } return copy; } if (config instanceof RegExp) { // converting explicitly to string since we create a new regex object // in fastboot: https://github.com/ember-fastboot/fastboot/blob/master/src/fastboot-request.js#L28 return config.toString(); } if (config instanceof Object) { let copy = {}; for (let attr in config) { if (config.hasOwnProperty(attr)) { copy[attr] = this._cloneConfigObject(config[attr]); } } return copy; } throw new Error('App config cannot be cloned for FastBoot.'); }, _getHostAppConfig() { let env = this.app.env; // clone the config object let appConfig = this._cloneConfigObject(this.project.config(env)); // do not boot the app automatically in fastboot. The instance is booted and // lives for the lifetime of the request. let APP = appConfig.APP; if (APP) { APP.autoboot = false; } else { appConfig.APP = { autoboot: false }; } return appConfig; }, _buildFastbootConfigTree(tree) { let appConfig = this._getHostAppConfig(); let fastbootAppConfig = appConfig.fastboot; return new FastBootConfig(tree, { assetMapPath: this.assetMapPath, project: this.project, name: this.app.name, outputPaths: this.app.options.outputPaths, ui: this.ui, fastbootAppConfig: fastbootAppConfig, appConfig: appConfig }); }, serverMiddleware(options) { let emberCliVersion = this._getEmberCliVersion(); let app = options.app; if (emberCliVersion.gte('2.12.0-beta.1')) { // only run the middleware when ember-cli version for app is above 2.12.0-beta.1 since // that version contains API to hook fastboot into ember-cli app.use((req, resp, next) => { const fastbootQueryParam = (req.query.hasOwnProperty('fastboot') && req.query.fastboot === 'false') ? false : true; const enableFastBootServe = !process.env.FASTBOOT_DISABLED && fastbootQueryParam; const broccoliHeader = req.headers['x-broccoli']; const outputPath = broccoliHeader['outputPath']; if (req.serveUrl && enableFastBootServe) { // if it is a base page request, then have fastboot serve the base page if (!this.fastboot) { // TODO(future): make this configurable for allowing apps to pass sandboxGlobals // and custom sandbox class this.ui.writeLine(chalk.green('App is being served by FastBoot')); this.fastboot = new FastBoot({ distPath: outputPath }); } let fastbootMiddleware = FastBootExpressMiddleware({ fastboot: this.fastboot }); fastbootMiddleware(req, resp, next); } else { // forward the request to the next middleware (example other assets, proxy etc) next(); } }); } }, postBuild(result) { if (this.fastboot) { // should we reload fastboot if there are only css changes? Seems it maynot be needed. // TODO(future): we can do a smarter reload here by running fs-tree-diff on files loaded // in sandbox. this.ui.writeLine(chalk.blue('Reloading FastBoot...')); this.fastboot.reload({ distPath: result.directory }); } }, _getEmberCliVersion() { const checker = getVersionChecker(this); return checker.for('ember-cli', 'npm'); }, _getEmberVersion() { const checker = getVersionChecker(this); const emberVersionChecker = checker.for('ember-source', 'npm'); if (emberVersionChecker.version) { return emberVersionChecker; } return checker.for('ember', 'bower'); }, };