UNPKG

37.8 kBJavaScriptView Raw
1'use strict';
2
3const Funnel = require('broccoli-funnel');
4const merge = require('lodash/merge');
5const mergeTrees = require('ember-cli/lib/broccoli/merge-trees');
6const fs = require('fs');
7const path = require('path');
8const writeFile = require('broccoli-file-creator');
9const concat = require('broccoli-concat');
10const DependencyFunnel = require('broccoli-dependency-funnel');
11const defaultsDeep = require('lodash/defaultsDeep');
12const calculateCacheKeyForTree = require('calculate-cache-key-for-tree');
13const Addon = require('ember-cli/lib/models/addon');
14const { memoize } = require('./utils/memoize');
15const maybeMergeTrees = require('./utils/maybe-merge-trees');
16const deeplyNonDuplicatedAddon = require('./utils/deeply-non-duplicated-addon');
17const restoreOriginalAddons = require('./utils/restore-original-addons');
18const p = require('ember-cli-preprocess-registry/preprocessors');
19const shouldCompactReexports = require('./utils/should-compact-reexports');
20const appendCompactReexportsIfNeeded = require('./utils/append-compact-reexports-if-needed');
21const ensureLazyLoadingHash = require('./utils/ensure-lazy-loading-hash');
22const preprocessCss = p.preprocessCss;
23const preprocessMinifyCss = p.preprocessMinifyCss;
24const BroccoliDebug = require('broccoli-debug');
25
26// Older versions of Ember-CLI may not have the Addon's tree cache
27const HAS_TREE_CACHE = !!Addon._treeCache;
28const EOL = require('os').EOL;
29const DEFAULT_CONFIG = {
30 outputPaths: {
31 vendor: {
32 css: '/assets/engine-vendor.css',
33 js: '/assets/engine-vendor.js',
34 },
35 },
36 trees: {
37 addon: 'addon',
38 },
39 minifyCSS: {},
40};
41
42const findRoot = require('./utils/find-root');
43const findHost = require('./utils/find-host');
44const findHostsHost = require('./utils/find-hosts-host');
45const processBabel = require('./utils/process-babel');
46const buildExternalTree = memoize(function buildExternalTree() {
47 const treePath = this.treePaths['vendor'];
48 const vendorPath = treePath ? path.resolve(this.root, treePath) : null;
49 let trees = [];
50
51 if (vendorPath && fs.existsSync(vendorPath)) {
52 trees.push(new Funnel(vendorPath, {
53 destDir: 'vendor',
54 }));
55 }
56
57 let nodeModulesTrees = Array.from(this._nodeModules.values(), module => new Funnel(module.path, {
58 srcDir: '/',
59 destDir: `node_modules/${module.name}/`,
60 annotation: `Funnel (node_modules/${module.name})`,
61 }));
62
63 trees = trees.concat(...nodeModulesTrees);
64
65 let externalTree = mergeTrees(trees, {
66 annotation: 'TreeMerger (ExternalTree)',
67 overwrite: true,
68 });
69
70 for (let customTransformEntry of this._customTransformsMap) {
71 let transformName = customTransformEntry[0];
72 let transformConfig = customTransformEntry[1];
73
74 let transformTree = new Funnel(externalTree, {
75 files: transformConfig.files,
76 annotation: `Funnel (custom transform: ${transformName})`,
77 });
78
79 externalTree = mergeTrees([externalTree, transformConfig.callback(transformTree, transformConfig.options)], {
80 annotation: `TreeMerger (custom transform: ${transformName})`,
81 overwrite: true,
82 });
83 }
84
85 return externalTree;
86});
87
88const buildVendorTree = memoize(function buildVendorTree() {
89 // Manually invoke the child addons addon trees.
90 let childAddonsAddonTrees = this.nonDuplicatedAddonInvoke('treeFor', [
91 'addon',
92 'buildVendorTree',
93 ]);
94 let childAddonsAddonTreesMerged = mergeTrees(childAddonsAddonTrees, {
95 overwrite: true,
96 });
97
98 return childAddonsAddonTreesMerged;
99});
100
101const buildVendorJSTree = memoize(function buildVendorJSTree(vendorTree) {
102 // Filter out the JS so that we can process it correctly.
103 let vendorJSTree = new Funnel(vendorTree, {
104 include: ['**/*.js'],
105 exclude: ['vendor/**/*.*'],
106 });
107
108 return vendorJSTree;
109});
110
111const buildVendorCSSTree = memoize(function buildVendorCSSTree(vendorTree) {
112 // Filter out the CSS so that we can process it correctly.
113 let vendorCSSTree = new Funnel(vendorTree, {
114 include: ['**/*.css'],
115 exclude: ['vendor/**/*.*'],
116 });
117
118 return this.debugTree(vendorCSSTree, 'vendor-style:input');
119});
120
121const buildEngineJSTree = memoize(function buildEngineJSTree() {
122 let engineSourceTree;
123 let treePath;
124 let addonTree = this.options.trees.addon;
125
126 if (typeof addonTree === 'string') {
127 treePath = path.resolve(this.root, addonTree);
128 }
129
130 if (treePath && fs.existsSync(treePath)) {
131 engineSourceTree = this.treeGenerator(treePath);
132 } else {
133 engineSourceTree = addonTree;
134 }
135
136 // We want the config and child app trees to be compiled with the engine source
137 let configTree = writeFile('config/environment.js', this.getEngineConfigContents());
138
139 // This is an extraction of what would normally be run by the `treeFor` hook.
140 let childAppTree = mergeTrees(this.eachAddonInvoke('treeFor', ['app']), {
141 overwrite: true
142 });
143
144 let augmentedEngineTree = mergeTrees([
145 configTree,
146 childAppTree,
147 engineSourceTree
148 ].filter(Boolean),
149 { overwrite: true }
150 );
151 let engineTree = this.compileAddon(augmentedEngineTree);
152
153 return engineTree;
154});
155
156const buildEngineJSTreeWithoutRoutes = memoize(
157 function buildEngineJSTreeWithoutRoutes() {
158 return new DependencyFunnel(buildEngineJSTree.call(this), {
159 exclude: true,
160 entry: this.name + '/routes.js',
161 external: ['ember-engines/routes'],
162 });
163 }
164);
165
166const buildEngineRoutesJSTree = memoize(function buildEngineRouteJSTree(sourceMapConfig) {
167 // Get complete engine JS tree
168 let engineAppTree = buildEngineJSTree.call(this);
169
170 // Separate routes
171 let engineRoutesTree = new DependencyFunnel(engineAppTree, {
172 include: true,
173 entry: this.name + '/routes.js',
174 external: ['ember-engines/routes'],
175 });
176
177 // If babel options aren't defined, we need to transpile the modules.
178 if (!this.options || !this.options.babel) {
179 engineRoutesTree = processBabel(engineRoutesTree);
180 }
181
182 // Concatenate routes.js and its dependencies into a single file.
183 engineRoutesTree = concat(engineRoutesTree, {
184 allowNone: true,
185 inputFiles: ['**/*.js'],
186 outputFile: 'engines-dist/' + this.name + '/assets/routes.js',
187 sourceMapConfig,
188 });
189
190 // Return concatenated JS tree
191 return this.debugTree(engineRoutesTree, 'routes:output');
192});
193
194const buildVendorJSWithImports = memoize(function buildVendorJSTreeWithImports(concatTranspiledVendorJSTree, sourceMapConfig) {
195 let externalTree = buildExternalTree.call(this);
196 let combined = mergeTrees(
197 [externalTree, concatTranspiledVendorJSTree].filter(Boolean),
198 { overwrite: true }
199 );
200
201 let vendorFiles = [];
202 for (let outputFile in this._scriptOutputFiles) {
203 let inputFiles = this._scriptOutputFiles[outputFile];
204
205 vendorFiles.push(
206 concat(combined, {
207 allowNone: true,
208 annotation: 'Concat: Vendor ' + outputFile,
209 headerFiles: inputFiles,
210 outputFile: 'engines-dist/' + this.name + outputFile,
211 separator: EOL + ';',
212 sourceMapConfig,
213 })
214 );
215 }
216
217 return mergeTrees(vendorFiles, { overwrite: true });
218});
219
220const buildVendorCSSWithImports = memoize(function buildVendorCSSWithImports(concatVendorCSSTree, sourceMapConfig) {
221 let externalTree = buildExternalTree.call(this);
222 let combined = mergeTrees(
223 [externalTree, concatVendorCSSTree].filter(Boolean),
224 { overwrite: true }
225 );
226
227 let vendorFiles = [];
228 for (let outputFile in this._styleOutputFiles) {
229 let inputFiles = this._styleOutputFiles[outputFile];
230
231 vendorFiles.push(
232 concat(combined, {
233 allowNone: true,
234 annotation: 'Concat: Vendor ' + outputFile,
235 inputFiles: inputFiles,
236 outputFile: 'engines-dist/' + this.name + outputFile,
237 sourceMapConfig,
238 })
239 );
240 }
241
242 return mergeTrees(vendorFiles, { overwrite: true });
243});
244
245const buildCompleteJSTree = memoize(function buildCompleteJSTree() {
246 let vendorTree = buildVendorTree.call(this);
247 let vendorJSTree = buildVendorJSTree.call(this, vendorTree);
248 let engineAppTree = buildEngineJSTree.call(this);
249
250 return mergeTrees([vendorJSTree, engineAppTree], { overwrite: true });
251});
252
253const buildEngineStyleTree = memoize(function buildEngineStyleTree() {
254 // gather engines own `addon/styles` and its dependencies `app/styles`
255 let engineStylesTree = this._treeFor('addon-styles');
256 let dependencyStyleTrees = this.nonDuplicatedAddonInvoke('treeFor', [
257 'styles',
258 ]);
259 let dependencyStyleTree = maybeMergeTrees(dependencyStyleTrees, {
260 overwrite: true,
261 });
262 let relocatedDependencyStyleTree;
263
264 // if dependency styles trees were found, relocate them to the expected
265 // path (`addon/styles)
266 if (dependencyStyleTree) {
267 relocatedDependencyStyleTree = new Funnel(dependencyStyleTree, {
268 allowEmpty: true,
269 srcDir: 'app/styles',
270 destDir: '/',
271 annotation: 'Funnel: relocate app/styles for (' + this.name + ')',
272 });
273 }
274
275 let combinedEngineStylesAndDependencyStylesTree = maybeMergeTrees(
276 [relocatedDependencyStyleTree, engineStylesTree],
277 { overwrite: true }
278 );
279
280 return this.debugTree(
281 combinedEngineStylesAndDependencyStylesTree,
282 'engine-style:input'
283 );
284});
285
286module.exports = {
287 extend(options) {
288 let originalInit =
289 options.init ||
290 function() {
291 this._super.init.apply(this, arguments);
292 };
293
294 options.cacheKeyForTree = function(treeType) {
295 // We do different things in the addon, public, and engine trees based on
296 // the value of `lazyLoading.enabled`, so we add it to the cache key
297 if (
298 treeType === 'addon' ||
299 treeType === 'public' ||
300 treeType === 'engine'
301 ) {
302 return calculateCacheKeyForTree(treeType, this, [this.lazyLoading]);
303 } else {
304 return calculateCacheKeyForTree(treeType, this);
305 }
306 };
307
308 /**
309 * Gets a map of all the addons that are used by all hosts above
310 * the current host.
311 *
312 * The key is the name of the addon and the value is first encountered instance of this addon.
313 */
314 options.ancestorHostAddons = function() {
315 if (this._hostAddons) {
316 return this._hostAddons;
317 }
318
319 let host = findHostsHost.call(this);
320
321 if (!host) {
322 return {};
323 }
324
325 let hostIsEngine = !!host.ancestorHostAddons;
326
327 let hostAddons = hostIsEngine
328 ? Object.assign({}, host.ancestorHostAddons())
329 : {};
330 let queue = hostIsEngine
331 ? host.addons.slice()
332 : host.project.addons.slice();
333
334 // Do a breadth-first walk of the addons in the host, ignoring those that
335 // have a different host (e.g., lazy engine)
336 while (queue.length) {
337 let addon = queue.pop();
338
339 if (addon.lazyLoading && addon.lazyLoading.enabled) {
340 continue;
341 }
342
343 if (hostAddons[addon.name]) {
344 continue;
345 }
346
347 hostAddons[addon.name] = addon;
348 queue.push.apply(queue, addon.addons);
349 }
350
351 this._hostAddons = hostAddons;
352
353 return hostAddons;
354 };
355
356 /**
357 * Similar to eachAddonInvoke, except that it does not run the method for
358 * addons that already appear in an ancestor host already. This prevents
359 * duplicate inclusion of code by child lazy engines.
360 */
361 options.nonDuplicatedAddonInvoke = function(methodName, args) {
362 this.initializeAddons();
363
364 let invokeArguments = args || [];
365 let hostAddons = this.ancestorHostAddons();
366 let treeName = invokeArguments[0];
367
368 // methodName could be "treeFor" or "included"
369 // the "included" methodName will be handled in the old implementation for now
370 // TODO: when deeplyNonDuplicatedAddon is completely ready
371 // make sure that the "included" methodName is considered
372 if (process.env.EMBER_ENGINES_ADDON_DEDUPE && methodName === 'treeFor') {
373 let trees
374
375 try {
376 deeplyNonDuplicatedAddon(hostAddons, this, treeName);
377
378 trees = this.addons
379 .filter(addon => {
380 if (!addon[methodName]) {
381 // no method to call
382 return false;
383 }
384 return true;
385 })
386 .map(addon => {
387 return addon[methodName].apply(addon, invokeArguments);
388 });
389 } finally {
390 restoreOriginalAddons(this);
391 }
392
393 return trees
394 }
395
396 // old implementation
397 return this.addons
398 .map(addon => {
399 if (!addon[methodName]) {
400 // no method to call
401 return;
402 }
403
404 let hostAddon = hostAddons[addon.name];
405
406 if (hostAddon && hostAddon.cacheKeyForTree) {
407 if (methodName === 'treeFor') {
408 let hostAddonCacheKey = hostAddon.cacheKeyForTree(treeName);
409 let addonCacheKey = addon.cacheKeyForTree(treeName);
410
411 if (
412 addonCacheKey != null &&
413 addonCacheKey === hostAddonCacheKey
414 ) {
415 // the addon specifies cache key and it is the same as host instance of the addon, skip the tree
416 return;
417 }
418 } else {
419 return;
420 }
421 }
422
423 return addon[methodName].apply(addon, invokeArguments);
424 }).filter(Boolean);
425 };
426
427 options.debugTree = BroccoliDebug.buildDebugCallback(
428 'ember-engines:' + options.name
429 );
430
431 options._concatStyles =
432 options._concatStyles ||
433 function concatProcessedStyles(type, tree, sourceMapConfig) {
434 let engineStylesOutputDir = 'engines-dist/' + this.name + '/assets/';
435
436 // Move styles tree into the correct place.
437 // `**/*.css` all gets merged.
438 return concat(tree, {
439 allowNone: true,
440 inputFiles: ['**/*.css'],
441 outputFile: engineStylesOutputDir + type + '.css',
442 sourceMapConfig,
443 });
444 };
445
446 options.init = function() {
447 this._engineConfig = new Map();
448 this.options = defaultsDeep(options, DEFAULT_CONFIG);
449
450 ensureLazyLoadingHash(this);
451
452 // NOTE: This is a beautiful hack to deal with core object calling toString on the function.
453 // It'll throw a deprecation warning if this isn't present because it doesn't see a `_super`
454 // invocation. Do not remove the following line!
455 // this._super()
456
457 let result = originalInit.apply(this, arguments);
458
459 if (!this._addonPreprocessTree && this._addonPostprocessTree) {
460 throw new Error(
461 'ember-engines@0.5 requires ember-cli@2.12, please update your ember-cli version.'
462 );
463 }
464
465 ['ember-addon', 'ember-engine'].forEach(keyword => {
466 if (!this.pkg.keywords || this.pkg.keywords.indexOf(keyword) === -1) {
467 this.ui.writeWarnLine(
468 this.pkg.name +
469 ` engine must specify "${keyword}" in the keywords section on package.json`
470 );
471 }
472 });
473
474 // Require that the user specify a lazyLoading property.
475 if (!('lazyLoading' in this)) {
476 this.ui.writeDeprecateLine(
477 this.pkg.name +
478 ' engine must specify the `lazyLoading.enabled` property as to whether the engine should be lazily loaded.'
479 );
480 }
481
482 if (shouldCompactReexports(this, options)) {
483 this.ui.writeDebugLine('Compacting re-exports');
484
485 if (!this.options.babel.plugins) {
486 this.options.babel.plugins = [];
487 }
488
489 appendCompactReexportsIfNeeded(this.options.babel.plugins);
490 } else {
491 this.ui.writeDebugLine('Not compacting re-exports');
492 }
493
494 // Change the host inside of engines.
495 // Support pre-2.7 Ember CLI.
496 let originalFindHost = this._findHost || findRoot;
497
498 // Unfortunately results in duplication, but c'est la vie.
499 this._findHost = findHost;
500
501 this.otherAssetPaths = [];
502 this._scriptOutputFiles = {};
503 this._styleOutputFiles = {};
504 this._nodeModules = new Map();
505 this._customTransformsMap = new Map();
506 this.shouldIncludeAddon = () => true;
507
508 // Determines if this Engine or any of its parents are lazy
509 this._hasLazyAncestor = findHost.call(this) !== findRoot.call(this);
510
511 this._processedExternalTree = function() {
512 return buildExternalTree.call(this);
513 };
514
515 this.import = function(asset, options) {
516 let host = originalFindHost.call(this);
517 let target = this._findHost();
518
519 target.env = host.env;
520 target._import = host._import;
521 target._getAssetPath = host._getAssetPath;
522 target.otherAssets = host.otherAssets;
523 target._mergeTrees = host._mergeTrees;
524
525 // We're delegating to the upstream EmberApp behavior for eager engines.
526 if (this.lazyLoading.enabled !== true) {
527 // Asset can be an object with environment properties.
528 asset = target._getAssetPath(asset);
529
530 // The asset path can be undefined depending on `env`.
531 if (typeof asset !== 'string') {
532 return;
533 }
534
535 // This is hard-coded in Ember CLI, not tied to treePaths.
536 asset.replace(/^vendor/, '');
537 }
538 return host.import.call(target, asset, options);
539 };
540
541 let originalIncluded = this.included;
542 this.included = function() {
543 if (this.lazyLoading.enabled === true) {
544 // Do this greedily so that it runs before the `included` hook.
545 this.import('engines-dist/' + this.name + '/assets/engine-vendor.js');
546 this.import(
547 'engines-dist/' + this.name + '/assets/engine-vendor.css'
548 );
549
550 let host = originalFindHost.call(this);
551 host._importAddonTransforms.call(this);
552 }
553
554 /**
555 ALERT! This changes the semantics of `app` inside of an engine.
556 For maximum compatibility inside of the `included(app)` call we always
557 supply the host application as `app`.
558
559 If you _truly_ want your immediate parent you should access it via
560 `this.parent`.
561 */
562 if (originalIncluded) {
563 let ui = this.ui;
564 let name = this.name;
565 let host = originalFindHost.call(this);
566 let originalHostImport = host.import;
567 let customHost = Object.create(host);
568 if (!process.env.SUPPRESS_APP_IMPORT_WARNING) {
569 customHost.import = function() {
570 let stack = new Error().stack;
571 ui.writeWarnLine(
572 '`app.import` should be avoided and `this.import` should be used instead. ' +
573 'Using `app.import` forces the asset in question to be hoisted in all scenarios (' +
574 'regardless of `lazyLoading.enabled` flag).\n\n Import performed on `' +
575 name +
576 "`'s `app` argument at:\n\n " +
577 stack +
578 '\n'
579 );
580 return originalHostImport.apply(this, arguments);
581 };
582 }
583 originalIncluded.call(this, customHost);
584 }
585
586 this.nonDuplicatedAddonInvoke('included', [this]);
587 };
588
589 // The treeForEngine method constructs and returns a tree that represents
590 // the engines routes.js file and its dependencies. This is used later to
591 // promote the engines routes.js to the host.
592 //
593 // If the lazyLoading.includeRoutesInApplication option is false, we don't
594 // want to promote the routes into the host.
595 this.treeForEngine = function() {
596 let extractRoutes =
597 this._hasLazyAncestor &&
598 this.lazyLoading.includeRoutesInApplication !== false;
599 if (!extractRoutes) {
600 return;
601 }
602
603 // The only thing that we want to promote from a lazy engine is the
604 // routes.js file and all of its dependencies, which is why we build the
605 // complete JS tree.
606 let completeJSTree = buildCompleteJSTree.call(this);
607
608 // Splice out the routes.js file and its dependencies.
609 // We will push these into the host application.
610 let engineRoutesTree = new DependencyFunnel(completeJSTree, {
611 include: true,
612 entry: this.name + '/routes.js',
613 external: ['ember-engines/routes'],
614 });
615
616 // They need to be in the modules directory for later processing.
617 return engineRoutesTree;
618 };
619
620 // Replace `treeForAddon` so that we control how this engine gets built.
621 // We may or may not want it to be combined like a default addon.
622 this.treeForAddon = function() {
623 if (this.lazyLoading.enabled === true) {
624 return;
625 }
626
627 // NOT LAZY LOADING!
628 // This is the scenario where we want to act like an addon.
629 let engineCSSTree = buildEngineStyleTree.call(this);
630
631 let compiledEngineCSSTree = this.debugTree(
632 this.compileStyles(engineCSSTree),
633 'styles'
634 );
635
636 // If any of this engine's ancestors are lazy we need to
637 // remove the engine's routes file, because we've already accounted
638 // for our route map file with the `treeForEngine` hook.
639 let engineJSTree = this._hasLazyAncestor
640 ? buildEngineJSTreeWithoutRoutes.call(this)
641 : buildEngineJSTree.call(this);
642
643 // Move the Engine tree to `modules`
644 engineJSTree = new Funnel(engineJSTree, {
645 destDir: 'modules',
646 });
647
648 // We only calculate our external tree for eager engines
649 // if they're going to be consumed by a lazy engine.
650 let externalTree;
651 let hostLazyLoading = this._findHost().lazyLoading;
652 if (hostLazyLoading && hostLazyLoading.enabled === true) {
653 externalTree = buildExternalTree.call(this);
654 }
655
656 return mergeTrees(
657 [externalTree, engineJSTree, compiledEngineCSSTree].filter(Boolean),
658 { overwrite: true }
659 );
660 };
661
662 this.compileLazyEngineStyles = function compileLazyEngineStyles(
663 vendorTree,
664 externalTree
665 ) {
666 let vendorCSSTree = buildVendorCSSTree.call(this, vendorTree);
667 let engineStylesOutputDir = 'engines-dist/' + this.name + '/assets/';
668
669 let hostOptions = findHostsHost.call(this).options || {};
670 let sourceMapConfig = hostOptions.sourcemaps;
671
672 // if the user specified `minifyCSS.enabled` at all, use their value
673 // otherwise default to `true` when in production
674 let shouldMinifyCSS =
675 'enabled' in this.options.minifyCSS
676 ? this.options.minifyCSS.enabled
677 : process.env.EMBER_ENV === 'production';
678 let minificationOptions = this.options.minifyCSS.options || {};
679 minificationOptions.registry = this.registry;
680
681 // get engines own addon-styles tree
682 let engineStylesTree = buildEngineStyleTree.call(this);
683
684 let primaryStyleTree = null;
685 if (engineStylesTree) {
686 let preprocessedEngineStylesTree = this._addonPreprocessTree(
687 'css',
688 engineStylesTree
689 );
690
691 let processedEngineStylesTree = preprocessCss(
692 preprocessedEngineStylesTree,
693 /* inputPath */ '/',
694 /* outputPath */ '/',
695 {
696 outputPaths: { addon: engineStylesOutputDir + 'engine.css' },
697 registry: this.registry,
698 }
699 );
700
701 processedEngineStylesTree = this.debugTree(
702 processedEngineStylesTree,
703 'engine-style:postprocessed'
704 );
705
706
707 primaryStyleTree = this._concatStyles(
708 'engine',
709 processedEngineStylesTree,
710 sourceMapConfig
711 );
712
713 primaryStyleTree = this.debugTree(
714 primaryStyleTree,
715 'engine-style:post-concat'
716 );
717
718 if (shouldMinifyCSS) {
719 primaryStyleTree = preprocessMinifyCss(
720 primaryStyleTree,
721 minificationOptions
722 );
723 }
724
725 primaryStyleTree = this.debugTree(
726 primaryStyleTree,
727 'engine-style:output'
728 );
729 }
730
731 let concatVendorCSSTree = this._concatStyles(
732 'engine-vendor',
733 vendorCSSTree,
734 sourceMapConfig
735 );
736
737 concatVendorCSSTree = this.debugTree(
738 concatVendorCSSTree,
739 'vendor-style:pre-import'
740 );
741
742 let concatMergedVendorCSSTree = mergeTrees([
743 concatVendorCSSTree,
744 externalTree,
745 ]);
746
747 // So, this is weird, but imports are processed in order.
748 // This gives the chance for somebody to prepend onto the vendor files.
749 let vendorCSSImportTree = buildVendorCSSWithImports.call(
750 this,
751 concatMergedVendorCSSTree,
752 sourceMapConfig
753 );
754
755 if (shouldMinifyCSS) {
756 vendorCSSImportTree = preprocessMinifyCss(
757 vendorCSSImportTree,
758 minificationOptions
759 );
760 }
761
762 let mergedVendorCSSWithImportAndEngineStylesTree = mergeTrees(
763 [vendorCSSImportTree, primaryStyleTree].filter(Boolean),
764 { overwrite: true }
765 );
766
767 let combinedProcessedStylesTree = new Funnel(
768 mergedVendorCSSWithImportAndEngineStylesTree,
769 {
770 srcDir: 'engines-dist/',
771 destDir: 'engines-dist/',
772 }
773 );
774
775 // run post processing via the `postprocessTree` hook on the final output
776 let finalStylesTree = this._addonPostprocessTree(
777 'css',
778 combinedProcessedStylesTree
779 );
780
781 return this.debugTree(finalStylesTree, 'styles');
782 };
783
784 // We want to do the default `treeForPublic` behavior if we're not a lazy loading engine.
785 // If we are a lazy loading engine we now have to manually do the compilation steps for the engine.
786 // Luckily the public folder gets merged into the right place in the final output.
787 // We'll take advantage of that.
788 let originalTreeForPublic = this.treeForPublic;
789 this.treeForPublic = function() {
790 let hostOptions = findHostsHost.call(this).options || {};
791 let sourceMapConfig = hostOptions.sourcemaps;
792
793 let configTemplatePath = path.join(
794 __dirname,
795 '/engine-config-node-module.js'
796 );
797 let configTemplate = fs.readFileSync(configTemplatePath, {
798 encoding: 'utf8',
799 });
800
801 let configEnvironment = writeFile(
802 'engines-dist/' + options.name + '/config/environment.js',
803 configTemplate
804 .replace('{{MODULE_PREFIX}}', options.name)
805 .replace('{{CONFIG}}', JSON.stringify(this.engineConfig()))
806 );
807
808 // NOT LAZY LOADING!
809 // In this scenario we just want to do the default behavior and bail.
810 if (this.lazyLoading.enabled !== true) {
811 let originalTree = originalTreeForPublic.apply(this, arguments);
812 return mergeTrees([configEnvironment, originalTree]);
813 }
814
815 // LAZY LOADING!
816 // But we have to implement everything manually for the lazy loading scenario.
817
818 let vendorTree = buildVendorTree.call(this);
819 let vendorJSTree = buildVendorJSTree.call(this, vendorTree);
820 let externalTree = new Funnel(
821 mergeTrees(this.nonDuplicatedAddonInvoke('treeFor', ['vendor']), {
822 overwrite: true,
823 annotation: `Lazy-engine#treeFor (${options.name} vendor)`,
824 }),
825 {
826 destDir: 'vendor',
827 allowEmpty: true,
828 }
829 );
830
831 let finalStylesTree = this.compileLazyEngineStyles(
832 vendorTree,
833 externalTree
834 );
835
836 // Move the public tree. It is already all in a folder named `this.name`
837 let publicResult = originalTreeForPublic.apply(this, arguments);
838 let publicRelocated;
839 if (publicResult) {
840 publicRelocated = new Funnel(publicResult, {
841 destDir: 'engines-dist',
842 });
843 }
844
845 // Get the child addons public trees.
846 // Sometimes this will be an engine tree in which case we need to handle it differently.
847 let childAddonsPublicTrees = this.nonDuplicatedAddonInvoke('treeFor', [
848 'public',
849 ]);
850 let childAddonsPublicTreesMerged = mergeTrees(childAddonsPublicTrees, {
851 overwrite: true,
852 });
853 let childLazyEngines = new Funnel(childAddonsPublicTreesMerged, {
854 srcDir: 'engines-dist',
855 destDir: 'engines-dist',
856 allowEmpty: true,
857 });
858 let childAddonsPublicTreesRelocated = new Funnel(
859 childAddonsPublicTreesMerged,
860 {
861 exclude: ['engines-dist', 'engines-dist/**/*.*'],
862 destDir: 'engines-dist/' + this.name,
863 }
864 );
865 let addonsEnginesPublicTreesMerged = mergeTrees(
866 [childLazyEngines, childAddonsPublicTreesRelocated],
867 { overwrite: true }
868 );
869
870 let engineJSTree = buildEngineJSTreeWithoutRoutes.call(this);
871
872 // If babel options aren't defined, we need to transpile the modules.
873 if (!this.options || !this.options.babel) {
874 engineJSTree = processBabel(engineJSTree);
875 vendorJSTree = processBabel(vendorJSTree);
876 }
877
878 // Concatenate all of the engine's JavaScript into a single file.
879
880 let concatEngineTree = concat(engineJSTree, {
881 allowNone: true,
882 inputFiles: ['**/*.js'],
883 outputFile: 'engines-dist/' + this.name + '/assets/engine.js',
884 sourceMapConfig,
885 });
886
887 // Concatenate all of the engine's "vendor" trees
888 let concatVendorJSTree = concat(vendorJSTree, {
889 allowNone: true,
890 inputFiles: ['**/*.js'],
891 outputFile: 'engines-dist/' + this.name + '/assets/engine-vendor.js',
892 sourceMapConfig,
893 });
894
895 let concatMergedVendorJSTree = mergeTrees([
896 concatVendorJSTree,
897 externalTree,
898 ]);
899
900 // So, this is weird, but imports are processed in order.
901 // This gives the chance for somebody to prepend onto the vendor files.
902 let vendorJSImportTree = buildVendorJSWithImports.call(
903 this,
904 concatMergedVendorJSTree,
905 sourceMapConfig
906 );
907
908 let otherAssets;
909 if (this.otherAssets) {
910 otherAssets = this.otherAssets();
911 }
912
913 // Merge all of our final trees!
914 let finalMergeTrees = [
915 configEnvironment,
916 publicRelocated,
917 addonsEnginesPublicTreesMerged,
918 otherAssets,
919 finalStylesTree,
920 vendorJSImportTree,
921 concatEngineTree,
922 ];
923
924 // Separate engines routes from the host if includeRoutesInApplication
925 // is false
926 let separateRoutes =
927 this.lazyLoading.includeRoutesInApplication === false;
928 if (separateRoutes) {
929 let engineRoutesTree = buildEngineRoutesJSTree.call(this, sourceMapConfig);
930 finalMergeTrees.push(engineRoutesTree);
931 }
932
933 return mergeTrees(finalMergeTrees.filter(Boolean), {
934 overwrite: true,
935 });
936 };
937
938 return result;
939 };
940
941 /**
942 Returns configuration settings that will augment the application's
943 configuration settings.
944
945 By default, engines return `null`, and maintain their own separate
946 configuration settings which are retrieved via `engineConfig()`.
947
948 @public
949 @method config
950 @param {String} env Name of current environment (e.g. "developement")
951 @param {Object} baseConfig Initial application configuration
952 @return {Object} Configuration object to be merged with application configuration.
953 */
954 options.config =
955 options.config ||
956 function() {
957 return null;
958 };
959
960 /**
961 Returns an engine's configuration settings, to be used exclusively by the
962 engine.
963
964 By default, this method simply reads the configuration settings from
965 an engine's `config/environment.js`.
966
967 @public
968 @method engineConfig
969 @return {Object} Configuration object that will be provided to the engine.
970 */
971 options.engineConfig = function(env, baseConfig) {
972 let configPath = 'config';
973
974 if (
975 this.pkg['ember-addon'] &&
976 this.pkg['ember-addon']['engineConfigPath']
977 ) {
978 configPath = this.pkg['ember-addon']['engineConfigPath'];
979 }
980
981 configPath = path.join(this.root, configPath, 'environment.js');
982
983 let key = `${configPath}|${env}`;
984
985 if (this._engineConfig.has(key)) {
986 return this._engineConfig.get(key);
987 }
988
989 let config;
990 if (fs.existsSync(configPath)) {
991 let configGenerator = require(configPath);
992 let engineConfig = configGenerator(env, baseConfig);
993 let addonsConfig = this.getAddonsConfig(env, engineConfig);
994
995 config = merge(addonsConfig, engineConfig);
996 } else {
997 config = this.getAddonsConfig(env, {});
998 }
999 this._engineConfig.set(key, config);
1000 return config;
1001 };
1002
1003 /**
1004 Returns the addons' configuration.
1005
1006 @private
1007 @method getAddonsConfig
1008 @param {String} env Environment name
1009 @param {Object} engineConfig Engine configuration
1010 @return {Object} Merged configuration of all addons
1011 */
1012 options.getAddonsConfig = function(env, engineConfig) {
1013 this.initializeAddons();
1014
1015 let initialConfig = merge({}, engineConfig);
1016
1017 return this.addons.reduce((config, addon) => {
1018 if (addon.config) {
1019 merge(config, addon.config(env, config));
1020 }
1021
1022 return config;
1023 }, initialConfig);
1024 };
1025
1026 /**
1027 Overrides the content provided for the `head` section to include
1028 the engine's configuration settings as a meta tag.
1029
1030 @public
1031 @method contentFor
1032 @param type
1033 */
1034 options.contentFor = function(type, config) {
1035 if (type === 'head') {
1036 let engineConfig = this.engineConfig(config.environment, {});
1037
1038 let content =
1039 '<meta name="' +
1040 options.name +
1041 '/config/environment" ' +
1042 'content="' +
1043 escape(JSON.stringify(engineConfig)) +
1044 '" />';
1045
1046 return content;
1047 }
1048
1049 return '';
1050 };
1051
1052 /**
1053 * When using ember-cli-fastboot, this will ensure a lazy Engine's assets
1054 * are loaded into the FastBoot sandbox.
1055 *
1056 * @override
1057 */
1058 options.updateFastBootManifest = function(manifest) {
1059 if (this.lazyLoading.enabled) {
1060 manifest.vendorFiles.push(
1061 'engines-dist/' + options.name + '/assets/engine-vendor.js'
1062 );
1063 manifest.vendorFiles.push(
1064 'engines-dist/' + options.name + '/assets/engine.js'
1065 );
1066 }
1067
1068 manifest.vendorFiles.push(
1069 'engines-dist/' + options.name + '/config/environment.js'
1070 );
1071
1072 return manifest;
1073 };
1074
1075 /**
1076 Returns the contents of the module to be used for accessing the Engine's
1077 config.
1078
1079 @public
1080 @method getEngineConfigContents
1081 @return {String}
1082 */
1083 options.getEngineConfigContents =
1084 options.getEngineConfigContents ||
1085 function() {
1086 const configTemplate = fs.readFileSync(`${__dirname}/engine-config-from-meta.js`, 'UTF8');
1087
1088 return configTemplate.replace('{{MODULE_PREFIX}}', options.name);
1089 };
1090
1091 /**
1092 Returns the appropriate set of trees for this engine's child addons
1093 given a type of tree. It will merge these trees (if present) with the
1094 engine's tree of the same name.
1095
1096 @public
1097 @method treeFor
1098 @param {String} name
1099 @return {Tree}
1100 */
1101 options.treeFor = function treeFor(name, source) {
1102 // @TODO: remove once support for ember-cli@2.x is dropped.
1103 // https://github.com/ember-cli/ember-cli/pull/8264
1104 this._requireBuildPackages && this._requireBuildPackages();
1105
1106 let cacheKey;
1107 if (HAS_TREE_CACHE) {
1108 cacheKey = this.cacheKeyForTree(name);
1109 let cachedTree = Addon._treeCache.getItem(cacheKey);
1110 if (cachedTree) {
1111 return cachedTree;
1112 }
1113 }
1114
1115 /**
1116 Scenarios where we don't want to call `eachAddonInvoke`:
1117 - app tree.
1118 - addon tree of a lazy engine.
1119 - public tree of a lazy engine.
1120
1121 We handle these cases manually inside of treeForPublic.
1122 This is to consolidate child dependencies of this engine
1123 into the engine namespace as opposed to shoving them into
1124 the host application's vendor.js file.
1125 */
1126
1127 let trees;
1128 if (
1129 name === 'app' ||
1130 name === 'styles' ||
1131 (name === 'addon' && this.lazyLoading.enabled === true) ||
1132 (name === 'vendor' && this.lazyLoading.enabled === true) ||
1133 (name === 'public' && this.lazyLoading.enabled === true)
1134 ) {
1135 trees = [];
1136 } else {
1137 trees = this.eachAddonInvoke('treeFor', [name]);
1138 }
1139
1140 if (name === 'addon' && source !== 'buildVendorTree') {
1141 trees = trees.concat(this.eachAddonInvoke('treeForEngine', [this]));
1142 trees.push(this.treeForEngine());
1143 }
1144
1145 // The rest of this is the default implementation of `treeFor`.
1146
1147 let tree = this._treeFor(name);
1148
1149 if (tree) {
1150 trees.push(tree);
1151 }
1152
1153 if (this.isDevelopingAddon() && this.hintingEnabled() && name === 'app') {
1154 trees.push(this.jshintAddonTree());
1155 }
1156
1157 let mergedTree = mergeTrees(trees.filter(Boolean), {
1158 overwrite: true,
1159 annotation: 'Engine#treeFor (' + options.name + ' - ' + name + ')',
1160 });
1161
1162 if (HAS_TREE_CACHE) {
1163 Addon._treeCache.setItem(cacheKey, mergedTree);
1164 }
1165
1166 return mergedTree;
1167 };
1168
1169 return options;
1170 },
1171};