ember-introjs
Version:
An Ember Component for intro.js
363 lines (299 loc) • 10.9 kB
JavaScript
/* 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');
},
};