1 |
|
2 | 'use strict';
|
3 |
|
4 | const path = require('path');
|
5 | const fs = require('fs');
|
6 |
|
7 | const MergeTrees = require('broccoli-merge-trees');
|
8 | const FastBootExpressMiddleware = require('fastboot-express-middleware');
|
9 | const FastBoot = require('fastboot');
|
10 | const chalk = require('chalk');
|
11 |
|
12 | const fastbootAppBoot = require('./lib/utilities/fastboot-app-boot');
|
13 | const FastBootConfig = require('./lib/broccoli/fastboot-config');
|
14 | const fastbootAppFactoryModule = require('./lib/utilities/fastboot-app-factory-module');
|
15 | const migrateInitializers = require('./lib/build-utilities/migrate-initializers');
|
16 | const SilentError = require('silent-error');
|
17 |
|
18 | const Concat = require('broccoli-concat');
|
19 | const Funnel = require('broccoli-funnel');
|
20 | const p = require('ember-cli-preprocess-registry/preprocessors');
|
21 | const fastbootTransform = require('fastboot-transform');
|
22 | const existsSync = fs.existsSync;
|
23 |
|
24 | let checker;
|
25 | function 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 |
|
37 |
|
38 | module.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 |
|
59 |
|
60 |
|
61 |
|
62 |
|
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 |
|
72 |
|
73 | app.options.autoRun = false;
|
74 |
|
75 | app.import('vendor/experimental-render-mode-rehydrate.js');
|
76 |
|
77 |
|
78 |
|
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 |
|
89 |
|
90 |
|
91 |
|
92 | importTransforms() {
|
93 | return {
|
94 | fastbootShim: fastbootTransform
|
95 | }
|
96 | },
|
97 |
|
98 | |
99 |
|
100 |
|
101 |
|
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 |
|
118 |
|
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 |
|
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 |
|
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 |
|
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 |
|
176 |
|
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 |
|
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 |
|
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 |
|
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 |
|
237 | return new MergeTrees([newTree, fastbootConfigTree], {overwrite: true});
|
238 | },
|
239 |
|
240 | |
241 |
|
242 |
|
243 |
|
244 |
|
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 |
|
262 |
|
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 |
|
283 | let appConfig = this._cloneConfigObject(this.project.config(env));
|
284 |
|
285 |
|
286 |
|
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 |
|
317 |
|
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 |
|
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 |
|
345 | next();
|
346 | }
|
347 | });
|
348 | }
|
349 | },
|
350 |
|
351 | postBuild(result) {
|
352 | if (this.fastboot) {
|
353 |
|
354 |
|
355 |
|
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 |
|
386 |
|
387 |
|
388 |
|
389 |
|
390 |
|
391 |
|
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 | };
|