UNPKG

12.4 kBJavaScriptView Raw
1/* eslint-env node */
2'use strict';
3
4const path = require('path');
5const fs = require('fs');
6
7const MergeTrees = require('broccoli-merge-trees');
8const FastBootExpressMiddleware = require('fastboot-express-middleware');
9const FastBoot = require('fastboot');
10const chalk = require('chalk');
11
12const fastbootAppBoot = require('./lib/utilities/fastboot-app-boot');
13const FastBootConfig = require('./lib/broccoli/fastboot-config');
14const fastbootAppFactoryModule = require('./lib/utilities/fastboot-app-factory-module');
15const migrateInitializers = require('./lib/build-utilities/migrate-initializers');
16const SilentError = require('silent-error');
17
18const Concat = require('broccoli-concat');
19const Funnel = require('broccoli-funnel');
20const p = require('ember-cli-preprocess-registry/preprocessors');
21const fastbootTransform = require('fastboot-transform');
22const existsSync = fs.existsSync;
23
24let checker;
25function getVersionChecker(context) {
26 if (!checker) {
27 const VersionChecker = require('ember-cli-version-checker');
28 checker = new VersionChecker(context);
29 }
30 return checker;
31}
32
33
34
35/*
36 * Main entrypoint for the Ember CLI addon.
37 */
38module.exports = {
39 name: 'ember-cli-fastboot',
40
41 init() {
42 this._super.init && this._super.init.apply(this, arguments);
43 this._existsCache = new Map();
44 },
45
46 existsSync(path) {
47 if (this._existsCache.has(path)) {
48 return this._existsCache.get(path);
49 }
50
51 const exists = existsSync(path);
52
53 this._existsCache.set(path, exists);
54 return exists;
55 },
56
57 /**
58 * Called at the start of the build process to let the addon know it will be
59 * used. Sets the auto run on app to be false so that we create and route app
60 * automatically only in browser.
61 *
62 * See: https://ember-cli.com/user-guide/#integration
63 */
64 included(app) {
65
66 let assetRev = this.project.addons.find(addon => addon.name === 'broccoli-asset-rev');
67 if(assetRev && !assetRev.supportsFastboot) {
68 throw new SilentError("This version of ember-cli-fastboot requires a newer version of broccoli-asset-rev");
69 }
70
71 // set autoRun to false since we will conditionally include creating app when app files
72 // is eval'd in app-boot
73 app.options.autoRun = false;
74
75 app.import('vendor/experimental-render-mode-rehydrate.js');
76
77 // get the app registry object and app name so that we can build the fastboot
78 // tree
79 this._appRegistry = app.registry;
80 this._name = app.name;
81
82 this.fastbootOptions = this._fastbootOptionsFor(app.env, app.project);
83
84 migrateInitializers(this.project);
85 },
86
87 /**
88 * Registers the fastboot shim that allows apps and addons to wrap non-compatible
89 * libraries in Node with a FastBoot check using `app.import`.
90 *
91 */
92 importTransforms() {
93 return {
94 fastbootShim: fastbootTransform
95 }
96 },
97
98 /**
99 * Inserts placeholders into index.html that are used by the FastBoot server
100 * to insert the rendered content into the right spot. Also injects a module
101 * for FastBoot application boot.
102 */
103 contentFor(type, config, contents) {
104 if (type === 'body') {
105 return "<!-- EMBER_CLI_FASTBOOT_BODY -->";
106 }
107
108 if (type === 'head') {
109 return "<!-- EMBER_CLI_FASTBOOT_TITLE --><!-- EMBER_CLI_FASTBOOT_HEAD -->";
110 }
111
112 if (type === 'app-boot') {
113 const isModuleUnification = this._isModuleUnification();
114 return fastbootAppBoot(config.modulePrefix, JSON.stringify(config.APP || {}), isModuleUnification);
115 }
116
117 // if the fastboot addon is installed, we overwrite the config-module so that the config can be read
118 // from meta tag/directly for browser build and from Fastboot config for fastboot target.
119 if (type === 'config-module') {
120 const originalContents = contents.join('');
121 const appConfigModule = `${config.modulePrefix}`;
122 contents.splice(0, contents.length);
123 contents.push(
124 'if (typeof FastBoot !== \'undefined\') {',
125 'return FastBoot.config(\'' + appConfigModule + '\');',
126 '} else {',
127 originalContents,
128 '}'
129 );
130 return;
131 }
132 },
133
134 treeForFastBoot(tree) {
135 let fastbootHtmlBarsTree;
136
137 // check the ember version and conditionally patch the DOM api
138 if (this._getEmberVersion().lt('2.10.0-alpha.1')) {
139 fastbootHtmlBarsTree = this.treeGenerator(path.resolve(__dirname, 'fastboot-app-lt-2-9'));
140 return tree ? new MergeTrees([tree, fastbootHtmlBarsTree]) : fastbootHtmlBarsTree;
141 }
142
143 return tree;
144 },
145
146 _processAddons(addons, fastbootTrees) {
147 addons.forEach((addon) => {
148 this._processAddon(addon, fastbootTrees);
149 });
150 },
151
152 _processAddon(addon, fastbootTrees) {
153 // walk through each addon and grab its fastboot tree
154 const currentAddonFastbootPath = path.join(addon.root, 'fastboot');
155
156 let fastbootTree;
157 if (this.existsSync(currentAddonFastbootPath)) {
158 fastbootTree = this.treeGenerator(currentAddonFastbootPath);
159 }
160
161 // invoke addToFastBootTree for every addon
162 if (addon.treeForFastBoot) {
163 let additionalFastBootTree = addon.treeForFastBoot(fastbootTree);
164 if (additionalFastBootTree) {
165 fastbootTrees.push(additionalFastBootTree);
166 }
167 } else if (fastbootTree !== undefined) {
168 fastbootTrees.push(fastbootTree);
169 }
170
171 this._processAddons(addon.addons, fastbootTrees);
172 },
173
174 /**
175 * Function that builds the fastboot tree from all fastboot complaint addons
176 * and project and transpiles it into appname-fastboot.js
177 */
178 _getFastbootTree() {
179 const appName = this._name;
180 const isModuleUnification = this._isModuleUnification();
181
182 let fastbootTrees = [];
183
184 this._processAddons(this.project.addons, fastbootTrees);
185 // check the parent containing the fastboot directory
186 const projectFastbootPath = path.join(this.project.root, 'fastboot');
187 if (this.existsSync(projectFastbootPath)) {
188 let fastbootTree = this.treeGenerator(projectFastbootPath);
189 fastbootTrees.push(fastbootTree);
190 }
191
192 // transpile the fastboot JS tree
193 let mergedFastBootTree = new MergeTrees(fastbootTrees, {
194 overwrite: true
195 });
196
197 let funneledFastbootTrees = new Funnel(mergedFastBootTree, {
198 destDir: appName
199 });
200 const processExtraTree = p.preprocessJs(funneledFastbootTrees, '/', this._name, {
201 registry: this._appRegistry
202 });
203
204 // FastBoot app factory module
205 const writeFile = require('broccoli-file-creator');
206 let appFactoryModuleTree = writeFile("app-factory.js", fastbootAppFactoryModule(appName, this._isModuleUnification()));
207
208 let newProcessExtraTree = new MergeTrees([processExtraTree, appFactoryModuleTree], {
209 overwrite: true
210 });
211
212 function stripLeadingSlash(filePath) {
213 return filePath.replace(/^\//, '');
214 }
215
216 let appFilePath = stripLeadingSlash(this.app.options.outputPaths.app.js);
217 let finalFastbootTree = new Concat(newProcessExtraTree, {
218 outputFile: appFilePath.replace(/\.js$/, '-fastboot.js')
219 });
220
221 return finalFastbootTree;
222 },
223
224 treeForPublic(tree) {
225 let fastbootTree = this._getFastbootTree();
226 let trees = [];
227 if (tree) {
228 trees.push(tree);
229 }
230 trees.push(fastbootTree);
231
232 let newTree = new MergeTrees(trees);
233
234 let fastbootConfigTree = this._buildFastbootConfigTree(newTree);
235
236 // Merge the package.json with the existing tree
237 return new MergeTrees([newTree, fastbootConfigTree], {overwrite: true});
238 },
239
240 /**
241 * Need to handroll our own clone algorithm since JSON.stringy changes regex
242 * to empty objects which breaks hostWhiteList property of fastboot.
243 *
244 * @param {Object} config
245 */
246 _cloneConfigObject(config) {
247 if (config === null || typeof config !== 'object') {
248 return config;
249 }
250
251 if (config instanceof Array) {
252 let copy = [];
253 for (let i=0; i< config.length; i++) {
254 copy[i] = this._cloneConfigObject(config[i]);
255 }
256
257 return copy;
258 }
259
260 if (config instanceof RegExp) {
261 // converting explicitly to string since we create a new regex object
262 // in fastboot: https://github.com/ember-fastboot/fastboot/blob/master/src/fastboot-request.js#L28
263 return config.toString();
264 }
265
266 if (config instanceof Object) {
267 let copy = {};
268 for (let attr in config) {
269 if (config.hasOwnProperty(attr)) {
270 copy[attr] = this._cloneConfigObject(config[attr]);
271 }
272 }
273
274 return copy;
275 }
276
277 throw new Error('App config cannot be cloned for FastBoot.');
278 },
279
280 _getHostAppConfig() {
281 let env = this.app.env;
282 // clone the config object
283 let appConfig = this._cloneConfigObject(this.project.config(env));
284
285 // do not boot the app automatically in fastboot. The instance is booted and
286 // lives for the lifetime of the request.
287 let APP = appConfig.APP;
288 if (APP) {
289 APP.autoboot = false;
290 } else {
291 appConfig.APP = { autoboot: false };
292 }
293
294 return appConfig;
295 },
296
297 _buildFastbootConfigTree(tree) {
298 let appConfig = this._getHostAppConfig();
299 let fastbootAppConfig = appConfig.fastboot;
300
301 return new FastBootConfig(tree, {
302 project: this.project,
303 name: this.app.name,
304 outputPaths: this.app.options.outputPaths,
305 ui: this.ui,
306 fastbootAppConfig: fastbootAppConfig,
307 appConfig: appConfig
308 });
309 },
310
311 serverMiddleware(options) {
312 let emberCliVersion = this._getEmberCliVersion();
313 let app = options.app;
314
315 if (emberCliVersion.gte('2.12.0-beta.1')) {
316 // only run the middleware when ember-cli version for app is above 2.12.0-beta.1 since
317 // that version contains API to hook fastboot into ember-cli
318
319 app.use((req, resp, next) => {
320 const fastbootQueryParam = (req.query.hasOwnProperty('fastboot') && req.query.fastboot === 'false') ? false : true;
321 const enableFastBootServe = !process.env.FASTBOOT_DISABLED && fastbootQueryParam;
322
323 if (req.serveUrl && enableFastBootServe) {
324 // if it is a base page request, then have fastboot serve the base page
325 if (!this.fastboot) {
326 const broccoliHeader = req.headers['x-broccoli'];
327 const outputPath = broccoliHeader['outputPath'];
328 const fastbootOptions = Object.assign(
329 {},
330 this.fastbootOptions,
331 { distPath: outputPath }
332 );
333
334 this.ui.writeLine(chalk.green('App is being served by FastBoot'));
335 this.fastboot = new FastBoot(fastbootOptions);
336 }
337
338 let fastbootMiddleware = FastBootExpressMiddleware({
339 fastboot: this.fastboot
340 });
341
342 fastbootMiddleware(req, resp, next);
343 } else {
344 // forward the request to the next middleware (example other assets, proxy etc)
345 next();
346 }
347 });
348 }
349 },
350
351 postBuild(result) {
352 if (this.fastboot) {
353 // should we reload fastboot if there are only css changes? Seems it maynot be needed.
354 // TODO(future): we can do a smarter reload here by running fs-tree-diff on files loaded
355 // in sandbox.
356 this.ui.writeLine(chalk.blue('Reloading FastBoot...'));
357 this.fastboot.reload({
358 distPath: result.directory
359 });
360 }
361 },
362
363 _getEmberCliVersion() {
364 const checker = getVersionChecker(this);
365
366 return checker.for('ember-cli', 'npm');
367 },
368
369 _getEmberVersion() {
370 const checker = getVersionChecker(this);
371 const emberVersionChecker = checker.for('ember-source', 'npm');
372
373 if (emberVersionChecker.version) {
374 return emberVersionChecker;
375 }
376
377 return checker.for('ember', 'bower');
378 },
379
380 _isModuleUnification() {
381 return (typeof this.project.isModuleUnification === 'function') && this.project.isModuleUnification();
382 },
383
384 /**
385 * Reads FastBoot configuration from application's `config/fastboot.js` file if present,
386 * otherwise returns empty object.
387 *
388 * The configuration file is expected to export a function with `environment` as an argument,
389 * which is same as a how `config/environment.js` works.
390 *
391 * TODO Allow add-ons to provide own options and merge them with the application's options.
392 */
393 _fastbootOptionsFor(environment, project) {
394 const configPath = path.join(path.dirname(project.configPath()), 'fastboot.js');
395
396 if (fs.existsSync(configPath)) {
397 return require(configPath)(environment);
398 }
399 return {};
400 }
401};