ember-cli-fastboot
Version:
Server-side rendering for Ember.js apps
224 lines (180 loc) • 6.02 kB
JavaScript
/* eslint-disable no-undef, prettier/prettier */
/* 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 md5Hex = require('md5-hex');
const path = require('path');
const Plugin = require('broccoli-plugin');
const stringify = require('json-stable-stringify');
const LATEST_SCHEMA_VERSION = 3;
module.exports = class FastBootConfig extends Plugin {
constructor(inputNode, options) {
super([inputNode], {
annotation: 'Generate: FastBoot package.json',
persistentOutput: true
});
this.project = options.project;
this.name = options.name;
this.ui = options.ui;
this.fastbootAppConfig = options.fastbootAppConfig;
this.outputPaths = options.outputPaths;
this.appName = options.appConfig.modulePrefix;
const appConfigModule = `${this.appName}`;
this.fastbootConfig = {};
this.fastbootConfig[appConfigModule] = options.appConfig;
this._fileToChecksumMap = {};
if (this.fastbootAppConfig && this.fastbootAppConfig.htmlFile) {
this.htmlFile = this.fastbootAppConfig.htmlFile;
} else {
this.htmlFile = 'index.html';
}
}
/**
* 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());
}
writeFileIfContentChanged(outputPath, content) {
let previous = this._fileToChecksumMap[outputPath];
let next = md5Hex(content);
if (previous !== next) {
fs.writeFileSync(outputPath, content);
this._fileToChecksumMap[outputPath] = next; // update map
}
}
buildConfig() {
// 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) => {
if (addon.fastbootConfigTree) {
let configFromAddon = addon.fastbootConfigTree();
if (!configFromAddon) {
throw new Error('`fastbootConfigTree` requires a map to be returned');
}
merge(this.fastbootConfig, configFromAddon);
}
});
}
buildDependencies() {
let dependencies = {};
let moduleWhitelist = [];
let ui = this.ui;
eachAddonPackage(this.project, pkg => {
let deps = getFastBootDependencies(pkg);
if (deps) {
deps.forEach(dep => {
let version = getDependencyVersion(pkg, dep);
if (dep in dependencies) {
version = dependencies[dep];
ui.writeLine(fmt("Duplicate FastBoot dependency %s. Versions may mismatch. Using range %s.", dep, version), ui.WARNING);
return;
}
moduleWhitelist.push(dep);
if (version) {
dependencies[dep] = version;
}
});
}
});
let pkg = this.project.pkg;
let projectDeps = pkg.fastbootDependencies;
if (projectDeps) {
projectDeps.forEach(dep => {
moduleWhitelist.push(dep);
let version = pkg.dependencies && pkg.dependencies[dep];
if (version) {
dependencies[dep] = version;
}
});
}
this.dependencies = dependencies;
this.moduleWhitelist = uniq(moduleWhitelist);
}
updateFastBootManifest(manifest) {
this.project.addons.forEach(addon =>{
if (addon.updateFastBootManifest) {
manifest = addon.updateFastBootManifest(manifest);
if (!manifest) {
throw new Error(`${addon.name} did not return the updated manifest from updateFastBootManifest hook.`);
}
}
});
return manifest;
}
buildManifest() {
function stripLeadingSlash(filePath) {
return filePath.replace(/^\//, '');
}
let appFilePath = stripLeadingSlash(this.outputPaths.app.js);
let appFastbootFilePath = appFilePath.replace(/\.js$/, '') + '-fastboot.js';
let vendorFilePath = stripLeadingSlash(this.outputPaths.vendor.js);
let manifest = {
appFiles: [appFilePath, appFastbootFilePath],
vendorFiles: [vendorFilePath],
htmlFile: this.htmlFile
};
this.manifest = this.updateFastBootManifest(manifest);
}
buildHostWhitelist() {
if (this.fastbootAppConfig) {
this.hostWhitelist = this.fastbootAppConfig.hostWhitelist;
}
}
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,
}
}, { space: 2 });
}
normalizeHostWhitelist() {
if (!this.hostWhitelist) {
return;
}
return this.hostWhitelist.map(function(entry) {
// Is a regex
if (entry.source) {
return '/' + entry.source + '/';
} else {
return entry;
}
});
}
}
function eachAddonPackage(project, cb) {
project.addons.map(addon => cb(addon.pkg));
}
function getFastBootDependencies(pkg) {
let addon = pkg['ember-addon'];
if (!addon) {
return addon;
}
if (addon.fastBootDependencies) {
throw new SilentError('ember-addon.fastBootDependencies has been replaced with ember-addon.fastbootDependencies [addon: ' + pkg.name + ']')
}
return addon.fastbootDependencies;
}
function getDependencyVersion(pkg, dep) {
if (!pkg.dependencies) {
throw new Error(fmt("Could not find FastBoot dependency '%s' in %s/package.json dependencies.", dep, pkg.name));
}
return pkg.dependencies[dep];
}