treeForAddon() {
- var tree = this._super.treeForAddon.apply(this, arguments);
- var checker = new VersionChecker(this);
- var isOldEmber = checker.for('ember', 'bower').lt('1.13.0');
+ let emberVersion = new VersionChecker(this.project).for('ember-source');
+ let shouldUsePolyfill = emberVersion.lt('4.5.0-alpha.4');
- if (isOldEmber) {
- tree = new Funnel(tree, { exclude: [ /instance-initializers/ ] });
+ if (shouldUsePolyfill) {
+ return this._super.treeForAddon.apply(this, arguments);
}
-
- return tree;
}
Indicates whether or not a blueprint is a candidate for automatic transpilation from TS to JS. This property could be false in the case that the blueprint is written in JS and is not intended to work with TS at all, OR in the case that the blueprint is written in TS and the author does not intend to support transpilation to JS.
+ public
+
+
+ shouldTransformTypeScript: Boolean
+
+
+
+
+
Indicates whether or not a blueprint is a candidate for automatic transpilation from TS to JS.
+This property could be false in the case that the blueprint is written in JS and is not intended
+to work with TS at all, OR in the case that the blueprint is written in TS and the author does
+not intend to support transpilation to JS.
- public
-
-
- addBowerPackagesToProject(packages, installOptions): Promise
-
-
-
-
-
-
Used to add an array of packages to the projects bower.json.
-
Generally, this would be done from the afterInstall hook, to
-ensure that a package that is required by a given blueprint is
-available.
-
Expects each array item to be an object with a name. Each object
-may optionally have a target to specify a specific version, or a
-source to specify a non-local name to be resolved.
- public
-
-
- addBowerPackageToProject(localPackageName, target, installOptions): Promise
-
-
-
-
-
-
Used to add a package to the projects bower.json.
-
Generally, this would be done from the afterInstall hook, to
-ensure that a package that is required by a given blueprint is
-available.
-
localPackageName and target may be thought of as equivalent
-to the key-value pairs in the dependency or devDepencency
-objects contained within a bower.json file.
Check if npm and bower installation directories are present, and raise an error message with instructions on how to proceed.
+
This returns the LCA host for a given engine; we use the associated package info to compute this (see getHostAddonInfo above); this finds the lowest common ancestor that is considered a host amongst all engines by the same name in the project. This function is intended to replace the original behavior in ember-engines.
+
+
+
+
+
+ public
+
+
+
+
+
+ getHostAddonInfo(packageInfoForLazyEngine): hostPackageInfo: PackageInfo, hostAndAncestorBundledPackageInfos: Set
+
+
+
This function intends to return a common host for a bundle host (lazy engine). The root package info should be the starting point (i.e., the project's package info). We do this by performing a breadth-first traversal until we find the intended lazy engine (represented as a package-info & the 1st argument passed to this function). As part of the traversal, we keep track of all paths to said engine; then, once we find the intended engine we use this to determine the nearest common host amongst all shortest paths.
Returns a Set of package-info objects that a given bundle host is directly responsible for bundling (i.e., it excludes other bundle hosts/lazy engines when it encounters these)
public
- checkInstallations( )
+ findLCAHost(engineInstance): EngineAddon | EmberApp
-
Check if npm and bower installation directories are present,
-and raise an error message with instructions on how to proceed.
-
If some of these package managers aren't being used in the project
-we just ignore them. Their usage is considered by checking the
-presence of your manifest files: package.json for npm and bower.json for bower.
+
This returns the LCA host for a given engine; we use the associated package info
+to compute this (see getHostAddonInfo above); this finds the lowest common ancestor
+that is considered a host amongst all engines by the same name in the project. This
+function is intended to replace the original behavior in ember-engines.
+
For more info, see the original implementation here:
+ public
+
+
+ getHostAddonInfo(packageInfoForLazyEngine): hostPackageInfo: PackageInfo, hostAndAncestorBundledPackageInfos: Set
+
+
+
+
+
+
This function intends to return a common host for a bundle host (lazy engine). The root
+package info should be the starting point (i.e., the project's package info). We do this
+by performing a breadth-first traversal until we find the intended lazy engine (represented
+as a package-info & the 1st argument passed to this function). As part of the traversal, we keep
+track of all paths to said engine; then, once we find the intended engine we use this to determine
+the nearest common host amongst all shortest paths.
+
Some context:
+
For a given engine/bundle host, this finds the lowest common ancestor that is considered a
+host amongst all engines by the same name in the project.
+
For example, given the following package structure:
+
--Project--
+ / \
+ / \
+
+
Lazy Engine A
+Addon A
+|
+|
+Lazy Engine B
+/
+/
+Lazy Engine A Lazy Engine C
+
+
The LCA host for Lazy Engine A is the project
+
The LCA host for Lazy Engine B is the project
+
The LCA host for Lazy Engine C is Lazy Engine B
+
+
This also returns hostAndAncestorBundledPackageInfos, which are all bundled addons above a given host:
+
+
hostAndAncestorBundledPackageInfos for lazy engine A includes all non-lazy dependencies of its LCA host & above (in this case, just the project)
+
hostAndAncestorBundledPackageInfos for lazy engine B includes all non-lazy dependencies of its LCA host & above (in this case, just the project)
+
hostAndAncestorBundledPackageInfos for lazy engine C includes non-lazy deps of lazy engine B & non-lazy deps of the project (LCA host & above)
Unfortunately, we can't easily repurpose the logic in ember-engines since the algorithm has to be different;
+in ember-engines we need access to the actual addon instance, however, this is intended to be used during
+addon instantiation, so we only have access to package-info objects. In having said this, we can repurpose
+the hostPackageInfo to determine the LCA host; see below findLCAHost.
+ private
+
+
+ _getBundledPackageInfos(pkgInfoToStartAt): Set
+
+
+
+
+
+
Returns a Set of package-info objects that a given bundle host is
+directly responsible for bundling (i.e., it excludes other bundle
+hosts/lazy engines when it encounters these)
This is only supposed to be called by the addon instantiation code. Also, the assumption here is that this PackageInfo really is for an Addon, so we don't need to check each time.
+
Create an instance of the addon represented by this packageInfo or (if we are supporting per-bundle caching and this is an allow-caching-per-bundle addon) check if we should be creating a proxy instead.
Initialize the child addons array of a newly-created addon instance. Normally when an addon derives from Addon, child addons will be created during 'setupRegistry' and this code is essentially unnecessary. But if an addon is created with custom constructors that don't call 'setupRegistry', any child addons may not yet be initialized.
Indicate if this packageInfo is for a project. Should be called only after the project has been loaded (see {@link PackageInfoCache#loadProject} for details).
This is only supposed to be called by the addon instantiation code. Also, the assumption here is that this PackageInfo really is for an Addon, so we don't need to check each time.
public
- getAddonConstructor( ): AddonConstructor
+ getAddonInstance(parent, project): Object
-
This is only supposed to be called by the addon instantiation code.
-Also, the assumption here is that this PackageInfo really is for an
-Addon, so we don't need to check each time.
+
Create an instance of the addon represented by this packageInfo or (if we
+are supporting per-bundle caching and this is an allow-caching-per-bundle addon)
+check if we should be creating a proxy instead.
+
NOTE: we assume that the value of 'allowCachingPerBundle' does not change between
+calls to the constructor! A given addon is either allowing or not allowing caching
+for an entire run.
Initialize the child addons array of a newly-created addon instance. Normally when
+an addon derives from Addon, child addons will be created during 'setupRegistry' and
+this code is essentially unnecessary. But if an addon is created with custom constructors
+that don't call 'setupRegistry', any child addons may not yet be initialized.
Indicate if this packageInfo is for a project. Should be called only after the project
+has been loaded (see {@link PackageInfoCache#loadProject} for details).
This is only supposed to be called by the addon instantiation code.
+Also, the assumption here is that this PackageInfo really is for an
+Addon, so we don't need to check each time.
+
+
+
+
+
Return:
+
+ AddonConstructor
+
an instance of a constructor function for the Addon class
+whose package information is stored in this object.
Verify that a certain condition is met, or throw an error if otherwise.
+
This is useful for communicating expectations in the code to other human
+readers as well as catching bugs that accidentally violate these expectations.
+
const { assert } = require('ember-cli/lib/debug');
+
+// Test for truthiness:
+assert('Must pass a string.', typeof str === 'string');
+
+// Fail unconditionally:
+assert('This code path should never run.');
+
For large applications with many addons (and many instances of each, resulting in
+potentially many millions of addon instances during a build), the build can become
+very, very slow (tens of minutes) partially due to the sheer number of addon instances.
+The PerBundleAddonCache deals with this slowness by doing 3 things:
+
(1) Making only a single copy of each of certain addons and their dependent addons
+(2) Replacing any other instances of those addons with Proxy copies to the single instance
+(3) Having the Proxies return an empty array for their dependent addons, rather
+than proxying to the contents of the single addon instance. This gives up the
+ability of the Proxies to traverse downward into their child addons,
+something that many addons do not do anyway, for the huge reduction in duplications
+of those child addons. For applications that enable ember-engines dedupe logic,
+that logic is stateful, and having the Proxies allow access to the child addons array
+just breaks everything, because that logic will try multiple times to remove items
+it thinks are duplicated, messing up the single copy of the child addon array.
+See the explanation of the dedupe logic in
+{@link https://github.com/ember-engines/ember-engines/blob/master/packages/ember-engines/lib/utils/deeply-non-duplicated-addon.js}
+
What follows are the more technical details of how the PerBundleAddonCache implements
+the above 3 behaviors.
+
This class supports per-bundle-host (bundle host = project or lazy engine)
+caching of addon instances. During addon initialization we cannot add a
+cache to each bundle host object AFTER it is instantiated because running the
+addon constructor ultimately causes Addon class setupRegistry code to
+run which instantiates child addons, which need the cache to already be
+in place for the parent bundle host.
+We handle this by providing a global cache that exists independent of the
+bundle host objects. That is this object.
+
There are a number of "behaviors" being implemented by this object and
+its contents. They are:
+(1) Any addon that is a lazy engine has only a single real instance per
+project - all other references to the lazy engine are to be proxies. These
+lazy engines are compared by name, not by packageInfo.realPath.
+(2) Any addon that is not a lazy engine, there is only a single real instance
+of the addon per "bundle host" (i.e. lazy engine or project).
+(3) An optimization - any addon that is in a lazy engine but that is also
+in bundled by its LCA host - the single instance is the one bundled by this
+host. All other instances (in any lazy engine) are proxies.
The default implementation here is to indicate if the original addon entry point has the allowCachingPerBundle flag set either on itself or on its prototype.
Creates a cache entry for the bundleHostCache. Because we want to use the same sort of proxy for both bundle hosts and for 'regular' addon instances (though their cache entries have slightly different structures) we'll use the Symbol from getAddonProxy.
Given a parent object of a potential addon (another addon or the project), go up the 'parent' chain to find the potential addon's bundle host object (i.e. lazy engine or project.) Because Projects are always bundle hosts, this should always pass, but we'll throw if somehow it doesn't work.
Returns a proxy to a target with specific handling for the parent property, as well has to handle the app property; that is, the proxy should maintain correct local state in closure scope for the app property if it happens to be set by ember-cli. Other than parent & app, this function also proxies almost everything to target[TARGET_INSTANCE] with a few exceptions: we trap & return[]foraddons, and we don't return the originalincluded(it's already called on the "real" addon byember-cli`).
Resolves the perBundleAddonCacheUtil; this prefers the custom provided version by the consuming application, and defaults to an internal implementation here.
Validates that a new cache key for a given tree type matches the previous cache key for the same tree type. To opt-in to bundle addon caching for a given addon it's assumed that it returns stable cache keys; specifically this is because the interplay between bundle addon caching and ember-engines when transitive deduplication is enabled assumes stable cache keys, so we validate for this case here.
+ public
+
+
+ allowCachingPerBundle(addonEntryPointModule): Boolean
+
+
+
+
+
+
The default implementation here is to indicate if the original addon entry point has
+the allowCachingPerBundle flag set either on itself or on its prototype.
+
If a consuming application specifies a relative path to a custom utility via the
+ember-addon.perBundleAddonCacheUtil configuration, we prefer the custom implementation
+provided by the consumer.
+ public
+
+
+ bundleHostOwnsInstance((Object}, addonPkgInfo): Boolean
+
+
+
+
+
+
An optimization we support from lazy engines is the following:
+
If an addon instance is supposed to be bundled with a particular lazy engine, and
+same addon is also to be bundled by a common LCA host, prefer the one bundled by the
+host (since it's ultimately going to be deduped later by ember-engines).
+
NOTE: this only applies if this.engineAddonTransitiveDedupeEnabled is truthy. If it is not,
+the bundle host always "owns" the addon instance.
+
If deduping is enabled and the LCA host also depends on the same addon,
+the lazy-engine instances of the addon will all be proxies to the one in
+the LCA host. This function indicates whether the bundle host passed in
+(either the project or a lazy engine) is really the bundle host to "own" the
+new addon.
+ public
+
+
+ createBundleHostCacheEntry(bundleHostPkgInfo): Object
+
+
+
+
+
+
Creates a cache entry for the bundleHostCache. Because we want to use the same sort of proxy
+for both bundle hosts and for 'regular' addon instances (though their cache entries have
+slightly different structures) we'll use the Symbol from getAddonProxy.
+ public
+
+
+ findBundleHost(addonParent, addonPkgInfo): Object
+
+
+
+
+
+
Given a parent object of a potential addon (another addon or the project),
+go up the 'parent' chain to find the potential addon's bundle host object
+(i.e. lazy engine or project.) Because Projects are always bundle hosts,
+this should always pass, but we'll throw if somehow it doesn't work.
An addon instance (for the first copy of the addon) or a Proxy.
+An addon that is a lazy engine will only ever have a single copy in the cache.
+An addon that is not will have 1 copy per bundle host (Project or lazy engine),
+except if it is an addon that's also owned by a given LCA host and transitive
+dedupe is enabled (engineAddonTransitiveDedupeEnabled), in which case it will
+only have a single copy in the project's addon cache.
+ public
+
+
+ getAddonProxy(targetCacheEntry, parent):
+
+
+
+
+
+
Returns a proxy to a target with specific handling for the
+parent property, as well has to handle the app property;
+that is, the proxy should maintain correct local state in
+closure scope for the app property if it happens to be set
+by ember-cli. Other than parent & app, this function also
+proxies almost everything to target[TARGET_INSTANCE] with a few exceptions: we trap & return[]foraddons, and we don't return the originalincluded(it's already called on the "real" addon byember-cli`).
+
Note: the target is NOT the per-bundle cacheable instance of the addon. Rather,
+it is a cache entry POJO from PerBundleAddonCache.
the PerBundleAddonCache cache entry we are to proxy. It
+has one interesting property, the real addon instance the proxy is forwarding
+calls to (that property is not globally exposed).
+ public
+
+
+ resolvePerBundleAddonCacheUtil(project): AllowCachingPerBundle: Function
+
+
+
+
+
+
Resolves the perBundleAddonCacheUtil; this prefers the custom provided version by
+the consuming application, and defaults to an internal implementation here.
+ public
+
+
+ validateCacheKey(realAddonInstance, treeType, newCacheKey)
+
+
+
+
+
+
Validates that a new cache key for a given tree type matches the previous
+cache key for the same tree type. To opt-in to bundle addon caching for
+a given addon it's assumed that it returns stable cache keys; specifically
+this is because the interplay between bundle addon caching and ember-engines
+when transitive deduplication is enabled assumes stable cache keys, so we validate
+for this case here.
Returns a new project based on the first package.json that is found
+
Returns a new project based on the first package.json that is found
in pathName.
+
If the above package.json specifies ember-addon.projectRoot, we load
+the project based on the relative path between this directory and the
+specified projectRoot.
Set when the Watcher.detectWatchman helper method finishes running,
so that other areas of the system can be aware that watchman is being used.
For example, this information is used in the broccoli build pipeline to know
-if we can watch additional directories (like bower_components) "cheaply".
+if we can watch additional directories "cheaply".
if not windows, will fulfill with: { windows: false, elevated: null) if windows, and elevated will fulfill with: { windows: false, elevated: true) if windows, and is NOT elevated will fulfill with: { windows: false, elevated: false) will include heplful warning, so that users know (if possible) how to achieve better windows build performance
if not windows, will fulfill with: { windows: false, elevated: null) if windows, and elevated will fulfill with: { windows: false, elevated: true) if windows, and is NOT elevated will fulfill with: { windows: false, elevated: false) will include heplful warning, so that users know (if possible) how to achieve better windows build performance
this._importAddonTransforms();
this._notifyAddonIncluded();
- if (!this._addonInstalled('loader.js') && !this.options._ignoreMissingLoader) {
- throw new SilentError('The loader.js addon is missing from your project, please add it to `package.json`.');
- }
-
this._debugTree = BroccoliDebug.buildDebugCallback('ember-app');
this._defaultPackager = new DefaultPackager({
@@ -253,8 +230,31 @@
lib/broccoli/ember-app.js
},
});
- this._isPackageHookSupplied = typeof this.options.package === 'function';
this._cachedAddonBundles = {};
+
+ if (this.project.perBundleAddonCache && this.project.perBundleAddonCache.numProxies > 0) {
+ if (this.options.addons.include && this.options.addons.include.length) {
+ throw new Error(
+ [
+ `[ember-cli] addon bundle caching is disabled for apps that specify an addon "include"`,
+ '',
+ 'All addons using bundle caching:',
+ ...this.project.perBundleAddonCache.getPathsToAddonsOptedIn(),
+ ].join('\n')
+ );
+ }
+
+ if (this.options.addons.exclude && this.options.addons.exclude.length) {
+ throw new Error(
+ [
+ `[ember-cli] addon bundle caching is disabled for apps that specify an addon "exclude"`,
+ '',
+ 'All addons using bundle caching:',
+ ...this.project.perBundleAddonCache.getPathsToAddonsOptedIn(),
+ ].join('\n')
+ );
+ }
+ }
}
/**
@@ -303,6 +303,20 @@
lib/broccoli/ember-app.js
@param {Object} options
*/
_initOptions(options) {
+ deprecate(
+ 'Using the `outputPaths` build option is deprecated, as output paths will no longer be predetermined under Embroider.',
+ typeof options.outputPaths === 'undefined',
+ {
+ for: 'ember-cli',
+ id: 'ember-cli.outputPaths-build-option',
+ since: {
+ available: '5.3.0',
+ enabled: '5.3.0',
+ },
+ until: '6.0.0',
+ }
+ );
+
let resolvePathFor = (defaultPath, specified) => {
let path = defaultPath;
if (specified && typeof specified === 'string') {
@@ -333,8 +347,6 @@
lib/broccoli/ember-app.js
let trees = (options && options.trees) || {};
let appTree = buildTreeFor('app', trees.app);
-
- let testsPath = typeof trees.tests === 'string' ? resolvePathFor('tests', trees.tests) : null;
let testsTree = buildTreeFor('tests', trees.tests, options.tests);
// these are contained within app/ no need to watch again
@@ -347,47 +359,15 @@
lib/broccoli/ember-app.js
}
let templatesTree = buildTreeFor('app/templates', trees.templates, false);
-
- // do not watch bower's default directory by default
- let bowerTree = buildTreeFor(this.bowerDirectory, null, !!this.project._watchmanInfo.enabled);
-
- // Set the flag to make sure:
- //
- // - we do not blow up if there is no bower_components folder
- // - we do not attempt to merge bower and vendor together if they are the
- // same tree
- this._bowerEnabled = this.bowerDirectory !== 'vendor' && fs.existsSync(this.bowerDirectory);
-
let vendorTree = buildTreeFor('vendor', trees.vendor);
let publicTree = buildTreeFor('public', trees.public);
let detectedDefaultOptions = {
babel: {},
- jshintrc: {
- app: this.project.root,
- tests: testsPath,
- },
minifyCSS: {
enabled: this.isProduction,
options: { processImport: false },
},
- // TODO: remove this with a deprecation (nothing in the default app/addon setup consumes it)
- minifyJS: {
- enabled: this.isProduction,
- options: {
- compress: {
- // this is adversely affects heuristics for IIFE eval
- // eslint-disable-next-line camelcase
- negate_iife: false,
- // limit sequences because of memory issues during parsing
- sequences: 30,
- },
- output: {
- // no difference in size and much easier to debug
- semicolons: false,
- },
- },
- },
outputPaths: {
app: {
css: {
@@ -405,7 +385,6 @@
this.options.fingerprint.exclude.push('testem');
}
- _emberCLIBabelConfigKey() {
- let emberCLIBabelInstance = this.project.findAddonByName('ember-cli-babel');
-
- return emberCLIBabelConfigKey(emberCLIBabelInstance);
- }
-
/**
Resolves a path relative to the project's root
@@ -463,118 +426,26 @@
lib/broccoli/ember-app.js
@method _initVendorFiles
*/
_initVendorFiles() {
- let bowerDeps = this.project.bowerDependencies();
- let ember = this.project.findAddonByName('ember-source');
- let addonEmberCliShims = this.project.findAddonByName('ember-cli-shims');
- let bowerEmberCliShims = bowerDeps['ember-cli-shims'];
- let developmentEmber;
- let productionEmber;
- let emberTesting;
- let emberShims = null;
-
- if (ember) {
- developmentEmber = ember.paths.debug;
- productionEmber = ember.paths.prod;
- emberTesting = ember.paths.testing;
- emberShims = ember.paths.shims;
- } else {
- if (bowerEmberCliShims) {
- emberShims = `${this.bowerDirectory}/ember-cli-shims/app-shims.js`;
- }
+ let emberSource = this.project.findAddonByName('ember-source');
- // in Ember 1.10 and higher `ember.js` is deprecated in favor of
- // the more aptly named `ember.debug.js`.
- productionEmber = `${this.bowerDirectory}/ember/ember.prod.js`;
- developmentEmber = `${this.bowerDirectory}/ember/ember.debug.js`;
- if (!fs.existsSync(this._resolveLocal(developmentEmber))) {
- developmentEmber = `${this.bowerDirectory}/ember/ember.js`;
- }
- emberTesting = `${this.bowerDirectory}/ember/ember-testing.js`;
- }
-
- let handlebarsVendorFiles;
- if ('handlebars' in bowerDeps) {
- handlebarsVendorFiles = {
- development: `${this.bowerDirectory}/handlebars/handlebars.js`,
- production: `${this.bowerDirectory}/handlebars/handlebars.runtime.js`,
- };
- } else {
- handlebarsVendorFiles = null;
- }
+ assert(
+ 'Could not find `ember-source`. Please install `ember-source` by running `ember install ember-source`.',
+ emberSource
+ );
this.vendorFiles = omitBy(
merge(
{
- 'handlebars.js': handlebarsVendorFiles,
'ember.js': {
- development: developmentEmber,
- production: productionEmber,
+ development: emberSource.paths.debug,
+ production: emberSource.paths.prod,
},
- 'ember-testing.js': [emberTesting, { type: 'test' }],
- 'app-shims.js': emberShims,
- 'ember-resolver.js': [
- `${this.bowerDirectory}/ember-resolver/dist/modules/ember-resolver.js`,
- {
- exports: {
- 'ember/resolver': ['default'],
- },
- },
- ],
+ 'ember-testing.js': [emberSource.paths.testing, { type: 'test' }],
},
this.options.vendorFiles
),
isNull
);
-
- this._addJqueryInLegacyEmber();
-
- if (this._addonInstalled('ember-resolver') || !bowerDeps['ember-resolver']) {
- // if the project is using `ember-resolver` as an addon
- // remove it from `vendorFiles` (the npm version properly works
- // without `app.import`s)
- delete this.vendorFiles['ember-resolver.js'];
- }
-
- // Warn if ember-cli-shims is not included.
- // certain versions of `ember-source` bundle them by default,
- // so we must check if that is the load mechanism of ember
- // before checking `bower`.
- let emberCliShimsRequired = this._checkEmberCliBabel(this.project.addons);
- if (!emberShims && !addonEmberCliShims && !bowerEmberCliShims && emberCliShimsRequired) {
- this.project.ui.writeWarnLine(
- "You have not included `ember-cli-shims` in your project's `bower.json` or `package.json`. This only works if you provide an alternative yourself and unset `app.vendorFiles['app-shims.js']`."
- );
- }
-
- // If ember-testing.js is coming from Bower (not ember-source) and it does not
- // exist, then we remove it from vendor files. This is needed to support versions
- // of Ember older than 1.8.0 (when ember-testing.js was incldued in ember.js itself)
- if (!ember && this.vendorFiles['ember-testing.js'] && !fs.existsSync(this.vendorFiles['ember-testing.js'][0])) {
- delete this.vendorFiles['ember-testing.js'];
- }
- }
-
- _addJqueryInLegacyEmber() {
- if (this.project.findAddonByName('@ember/jquery')) {
- return;
- }
- let ember = this.project.findAddonByName('ember-source');
- let jqueryPath;
- if (ember) {
- let optionFeatures = this.project.findAddonByName('@ember/optional-features');
- if (optionFeatures && !optionFeatures.isFeatureEnabled('jquery-integration')) {
- return;
- }
- this.project.ui.writeDeprecateLine(
- 'The integration of jQuery into Ember has been deprecated and will be removed with Ember 4.0. You can either' +
- ' opt-out of using jQuery, or install the `@ember/jquery` addon to provide the jQuery integration. Please' +
- ' consult the deprecation guide for further details: https://emberjs.com/deprecations/v3.x#toc_jquery-apis'
- );
- jqueryPath = ember.paths.jquery;
- } else {
- jqueryPath = `${this.bowerDirectory}/jquery/dist/jquery.js`;
- }
- this.vendorFiles = merge({ 'jquery.js': jqueryPath }, this.vendorFiles);
}
/**
@@ -618,52 +489,24 @@
_notifyAddonIncluded() {
let addonNames = this.project.addons.map((addon) => addon.name);
- if (this.options.addons.blacklist) {
- this.options.addons.blacklist.forEach((addonName) => {
+ if (this.options.addons.exclude) {
+ this.options.addons.exclude.forEach((addonName) => {
if (addonNames.indexOf(addonName) === -1) {
- throw new Error(`Addon "${addonName}" defined in blacklist is not found`);
+ throw new Error(`Addon "${addonName}" defined in "exclude" is not found`);
}
});
}
- if (this.options.addons.whitelist) {
- this.options.addons.whitelist.forEach((addonName) => {
+ if (this.options.addons.include) {
+ this.options.addons.include.forEach((addonName) => {
if (addonNames.indexOf(addonName) === -1) {
- throw new Error(`Addon "${addonName}" defined in whitelist is not found`);
+ throw new Error(`Addon "${addonName}" defined in "include" is not found`);
}
});
}
@@ -806,92 +649,6 @@
lib/broccoli/ember-app.js
return this._addonTreesFor(type).map((addonBundle) => addonBundle.tree);
}
- _getDefaultPluginForType(type) {
- let plugins = this.registry.load(type);
- let defaultsForType = plugins.filter((plugin) => plugin.isDefaultForType);
-
- if (defaultsForType.length > 1) {
- throw new Error(
- `There are multiple preprocessor plugins marked as default for '${type}': ${defaultsForType
- .map((p) => p.name)
- .join(', ')}`
- );
- }
-
- return defaultsForType[0];
- }
-
- _compileAddonTemplates(tree) {
- let defaultPluginForType = this._getDefaultPluginForType('template');
- let options = {
- annotation: `_compileAddonTemplates`,
- registry: this.registry,
- };
-
- if (defaultPluginForType) {
- tree = defaultPluginForType.toTree(tree, options);
- } else {
- tree = preprocessTemplates(tree, options);
- }
-
- return tree;
- }
-
- _compileAddonJs(tree) {
- let defaultPluginForType = this._getDefaultPluginForType('js');
- let options = {
- annotation: '_compileAddonJs',
- registry: this.registry,
- };
-
- if (defaultPluginForType) {
- tree = defaultPluginForType.toTree(tree, options);
- } else {
- tree = preprocessJs(tree, '/', '/', options);
- }
-
- return tree;
- }
-
- _compileAddonTree(tree, skipTemplates) {
- if (!skipTemplates) {
- tree = this._compileAddonTemplates(tree);
- }
- tree = this._compileAddonJs(tree);
-
- return tree;
- }
-
- _precompileAppJsTree(tree) {
- let emberCLIBabelConfigKey = this._emberCLIBabelConfigKey();
-
- let original = this.options[emberCLIBabelConfigKey];
-
- // the app will handle transpilation after it tree-shakes
- // do it here instead of the constructor because
- // ember-data and others do their own compilation in their
- // treeForAddon without calling super
- // they need the original params preserved because they call
- // babel themselves and expect compilation the old way
- this.options[emberCLIBabelConfigKey] = Object.assign({}, original, {
- compileModules: false,
- disablePresetEnv: true,
- disableDebugTooling: true,
- disableEmberModulesAPIPolyfill: true,
- });
-
- tree = preprocessJs(tree, '/', '/', {
- annotation: `_precompileAppJsTree`,
- registry: this.registry,
- });
-
- // return the original params because there are multiple
- // entrances to preprocessJs
- this.options[emberCLIBabelConfigKey] = original;
-
- return tree;
- }
-
/**
Runs addon post-processing on a given tree and returns the processed tree.
@@ -1070,7 +827,7 @@
);
let addons = this.addonTree();
-
let trees = [vendor].concat(addons);
- if (this._bowerEnabled) {
- let bower = this._defaultPackager.packageBower(this.trees.bower, this.bowerDirectory);
-
- trees.push(bower);
- }
trees = this._nodeModuleTrees().concat(trees);
@@ -1420,16 +1165,6 @@
lib/broccoli/ember-app.js
});
}
- /**
- * @private
- * @method _addonInstalled
- * @param {String} addonName The name of the addon we are checking to see if it's installed
- * @return {Boolean}
- */
- _addonInstalled(addonName) {
- return !!this.registry.availablePlugins[addonName];
- }
-
/**
@public
@method dependencies
@@ -1473,7 +1208,7 @@
lib/broccoli/ember-app.js
}
let directory = path.dirname(assetPath);
- let subdirectory = directory.replace(new RegExp(`^vendor/|${this.bowerDirectory}|node_modules/`), '');
+ let subdirectory = directory.replace(new RegExp(`^vendor/|node_modules/`), '');
let extension = path.extname(assetPath);
if (!extension) {
@@ -1498,7 +1233,7 @@
lib/broccoli/ember-app.js
// TODO: refactor, this has gotten very messy. Relevant tests: tests/unit/broccoli/ember-app-test.js
let basename = path.basename(assetPath);
- if (isType(assetPath, 'js', { registry: this.registry })) {
+ if (p.isType(assetPath, 'js', { registry: this.registry })) {
if (options.using) {
if (!Array.isArray(options.using)) {
throw new Error('You must pass an array of transformations for `using` option');
@@ -1625,77 +1360,10 @@
lib/broccoli/ember-app.js
this.getTests(),
this.getExternalTree(),
this.getPublic(),
- this.getAppJavascript(this._isPackageHookSupplied),
+ this.getAppJavascript(),
].filter(Boolean);
}
- _legacyAddonCompile(type, outputDir, _options) {
- let options = Object.assign(
- {
- // moduleNormalizerDisabled: this.options.moduleNormalizerDisabled,
- amdFunnelDisabled: this.options.amdFunnelDisabled,
- skipTemplates: false,
- },
- _options
- );
-
- let addonBundles = this._cachedAddonBundles[type];
-
- let addonTrees = addonBundles.map((addonBundle) => {
- let { name, tree, root } = addonBundle;
-
- let precompiledSource = tree;
-
- if (!options.amdFunnelDisabled) {
- // don't want to double compile the AMD modules
- let hasAlreadyPrintedAmdDeprecation;
- precompiledSource = new AmdFunnel(precompiledSource, {
- callback: () => {
- if (!hasAlreadyPrintedAmdDeprecation) {
- this.project.ui.writeDeprecateLine(
- `Addon "${name}" (found at "${root}") is manually generating AMD modules. Code should be ES6 modules only. Support for this will be removed in a future version.`
- );
- hasAlreadyPrintedAmdDeprecation = true;
- }
- },
- annotation: `AmdFunnel (${type} ${name})`,
- });
- }
-
- return [tree, precompiledSource];
- });
-
- let precompiledSource = addonTrees.map((pair) => pair[1]);
- addonTrees = addonTrees.map((pair) => pair[0]);
-
- precompiledSource = mergeTrees(precompiledSource, {
- overwrite: true,
- annotation: `TreeMerger (${type})`,
- });
-
- precompiledSource = this._debugTree(precompiledSource, `precompiledAddonTree:${type}`);
-
- let compiledSource = this._compileAddonTree(precompiledSource, options.skipTemplates);
-
- compiledSource = this._debugTree(compiledSource, `postcompiledAddonTree:${type}`);
-
- let combinedAddonTree;
-
- if (options.amdFunnelDisabled) {
- combinedAddonTree = compiledSource;
- } else {
- combinedAddonTree = mergeTrees(addonTrees.concat(compiledSource), {
- overwrite: true,
- annotation: `AmdFunnel TreeMerger (${type})`,
- });
- }
-
- return new Funnel(combinedAddonTree, {
- destDir: outputDir,
- annotation: `Funnel: ${outputDir} ${type}`,
- });
- }
-
_legacyPackage(fullTree) {
let javascriptTree = this._defaultPackager.packageJavascript(fullTree);
let stylesTree = this._defaultPackager.packageStyles(fullTree);
@@ -1725,7 +1393,6 @@
lib/broccoli/ember-app.js
*/
toTree(additionalTrees) {
let packagedTree;
- let packageFn = this.options.package;
let fullTree = mergeTrees(this.toArray(), {
overwrite: true,
@@ -1734,20 +1401,12 @@
lib/broccoli/ember-app.js
fullTree = this._debugTree(fullTree, 'prepackage');
- if (isExperimentEnabled('PACKAGER')) {
- if (this._isPackageHookSupplied) {
- packagedTree = packageFn.call(this, fullTree);
- } else {
- this.project.ui.writeWarnLine('`package` hook must be a function, falling back to default packaging.');
- }
- }
-
if (!packagedTree) {
packagedTree = this._legacyPackage(fullTree);
}
let trees = [].concat(packagedTree, additionalTrees).filter(Boolean);
- let combinedPackageTree = new BroccoliMergeTrees(trees);
+ let combinedPackageTree = broccoliMergeTrees(trees);
return this.addonPostprocessTree('all', combinedPackageTree);
}
diff --git a/api/files/lib_cli_cli.js.html b/api/files/lib_cli_cli.js.html
index 57a58ff..a7f54a5 100644
--- a/api/files/lib_cli_cli.js.html
+++ b/api/files/lib_cli_cli.js.html
@@ -42,7 +42,6 @@
process.env[`EMBER_VERBOSE_${arg.toUpperCase()}`] = 'true';
});
- let platformCheckerToken = heimdall.start('platform-checker');
-
- const PlatformChecker = require('../utilities/platform-checker');
- let platform = new PlatformChecker(process.version);
- let recommendation =
- ' We recommend that you use the most-recent "Active LTS" version of Node.js.' +
- ' See https://git.io/v7S5n for details.';
-
- if (!this.testing) {
- if (platform.isDeprecated) {
- this.ui.writeDeprecateLine(`Node ${process.version} is no longer supported by Ember CLI.${recommendation}`);
- }
-
- if (!platform.isTested) {
- this.ui.writeWarnLine(
- `Node ${process.version} is not tested against Ember CLI on your platform.${recommendation}`
- );
- }
- }
-
- platformCheckerToken.stop();
-
logger.info('command: %s', commandName);
- if (!this.testing) {
- process.chdir(environment.project.root);
- let skipInstallationCheck = commandArgs.indexOf('--skip-installation-check') !== -1;
- if (environment.project.isEmberCLIProject() && !skipInstallationCheck) {
- const InstallationChecker = require('../models/installation-checker');
- new InstallationChecker({ project: environment.project }).checkInstallations();
- }
- }
-
let instrumentation = this.instrumentation;
let onCommandInterrupt;
@@ -361,7 +321,6 @@
lib/cli/cli.js
let help = new HelpCommand({
ui: this.ui,
- analytics: this.analytics,
commands: environment.commands,
tasks: environment.tasks,
project: environment.project,
diff --git a/api/files/lib_debug_assert.js.html b/api/files/lib_debug_assert.js.html
new file mode 100644
index 0000000..14a0b27
--- /dev/null
+++ b/api/files/lib_debug_assert.js.html
@@ -0,0 +1,129 @@
+
+
+
+
+ lib/debug/assert.js - ember-cli
+
+
+
+
+
+
+
+'use strict';
+
+/**
+ * Verify that a certain condition is met, or throw an error if otherwise.
+ *
+ * This is useful for communicating expectations in the code to other human
+ * readers as well as catching bugs that accidentally violate these expectations.
+ *
+ * ```js
+ * const { assert } = require('ember-cli/lib/debug');
+ *
+ * // Test for truthiness:
+ * assert('Must pass a string.', typeof str === 'string');
+ *
+ * // Fail unconditionally:
+ * assert('This code path should never run.');
+ * ```
+ *
+ * @method assert
+ * @param {String} description Describes the condition.
+ * This will become the message of the error thrown if the assertion fails.
+ * @param {Any} condition Must be truthy for the assertion to pass.
+ * If falsy, an error will be thrown.
+ */
+function assert(description, condition) {
+ if (!description) {
+ throw new Error('When calling `assert`, you must provide a description as the first argument.');
+ }
+
+ if (condition) {
+ return;
+ }
+
+ throw new Error(`ASSERTION FAILED: ${description}`);
+}
+
+module.exports = assert;
+
+
+'use strict';
+
+const chalk = require('chalk');
+const semver = require('semver');
+const assert = require('./assert');
+
+/**
+ * Display a deprecation message.
+ *
+ * ```js
+ * const { deprecate } = require('ember-cli/lib/debug');
+ *
+ * deprecate('The `foo` method is deprecated.', false, {
+ * for: 'ember-cli',
+ * id: 'ember-cli.foo-method',
+ * since: {
+ * available: '4.1.0',
+ * enabled: '4.2.0',
+ * },
+ * until: '5.0.0',
+ * url: 'https://example.com',
+ * });
+ * ```
+ *
+ * @method deprecate
+ * @param {String} description Describes the deprecation.
+ * @param {Any} condition If falsy, the deprecation message will be displayed.
+ * @param {Object} options An object including the deprecation's details:
+ * - `for` The library that the deprecation is for
+ * - `id` The deprecation's unique id
+ * - `since.available` A SemVer version indicating when the deprecation was made available
+ * - `since.enabled` A SemVer version indicating when the deprecation was enabled
+ * - `until` A SemVer version indicating until when the deprecation will be active
+ * - `url` A URL that refers to additional information about the deprecation
+ */
+function deprecate(description, condition, options) {
+ assert('When calling `deprecate`, you must provide a description as the first argument.', description);
+ assert('When calling `deprecate`, you must provide a condition as the second argument.', arguments.length > 1);
+
+ assert(
+ 'When calling `deprecate`, you must provide an options object as the third argument. The options object must include the `for`, `id`, `since` and `until` options (`url` is optional).',
+ options
+ );
+
+ assert('When calling `deprecate`, you must provide the `for` option.', options.for);
+ assert('When calling `deprecate`, you must provide the `id` option.', options.id);
+
+ assert(
+ 'When calling `deprecate`, you must provide the `since` option. `since` must include the `available` and/or the `enabled` option.',
+ options.since
+ );
+
+ assert(
+ 'When calling `deprecate`, you must provide the `since.available` and/or the `since.enabled` option.',
+ options.since.available || options.since.enabled
+ );
+
+ assert(
+ '`since.available` must be a valid SemVer version.',
+ !options.since.available || isSemVer(options.since.available)
+ );
+
+ assert('`since.enabled` must be a valid SemVer version.', !options.since.enabled || isSemVer(options.since.enabled));
+
+ assert(
+ 'When calling `deprecate`, you must provide a valid SemVer version for the `until` option.',
+ isSemVer(options.until)
+ );
+
+ if (condition) {
+ return;
+ }
+
+ let message = formatMessage(description, options);
+
+ warn(message);
+ warn(getStackTrace());
+
+ // Return the message for testing purposes.
+ // This can be removed once we can register deprecation handlers.
+ return message;
+}
+
+function isSemVer(version) {
+ return semver.valid(version) !== null;
+}
+
+function formatMessage(description, options) {
+ let message = [`DEPRECATION: ${description}`, `[ID: ${options.id}]`];
+
+ if (options.url) {
+ message.push(`See ${options.url} for more details.`);
+ }
+
+ return message.join(' ');
+}
+
+function getStackTrace() {
+ let error = new Error();
+ let lines = error.stack.split('\n');
+
+ lines.shift(); // Remove the word `Error`.
+
+ return lines.map((line) => line.trim()).join('\n');
+}
+
+function warn(message) {
+ console.warn(chalk.yellow(message));
+}
+
+module.exports = deprecate;
+
+
ADDON_TREE_CACHE.clear();
}
-function warn(message) {
- if (this.ui) {
- this.ui.writeDeprecateLine(message);
- } else {
- const chalk = require('chalk');
- console.log(chalk.yellow(`DEPRECATION: ${message}`));
- }
-}
-
/**
Root class for an Addon. If your addon module exports an Object this
will be extended from this base class. If you export a constructor (function),
@@ -399,27 +382,19 @@
lib/models/addon.js
this._packageInfo = this.packageInfoCache.loadAddon(this);
- // force us to use the real path as the root.
- this.root = this._packageInfo.realPath;
-
p.setupRegistry(this);
this._initDefaultBabelOptions();
},
_initDefaultBabelOptions() {
- this.__originalOptions = this.options = defaultsDeep(this.options, {
- babel: this[BUILD_BABEL_OPTIONS_FOR_PREPROCESSORS](),
+ this.options = defaultsDeep(this.options, {
+ babel: {},
});
- let defaultEmberCLIBabelOptions = {
+ this.options['ember-cli-babel'] = defaultsDeep(this.options['ember-cli-babel'], {
compileModules: true,
- };
- let emberCLIBabelConfigKey = this._emberCLIBabelConfigKey();
- this.__originalOptions[emberCLIBabelConfigKey] = this.options[emberCLIBabelConfigKey] = defaultsDeep(
- this.options[emberCLIBabelConfigKey],
- defaultEmberCLIBabelOptions
- );
+ });
},
/**
@@ -494,16 +469,14 @@
lib/models/addon.js
return true;
}
- // Addon names in index.js and package.json should be the same at all times whether they have scope or not.
+ // Addon names in index.js and package.json must be the same at all times whether they have scope or not.
if (this.root === this.parent.root) {
- if (parentName !== this.name && !process.env.EMBER_CLI_IGNORE_ADDON_NAME_MISMATCH) {
+ if (parentName !== this.name) {
let pathToDisplay = process.cwd() === this.root ? process.cwd() : path.relative(process.cwd(), this.root);
throw new SilentError(
- 'ember-cli: Your names in package.json and index.js should match. ' +
- `The addon in ${pathToDisplay} currently have '${parentName}' in package.json and '${this.name}' in index.js. ` +
- 'Until ember-cli v3.9, this error can be disabled by setting env variable EMBER_CLI_IGNORE_ADDON_NAME_MISMATCH to "true". ' +
- 'For more information about this workaround, see: https://github.com/ember-cli/ember-cli/pull/7950.'
+ 'ember-cli: Your names in package.json and index.js must match. ' +
+ `The addon in ${pathToDisplay} currently has '${parentName}' in package.json and '${this.name}' in index.js.`
);
}
@@ -526,7 +499,10 @@
lib/models/addon.js
* @method discoverAddons
*/
discoverAddons() {
- let pkgInfo = this.packageInfoCache.getEntry(this.root);
+ // prefer `packageRoot`, fallback to `root`; this is to maintain backwards compatibility for
+ // consumers who create a new instance of the base addon model class directly and don't set
+ // `packageRoot`
+ let pkgInfo = this.packageInfoCache.getEntry(this.packageRoot || this.root);
if (pkgInfo) {
let addonPackageList = pkgInfo.discoverAddonAddons();
@@ -629,8 +605,8 @@
lib/models/addon.js
let tree;
if (!this.project) {
- this._warn(
- `Addon: \`${this.name}\` is missing addon.project, this may be the result of an addon forgetting to invoke \`super\` in its init.`
+ throw new SilentError(
+ `Addon \`${this.name}\` is missing \`addon.project\`. This may be the result of an addon forgetting to invoke \`super\` in its init hook.`
);
}
// TODO: fix law of demeter `_watchmanInfo.canNestRoots` is obviously a poor idea
@@ -653,18 +629,6 @@
lib/models/addon.js
return ensurePosixPath(normalizedAbsoluteTreePath);
},
- /**
- * @private
- * @method _warn
- */
- _warn: warn,
-
- _emberCLIBabelConfigKey() {
- let emberCLIBabelInstance = findAddonByName(this.addons, 'ember-cli-babel');
-
- return emberCLIBabelConfigKey(emberCLIBabelInstance);
- },
-
/**
Returns a given type of tree (if present), merged with the
application tree. For each of the trees available using this
@@ -918,15 +882,12 @@
lib/models/addon.js
@example
```js
treeForAddon() {
- var tree = this._super.treeForAddon.apply(this, arguments);
- var checker = new VersionChecker(this);
- var isOldEmber = checker.for('ember', 'bower').lt('1.13.0');
+ let emberVersion = new VersionChecker(this.project).for('ember-source');
+ let shouldUsePolyfill = emberVersion.lt('4.5.0-alpha.4');
- if (isOldEmber) {
- tree = new Funnel(tree, { exclude: [ /instance-initializers/ ] });
+ if (shouldUsePolyfill) {
+ return this._super.treeForAddon.apply(this, arguments);
}
-
- return tree;
}
```
*/
@@ -1026,17 +987,16 @@
lib/models/addon.js
if (registryHasPreprocessor(this.registry, 'js')) {
return this.preprocessJs(namespacedTree, '/', this.name, {
registry: this.registry,
+ treeType: 'addon-test-support',
});
- } else {
- this._warn(
- `Addon test support files were detected in \`${this._treePathFor('addon-test-support')}\`, but no JavaScript ` +
- `preprocessors were found for \`${this.name}\`. Please make sure to add a preprocessor ` +
- `(most likely \`ember-cli-babel\`) to \`dependencies\` (NOT \`devDependencies\`) in ` +
- `\`${this.name}\`'s \`package.json\`.`
- );
-
- return processModulesOnly(namespacedTree, `Babel Fallback - Addon#treeForAddonTestSupport (${this.name})`);
}
+
+ throw new SilentError(
+ `Addon test-support files were detected in \`${this._treePathFor('addon-test-support')}\`, but no JavaScript ` +
+ `preprocessor was found for \`${this.name}\`. Please make sure to add a preprocessor ` +
+ `(most likely \`ember-cli-babel\`) to \`dependencies\` (NOT \`devDependencies\`) in ` +
+ `\`${this.name}\`'s \`package.json\`.`
+ );
},
/**
@@ -1054,6 +1014,7 @@
/**
Looks in the addon/ and addon/templates trees to determine if template files
- exists that need to be precompiled.
+ exist that need to be precompiled.
This is executed once when building, but not on rebuilds.
@@ -1079,7 +1040,7 @@
lib/models/addon.js
/**
Looks in the addon/ and addon/templates trees to determine if template files
- exists in the pods format that need to be precompiled.
+ exist in the pods format that need to be precompiled.
This is executed once when building, but not on rebuilds.
@@ -1108,7 +1069,7 @@
lib/models/addon.js
addonTemplatesTreeInAddonTree && addonTemplatesTreePath.replace(`${addonTreePath}/`, '');
let podTemplateMatcher = new RegExp(`template.(${templateExtensions.join('|')})$`);
let hasPodTemplates = files.some((file) => {
- // short circuit if this is actually a `addon/templates` file
+ // short circuit if this is actually an `addon/templates` file
if (addonTemplatesTreeInAddonTree && file.indexOf(addonTemplatesRelativeToAddonPath) === 0) {
return false;
}
@@ -1218,6 +1179,7 @@
*/
compileAddon(tree) {
if (!this.options) {
- this._warn(
+ throw new SilentError(
`Ember CLI addons manage their own module transpilation during the \`treeForAddon\` processing. ` +
`\`${this.name}\` (found at \`${this.root}\`) has removed \`this.options\` ` +
- `which conflicts with the addons ability to transpile its \`addon/\` files properly. ` +
- `Falling back to default babel configuration options.`
+ `which conflicts with the addons ability to transpile its \`addon/\` files properly.`
);
-
- this.options = {};
}
if (!this.options.babel) {
- this._warn(
+ throw new SilentError(
`Ember CLI addons manage their own module transpilation during the \`treeForAddon\` processing. ` +
- `\`${this.name}\` (found at \`${this.root}\`) has overridden the \`this.options.babel\` ` +
- `options which conflicts with the addons ability to transpile its \`addon/\` files properly. ` +
- `Falling back to default babel configuration options.`
+ `\`${this.name}\` (found at \`${this.root}\`) has overridden the \`this.options.babel\` options ` +
+ `which conflicts with the addons ability to transpile its \`addon/\` files properly.`
);
-
- this.options.babel = this.__originalOptions.babel;
}
- let emberCLIBabelConfigKey = this._emberCLIBabelConfigKey();
- if (!this.options[emberCLIBabelConfigKey]) {
- this._warn(
+ if (!this.options['ember-cli-babel']) {
+ throw new SilentError(
`Ember CLI addons manage their own module transpilation during the \`treeForAddon\` processing. ` +
- `\`${this.name}\` (found at \`${this.root}\`) has overridden the \`this.options.${emberCLIBabelConfigKey}\` ` +
- `options which conflicts with the addons ability to transpile its \`addon/\` files properly. ` +
- `Falling back to default babel configuration options.`
+ `\`${this.name}\` (found at \`${this.root}\`) has overridden the \`this.options['ember-cli-babel']\` options ` +
+ `which conflicts with the addons ability to transpile its \`addon/\` files properly.`
);
-
- this.options[emberCLIBabelConfigKey] = this.__originalOptions[emberCLIBabelConfigKey];
}
let scopedInput = new Funnel(tree, {
@@ -1283,7 +1235,7 @@
lib/models/addon.js
},
/**
- Returns a tree with JSHhint output for all addon JS.
+ Returns a tree with JSHint output for all addon JS.
@private
@method jshintAddonTree
@@ -1348,23 +1300,6 @@
lib/models/addon.js
});
},
- /**
- Returns a tree containing the addon's js files
-
- @private
- @deprecated
- @method addonJsFiles
- @return {Tree} The filtered addon js files
- */
- addonJsFiles(tree) {
- this._warn(`Addon.prototype.addonJsFiles is deprecated`);
-
- return new Funnel(tree, {
- destDir: this.moduleName(),
- annotation: 'Funnel: Addon JS',
- });
- },
-
/**
Preprocesses a javascript tree.
@@ -1390,25 +1325,19 @@
lib/models/addon.js
let processedAddonJS = this.preprocessJs(preprocessedAddonJS, '/', this.name, {
annotation: `processedAddonJsFiles(${this.name})`,
registry: this.registry,
+ treeType: 'addon',
});
- let postprocessedAddonJs = this._addonPostprocessTree('js', processedAddonJS);
-
- if (!registryHasPreprocessor(this.registry, 'js')) {
- this._warn(
- `Addon files were detected in \`${this._treePathFor('addon')}\`, but no JavaScript ` +
- `preprocessors were found for \`${this.name}\`. Please make sure to add a preprocessor ` +
- '(most likely `ember-cli-babel`) to in `dependencies` (NOT `devDependencies`) in ' +
- `\`${this.name}\`'s \`package.json\`.`
- );
-
- postprocessedAddonJs = processModulesOnly(
- postprocessedAddonJs,
- `Babel Fallback - Addon#processedAddonJsFiles(${this.name})`
- );
+ if (registryHasPreprocessor(this.registry, 'js')) {
+ return this._addonPostprocessTree('js', processedAddonJS);
}
- return postprocessedAddonJs;
+ throw new SilentError(
+ `Addon files were detected in \`${this._treePathFor('addon')}\`, but no JavaScript ` +
+ `preprocessor was found for \`${this.name}\`. Please make sure to add a preprocessor ` +
+ '(most likely `ember-cli-babel`) to `dependencies` (NOT `devDependencies`) in ' +
+ `\`${this.name}\`'s \`package.json\`.`
+ );
},
/**
@@ -1445,7 +1374,7 @@
lib/models/addon.js
},
/**
- Augments the applications configuration settings.
+ Augments the application's configuration settings.
Object returned from this hook is merged with the application's configuration object.
@@ -1455,7 +1384,6 @@
lib/models/addon.js
- Modifying configuration options (see list of defaults [here](https://github.com/ember-cli/ember-cli/blob/v2.4.3/lib/broccoli/ember-app.js#L163))
- For example
- - `minifyJS`
- `storeConfigInMeta`
- et, al
@@ -1827,24 +1755,6 @@
_printableProperties: ['name', 'description', 'availableOptions', 'anonymousOptions', 'overridden'],
- init(blueprintPath) {
+ /**
+ Indicates whether or not a blueprint is a candidate for automatic transpilation from TS to JS.
+ This property could be false in the case that the blueprint is written in JS and is not intended
+ to work with TS at all, OR in the case that the blueprint is written in TS and the author does
+ not intend to support transpilation to JS.
+
+ @public
+ @property shouldTransformTypeScript
+ @type Boolean
+ */
+ shouldTransformTypeScript: false,
+
+ init(blueprintPath, blueprintOptions) {
this._super();
this.path = blueprintPath;
this.name = path.basename(blueprintPath);
+
+ this._processOptions(blueprintOptions);
+ },
+
+ /**
+ Process the options object coming from either
+ the `init`, `install` or `uninstall` hook.
+
+ @private
+ @method _processOptions
+ @param {Object} options
+ */
+ _processOptions(options = {}) {
+ this.options = options;
+ this.dryRun = options.dryRun;
+ this.pod = options.pod;
+ this.project = options.project;
+ this.ui = options.ui;
},
/**
@@ -306,9 +334,10 @@
let fileInfos = await process.call(this, intoDir, locals);
// commit changes for each FileInfo (with prompting as needed)
- await Promise.all(fileInfos.map((fi) => this._commit(fi)));
+ await Promise.all(fileInfos.map((info) => this._commit(info)));
// run afterInstall/afterUninstall userland hooks
await afterHook.call(this, options);
},
+ /**
+ @private
+ @method shouldConvertToJS
+ @param {Object} options
+ @param {FileInfo} fileInfo
+ @return {Boolean}
+ */
+ shouldConvertToJS(options, fileInfo) {
+ // If this isn't turned on, it doesn't matter what else was passed, we're not touching it.
+ if (!this.shouldTransformTypeScript) {
+ return false;
+ }
+
+ // If the blueprint isn't a TS file to begin with, there's nothing to convert.
+ if (!isTypeScriptFile(fileInfo.outputPath)) {
+ // If the user wants TypeScript output but there is no TypeScript blueprint available, we want
+ // to warn them that they're not going to get what they're expecting while still at least giving
+ // them the JS output. We check for this *after* checking `shouldTranformTypeScript` because
+ // it's possible for people to set `{typescript: true}` in their `.ember-cli` file, which would
+ // then erroneously trigger this message every time they generate a JS blueprint even though
+ // they didn't pass the flag.
+ if (options.typescript === true && isJavaScriptFile(fileInfo.outputPath)) {
+ this.ui.writeLine(
+ chalk.yellow(
+ "You passed the '--typescript' flag but there is no TypeScript blueprint available. " +
+ 'A JavaScript blueprint will be generated instead.'
+ )
+ );
+ }
+
+ return false;
+ }
+
+ // Indicates when the user explicitly passed either `--typescript` or `--no-typescript` as opposed
+ // to not passing a flag at all and allowing for default behavior
+ const userExplicitlySelectedTypeScriptStatus = options.typescript !== undefined;
+
+ // Indicates when the user has asked for TypeScript either globally (by setting
+ // `isTypeScriptProject` to true) or locally (by passing the `--typescript` flag when they
+ // invoked the generator). Although ember-cli merges `.ember-cli` and all of the flag values into
+ // one object, we thought the DX would be improved by differentiating between what is intended
+ // to be global vs. local config.
+ const shouldUseTypeScript = userExplicitlySelectedTypeScriptStatus
+ ? options.typescript
+ : options.isTypeScriptProject;
+
+ // if the user wants TS output and we have a TS file available, we do *not* want to downlevel to JS
+ if (shouldUseTypeScript) {
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ @private
+ @method convertToJS
+ @param {FileInfo} fileInfo
+ @return {Promise}
+ */
+ async convertToJS(fileInfo) {
+ let rendered = await fileInfo.render();
+
+ const { removeTypes } = require('remove-types');
+ const transformed = await removeTypes(rendered);
+
+ fileInfo.rendered = transformed;
+
+ fileInfo.displayPath = replaceExtension(fileInfo.displayPath, '.js');
+ fileInfo.outputPath = replaceExtension(fileInfo.outputPath, '.js');
+
+ return fileInfo;
+ },
+
/**
@method install
@param {Object} options
@return {Promise}
*/
install(options) {
- let ui = (this.ui = options.ui);
- let dryRun = (this.dryRun = options.dryRun);
- this.project = options.project;
- this.pod = options.pod;
- this.options = options;
- this.hasPathToken = hasPathToken(this.files());
+ this._processOptions(options);
- ui.writeLine(`installing ${this.name}`);
+ this.hasPathToken = hasPathToken(this.files(this.options));
- if (dryRun) {
- ui.writeLine(chalk.yellow('You specified the dry-run flag, so no' + ' changes will be written.'));
+ this.ui.writeLine(`installing ${this.name}`);
+
+ if (this.dryRun) {
+ this.ui.writeLine(chalk.yellow('You specified the `dry-run` flag, so no changes will be written.'));
}
this._normalizeEntityName(options.entity);
@@ -553,17 +653,14 @@
lib/models/blueprint.js
@return {Promise}
*/
uninstall(options) {
- let ui = (this.ui = options.ui);
- let dryRun = (this.dryRun = options.dryRun);
- this.project = options.project;
- this.pod = options.pod;
- this.options = options;
- this.hasPathToken = hasPathToken(this.files());
+ this._processOptions(options);
+
+ this.hasPathToken = hasPathToken(this.files(this.options));
- ui.writeLine(`uninstalling ${this.name}`);
+ this.ui.writeLine(`uninstalling ${this.name}`);
- if (dryRun) {
- ui.writeLine(chalk.yellow('You specified the dry-run flag, so no' + ' files will be deleted.'));
+ if (this.dryRun) {
+ this.ui.writeLine(chalk.yellow('You specified the `dry-run` flag, so no files will be deleted.'));
}
this._normalizeEntityName(options.entity);
@@ -718,7 +815,7 @@
@param {Object} templateVariables
*/
processFilesForUninstall(intoDir, templateVariables) {
- let fileInfos = this._getFileInfos(this.files(), intoDir, templateVariables);
+ let fileInfos = this._getFileInfos(this.files(this.options), intoDir, templateVariables);
this._ignoreUpdateFiles();
- return finishProcessingForUninstall(fileInfos.filter(isValidFile));
+ fileInfos = fileInfos.filter(isValidFile).reduce((acc, info) => {
+ // if it's possible that this blueprint could have produced either typescript OR javascript, we have to do some
+ // work to figure out which files to delete.
+ if (this.shouldTransformTypeScript) {
+ if (this.options.typescript === true) {
+ // if the user explicitly passed `--typescript`, we only want to delete TS files, so we stick with the existing
+ // info object since it will contain a .ts outputPath (since we know this blueprint is authored in TS because
+ // of our check above)
+ acc.push(info);
+ return acc;
+ }
+
+ const jsInfo = new FileInfo({
+ ...info,
+ outputPath: replaceExtension(info.outputPath, '.js'),
+ displayPath: replaceExtension(info.displayPath, '.js'),
+ });
+
+ if (this.options.typescript === false) {
+ // if the user explicitly passed `--no-typescript`, we only want to delete JS file, so we return our newly
+ // created jsInfo object since it contains the javascript version of the output path.
+ acc.push(jsInfo);
+ return acc;
+ }
+
+ if (this.options.typescript === undefined) {
+ // if the user didn't specify one way or the other, then both the JS and TS paths are possibilities, so we add
+ // both of them to the list. `finishProcessingForUninstall` will actually look to see which of them exists and
+ // delete whatever it finds.
+ acc.push(info, jsInfo);
+ return acc;
+ }
+ }
+
+ acc.push(info);
+ return acc;
+ }, []);
+
+ return finishProcessingForUninstall(fileInfos);
},
/**
@@ -880,7 +1026,7 @@
});
},
- /**
- Used to add a package to the projects `bower.json`.
-
- Generally, this would be done from the `afterInstall` hook, to
- ensure that a package that is required by a given blueprint is
- available.
-
- `localPackageName` and `target` may be thought of as equivalent
- to the key-value pairs in the `dependency` or `devDepencency`
- objects contained within a bower.json file.
- @method addBowerPackageToProject
- @param {String} localPackageName
- @param {String} target
- @param {Object} installOptions
- @return {Promise}
-
- @example
- ```js
- addBowerPackageToProject('jquery', '~1.11.1');
- addBowerPackageToProject('old_jquery', 'jquery#~1.9.1');
- addBowerPackageToProject('bootstrap-3', 'https://twitter.github.io/bootstrap/assets/bootstrap');
- ```
- */
- addBowerPackageToProject(localPackageName, target, installOptions) {
- let lpn = localPackageName;
- let tar = target;
- let packageObject = bowEpParser.json2decomposed(lpn, tar);
- return this.addBowerPackagesToProject([packageObject], installOptions);
- },
-
- /**
- Used to add an array of packages to the projects `bower.json`.
-
- Generally, this would be done from the `afterInstall` hook, to
- ensure that a package that is required by a given blueprint is
- available.
-
- Expects each array item to be an object with a `name`. Each object
- may optionally have a `target` to specify a specific version, or a
- `source` to specify a non-local name to be resolved.
-
- @method addBowerPackagesToProject
- @param {Array} packages
- @param {Object} installOptions
- @return {Promise}
- */
- addBowerPackagesToProject(packages, installOptions) {
- let task = this.taskFor('bower-install');
- let installText = packages.length > 1 ? 'install bower packages' : 'install bower package';
- let packageNames = [];
- let packageNamesAndVersions = packages
- .map((pkg) => {
- pkg.source = pkg.source || pkg.name;
- packageNames.push(pkg.name);
- return pkg;
- })
- .map(bowEpParser.compose);
-
- this._writeStatusToUI(chalk.green, installText, packageNames.join(', '));
-
- return task.run({
- verbose: true,
- packages: packageNamesAndVersions,
- installOptions: installOptions || { save: true },
- });
- },
-
/**
Used to add an addon to the project's `package.json` and run it's
`defaultBlueprint` if it provides one.
@@ -1205,12 +1284,43 @@
lib/models/blueprint.js
let installText = packages.length > 1 ? 'install addons' : 'install addon';
this._writeStatusToUI(chalk.green, installText, taskOptions['packages'].join(', '));
- return this.taskFor('addon-install').run(taskOptions);
+ let previousCwd;
+ if (!this.project.isEmberCLIProject()) {
+ // our blueprint ran *outside* an ember-cli project. So the only way this
+ // makes sense if the blueprint generated a new project, which we're now
+ // ready to add some addons into. So we need to switch from "outside" mode
+ // to "inside" by reinitializing the Project.
+ //
+ // One might think the cache clear is unnecessary, because in theory the
+ // caches should be shareable, but in practice the new Project we create
+ // below will have the same root dir as the NullProject and that makes bad
+ // things happen.
+ this.project.packageInfoCache._clear();
+ const Project = require('../../lib/models/project');
+ this.project = taskOptions.blueprintOptions.project = Project.closestSync(
+ options.blueprintOptions.target,
+ this.project.ui,
+ this.project.cli
+ );
+
+ // The install task adds dependencies based on the current working directory.
+ // But in case we created the new project by calling the blueprint with a custom target directory (options.target),
+ // the current directory will *not* be the one the project is created in, so we must adjust this here.
+ previousCwd = process.cwd();
+ process.chdir(options.blueprintOptions.target);
+ }
+
+ let result = this.taskFor('addon-install').run(taskOptions);
+ if (previousCwd) {
+ return result.then(() => process.chdir(previousCwd));
+ }
+
+ return result;
},
/**
Used to retrieve a task with the given name. Passes the new task
- the standard information available (like `ui`, `analytics`, `project`, etc).
+ the standard information available (like `ui`, `project`, etc).
@method taskFor
@param dasherizedName
@@ -1222,7 +1332,6 @@
this._printableProperties.forEach((key) => {
let value = this[key];
if (key === 'availableOptions') {
- value = _.cloneDeep(value);
+ value = cloneDeep(value);
value.forEach((option) => {
if (typeof option.type === 'function') {
option.type = option.type.name;
@@ -1356,6 +1465,8 @@
lib/models/blueprint.js
@param {Array} [options.paths] Extra paths to search for blueprints
@param {Boolean} [options.ignoreMissing] Throw a `SilentError` if a
matching Blueprint could not be found
+ @param {Object} [options.blueprintOptions] Options object that will be passed
+ along to the Blueprint instance on creation.
@return {Blueprint}
*/
Blueprint.lookup = function (name, options) {
@@ -1368,7 +1479,16 @@
lib/models/blueprint.js
let blueprintPath = path.resolve(lookupPath, name);
if (Blueprint._existsSync(blueprintPath)) {
- return Blueprint.load(blueprintPath);
+ return Blueprint.load(blueprintPath, options.blueprintOptions);
+ }
+ }
+
+ // Check if `name` itself is a path to a blueprint:
+ if (name.includes(path.sep)) {
+ let blueprintPath = path.resolve(name);
+
+ if (Blueprint._existsSync(blueprintPath)) {
+ return Blueprint.load(blueprintPath, options.blueprintOptions);
}
}
@@ -1383,9 +1503,10 @@
lib/models/blueprint.js
@method load
@namespace Blueprint
@param {String} blueprintPath
+ @param {Object} [blueprintOptions]
@return {Blueprint} blueprint instance
*/
-Blueprint.load = function (blueprintPath) {
+Blueprint.load = function (blueprintPath, blueprintOptions) {
if (fs.lstatSync(blueprintPath).isDirectory()) {
let Constructor = Blueprint;
@@ -1400,7 +1521,7 @@
lib/models/blueprint.js
}
}
- return new Constructor(blueprintPath);
+ return new Constructor(blueprintPath, blueprintOptions);
}
};
@@ -1431,7 +1552,7 @@
lib/models/blueprint.js
let blueprint = Blueprint.load(blueprintPath);
if (blueprint) {
let name = blueprint.name;
- blueprint.overridden = _.includes(seen, name);
+ blueprint.overridden = seen.includes(name);
seen.push(name);
return blueprint;
@@ -1440,7 +1561,7 @@
// Use Broccoli 2.0 by default, if this fails due to .read/.rebuild API, fallback to broccoli-builder
this.broccoliBuilderFallback = false;
- this.setupBroccoliBuilder();
this._instantiationStack = new Error().stack.replace(/[^\n]*\n/, '');
this._cleanup = this.cleanup.bind(this);
@@ -124,25 +121,31 @@
} catch (error) {
await this.processAddonBuildSteps('buildError', error);
+ // Mark this as a builder error so the watcher knows it has been handled
+ // and won't re-throw it
+ error.isBuilderError = true;
+
throw error;
} finally {
clearInterval(uiProgressIntervalID);
diff --git a/api/files/lib_models_command.js.html b/api/files/lib_models_command.js.html
index c688350..edc2e22 100644
--- a/api/files/lib_models_command.js.html
+++ b/api/files/lib_models_command.js.html
@@ -42,7 +42,6 @@
@method validateAndRun
@return {Promise}
*/
- validateAndRun(args) {
- return new Promise((resolve, reject) => {
- let commandOptions = this.parseArgs(args);
- // if the help option was passed, resolve with 'callHelp' to call help command
- if (commandOptions && (commandOptions.options.help || commandOptions.options.h)) {
- logger.info(`${this.name} called with help option`);
- return resolve('callHelp');
- }
+ async validateAndRun(args) {
+ let commandOptions = this.parseArgs(args);
- this.analytics.track({
- name: 'ember ',
- message: this.name,
- });
+ // If the `help` option was passed, resolve with `callHelp` to call the `help` command:
+ if (commandOptions && (commandOptions.options.help || commandOptions.options.h)) {
+ logger.info(`${this.name} called with help option`);
- if (commandOptions === null) {
- return reject();
- }
+ return 'callHelp';
+ }
- if (this.works === 'outsideProject' && this.isWithinProject) {
- throw new SilentError(`You cannot use the ${chalk.green(this.name)} command inside an ember-cli project.`);
- }
+ if (commandOptions === null) {
+ throw new SilentError();
+ }
- if (this.works === 'insideProject') {
- if (!this.project.hasDependencies()) {
- if (!this.isWithinProject && !this.project.isEmberCLIAddon()) {
- throw new SilentError(
- `You have to be inside an ember-cli project to use the ${chalk.green(this.name)} command.`
- );
- }
-
- let installInstuction = '`npm install`';
- if (isYarnProject(this.project.root)) {
- installInstuction = '`yarn install`';
- }
+ if (this.works === 'outsideProject' && this.isWithinProject) {
+ throw new SilentError(`You cannot use the ${chalk.green(this.name)} command inside an ember-cli project.`);
+ }
+
+ if (this.works === 'insideProject') {
+ if (!this.project.hasDependencies()) {
+ if (!this.isWithinProject && !this.project.isEmberCLIAddon()) {
throw new SilentError(
- `Required packages are missing, run ${installInstuction} from this directory to install them.`
+ `You have to be inside an ember-cli project to use the ${chalk.green(this.name)} command.`
);
}
- }
- let detector = new WatchDetector({
- ui: this.ui,
- childProcess: { execSync },
- fs,
- watchmanSupportsPlatform: /^win/.test(process.platform),
- cache,
- root: this.project.root,
- });
-
- let options = commandOptions.options;
+ let installCommand = await determineInstallCommand(this.project.root);
- if (this.hasOption('watcher')) {
- // do stuff to try and provide a good experience when it comes to file watching
- let watchPreference = detector.findBestWatcherOption(options);
- this.project._watchmanInfo = watchPreference.watchmanInfo;
- options.watcher = watchPreference.watcher;
+ throw new SilentError(
+ `Required packages are missing, run \`${installCommand}\` from this directory to install them.`
+ );
}
- resolve(this.run(options, commandOptions.args));
+ }
+
+ let detector = new WatchDetector({
+ ui: this.ui,
+ childProcess: { execSync },
+ fs,
+ watchmanSupportsPlatform: /^win/.test(process.platform),
+ cache,
+ root: this.project.root,
});
+
+ let options = commandOptions.options;
+
+ if (this.hasOption('watcher')) {
+ // Do stuff to try and provide a good experience when it comes to file watching:
+ let watchPreference = detector.findBestWatcherOption(options);
+
+ this.project._watchmanInfo = watchPreference.watchmanInfo;
+ options.watcher = watchPreference.watcher;
+ }
+
+ return this.run(options, commandOptions.args);
},
/**
@@ -427,23 +418,17 @@
+'use strict';
+
+function allPkgInfosEqualAtIndex(paths, index) {
+ const itemToCheck = paths[0][index];
+ return paths.every((pathToLazyEngine) => pathToLazyEngine[index] === itemToCheck);
+}
+
+class HostInfoCache {
+ constructor(project) {
+ this.project = project;
+ this._bundledPackageInfoCache = new Map();
+ this._hostAddonInfoCache = new Map();
+ this._lcaHostCache = new Map();
+ }
+
+ /**
+ * Given a path (calculated as part of `getHostAddonInfo`), return the correct
+ * "bundle host". A bundle host is considered the project or lazy engine.
+ *
+ * For example, given the following package structure:
+ *
+ * --Project--
+ * / \
+ * / \
+ * Lazy Engine A \
+ * Addon A
+ * |
+ * |
+ * Lazy Engine B
+ * / \
+ * / \
+ * Lazy Engine A Lazy Engine C
+ *
+ * The provided paths for lazy engine A would look like:
+ *
+ * - [Project]
+ * - [Project, Addon A, Lazy Engine B]
+ *
+ * For this project structure, this function would return [Project, [Project]]
+ *
+ * Similarly, given the following project structure:
+ *
+ * --Project--
+ * / \
+ * / \
+ * Lazy Engine A \
+ * / Lazy Engine B
+ * / |
+ * / |
+ * Lazy Engine C Lazy Engine C
+ *
+ * The provided paths for lazy engine C would look like:
+ *
+ * - [Project, Lazy Engine A]
+ * - [Project, Lazy Engine B]
+ *
+ * In this case, the host is the project and would also return [Project, [Project]]
+ *
+ * @method _findNearestBundleHost
+ * @param {Array<PackageInfo[]>} paths The found paths to a given bundle host
+ * @return {[PackageInfo, PackageInfo[]]}
+ * @private
+ */
+ _findNearestBundleHost(paths, pkgInfoForLazyEngine) {
+ // building an engine in isolation (it's considered the project, but it's
+ // also added as a dependency to the project by `ember-cli`)
+ if (this.project._packageInfo === pkgInfoForLazyEngine) {
+ return [this.project._packageInfo, [this.project._packageInfo]];
+ }
+
+ const shortestPath = paths.reduce(
+ (acc, pathToLazyEngine) => Math.min(acc, pathToLazyEngine.length),
+ Number.POSITIVE_INFINITY
+ );
+
+ const pathsEqualToShortest = paths.filter((pathToLazyEngine) => pathToLazyEngine.length === shortestPath);
+ const [firstPath] = pathsEqualToShortest;
+
+ for (let i = firstPath.length - 1; i >= 0; i--) {
+ const pkgInfo = firstPath[i];
+
+ if (pkgInfo.isForBundleHost() && allPkgInfosEqualAtIndex(pathsEqualToShortest, i)) {
+ return [pkgInfo, firstPath.slice(0, i + 1)];
+ }
+ }
+
+ // this should _never_ be triggered
+ throw new Error(
+ `[ember-cli] Could not find a common host for: \`${pkgInfoForLazyEngine.name}\` (located at \`${pkgInfoForLazyEngine.realPath}\`)`
+ );
+ }
+
+ /**
+ * Returns a `Set` of package-info objects that a given bundle host is
+ * _directly_ responsible for bundling (i.e., it excludes other bundle
+ * hosts/lazy engines when it encounters these)
+ *
+ * @method _getBundledPackageInfos
+ * @param {PackageInfo} pkgInfoToStartAt
+ * @return {Set<PackageInfo>}
+ * @private
+ */
+ _getBundledPackageInfos(pkgInfoToStartAt) {
+ let pkgInfos = this._bundledPackageInfoCache.get(pkgInfoToStartAt);
+
+ if (pkgInfos) {
+ return pkgInfos;
+ }
+
+ if (!pkgInfoToStartAt.isForBundleHost()) {
+ throw new Error(
+ `[ember-cli] \`${pkgInfoToStartAt.name}\` is not a bundle host; \`getBundledPackageInfos\` should only be used to find bundled package infos for a project or lazy engine`
+ );
+ }
+
+ pkgInfos = new Set();
+ this._bundledPackageInfoCache.set(pkgInfoToStartAt, pkgInfos);
+
+ let findAddons = (currentPkgInfo) => {
+ if (!currentPkgInfo.valid || !currentPkgInfo.addonMainPath) {
+ return;
+ }
+
+ if (pkgInfos.has(currentPkgInfo)) {
+ return;
+ }
+
+ if (currentPkgInfo.isForBundleHost()) {
+ return;
+ }
+
+ pkgInfos.add(currentPkgInfo);
+
+ let addonPackageList = currentPkgInfo.discoverAddonAddons();
+ addonPackageList.forEach((pkgInfo) => findAddons(pkgInfo));
+ };
+
+ let addonPackageList = pkgInfoToStartAt.project
+ ? pkgInfoToStartAt.discoverProjectAddons()
+ : pkgInfoToStartAt.discoverAddonAddons();
+
+ addonPackageList.forEach((pkgInfo) => findAddons(pkgInfo));
+
+ return pkgInfos;
+ }
+
+ /**
+ * This function intends to return a common host for a bundle host (lazy engine). The root
+ * package info should be the starting point (i.e., the project's package info). We do this
+ * by performing a breadth-first traversal until we find the intended lazy engine (represented
+ * as a package-info & the 1st argument passed to this function). As part of the traversal, we keep
+ * track of all paths to said engine; then, once we find the intended engine we use this to determine
+ * the nearest common host amongst all shortest paths.
+ *
+ * Some context:
+ *
+ * For a given engine/bundle host, this finds the lowest common ancestor that is considered a
+ * host amongst _all_ engines by the same name in the project.
+ *
+ * For example, given the following package structure:
+ *
+ * --Project--
+ * / \
+ * / \
+ * Lazy Engine A \
+ * Addon A
+ * |
+ * |
+ * Lazy Engine B
+ * / \
+ * / \
+ * Lazy Engine A Lazy Engine C
+ *
+ * - The LCA host for Lazy Engine A is the project
+ * - The LCA host for Lazy Engine B is the project
+ * - The LCA host for Lazy Engine C is Lazy Engine B
+ *
+ * This also returns `hostAndAncestorBundledPackageInfos`, which are all bundled addons above a given host:
+ *
+ * - `hostAndAncestorBundledPackageInfos` for lazy engine A includes all non-lazy dependencies of its LCA host & above (in this case, just the project)
+ * - `hostAndAncestorBundledPackageInfos` for lazy engine B includes all non-lazy dependencies of its LCA host & above (in this case, just the project)
+ * - `hostAndAncestorBundledPackageInfos` for lazy engine C includes non-lazy deps of lazy engine B & non-lazy deps of the project (LCA host & above)
+ *
+ * This is intended to mimic the behavior of `ancestorHostAddons` in `ember-engines`:
+ * https://github.com/ember-engines/ember-engines/blob/master/packages/ember-engines/lib/engine-addon.js#L333
+ *
+ * Unfortunately, we can't easily repurpose the logic in `ember-engines` since the algorithm has to be different;
+ * in `ember-engines` we need access to the actual addon instance, however, this is intended to be used _during_
+ * addon instantiation, so we only have access to package-info objects. In having said this, we _can_ repurpose
+ * the `hostPackageInfo` to determine the LCA host; see below `findLCAHost`.
+ *
+ * @method getHostAddonInfo
+ * @param {PackageInfo} packageInfoForLazyEngine
+ * @return {{ hostPackageInfo: PackageInfo, hostAndAncestorBundledPackageInfos: Set<PackageInfo> }}
+ */
+ getHostAddonInfo(packageInfoForLazyEngine) {
+ const cacheKey = `${this.project._packageInfo.realPath}-${packageInfoForLazyEngine.realPath}`;
+
+ let hostInfoCacheEntry = this._hostAddonInfoCache.get(cacheKey);
+
+ if (hostInfoCacheEntry) {
+ return hostInfoCacheEntry;
+ }
+
+ if (!packageInfoForLazyEngine.isForEngine()) {
+ throw new Error(
+ `[ember-cli] \`${packageInfoForLazyEngine.name}\` is not an engine; \`getHostAddonInfo\` should only be used to find host information about engines`
+ );
+ }
+
+ const queue = [{ pkgInfo: this.project._packageInfo, path: [] }];
+ const visited = new Set();
+ const foundPaths = [];
+
+ while (queue.length) {
+ const { pkgInfo: currentPackageInfo, path } = queue.shift();
+
+ const {
+ addonMainPath,
+ inRepoAddons = [],
+ dependencyPackages = {},
+ devDependencyPackages = {},
+ } = currentPackageInfo;
+
+ const isCurrentPackageInfoProject = this.project._packageInfo === currentPackageInfo;
+
+ // don't process non-ember addons
+ if (!isCurrentPackageInfoProject && typeof addonMainPath !== 'string') {
+ continue;
+ }
+
+ // store found paths
+ if (currentPackageInfo === packageInfoForLazyEngine) {
+ foundPaths.push([...path]);
+ }
+
+ // don't process a given `PackageInfo` object more than once
+ if (!visited.has(currentPackageInfo)) {
+ visited.add(currentPackageInfo);
+
+ // add current package info to current path
+ path.push(currentPackageInfo);
+
+ queue.push(
+ ...[...inRepoAddons, ...Object.values(dependencyPackages), ...Object.values(devDependencyPackages)].map(
+ (pkgInfo) => ({ pkgInfo, path: [...path] })
+ )
+ );
+ }
+ }
+
+ const [hostPackageInfo, foundPath] = this._findNearestBundleHost(foundPaths, packageInfoForLazyEngine);
+
+ const hostAndAncestorBundledPackageInfos = foundPath
+ .filter((pkgInfo) => pkgInfo.isForBundleHost())
+ .reduce((acc, curr) => {
+ acc.push(...this._getBundledPackageInfos(curr));
+ return acc;
+ }, []);
+
+ hostInfoCacheEntry = {
+ hostPackageInfo,
+ hostAndAncestorBundledPackageInfos: new Set(hostAndAncestorBundledPackageInfos),
+ };
+
+ this._hostAddonInfoCache.set(cacheKey, hostInfoCacheEntry);
+ return hostInfoCacheEntry;
+ }
+
+ /**
+ * This returns the LCA host for a given engine; we use the associated package info
+ * to compute this (see `getHostAddonInfo` above); this finds the lowest common ancestor
+ * that is considered a host amongst _all_ engines by the same name in the project. This
+ * function is intended to replace the original behavior in `ember-engines`.
+ *
+ * For more info, see the original implementation here:
+ *
+ * https://github.com/ember-engines/ember-engines/blob/master/packages/ember-engines/lib/utils/find-lca-host.js
+ *
+ * @method findLCAHost
+ * @param {EngineAddon} engineInstance
+ * @return {EngineAddon|EmberApp}
+ */
+ findLCAHost(engineInstance) {
+ // only compute once for a given engine
+ // we're using the engine name as the cache key here because regardless of its
+ // version, lazy engines will always get output to: `engines-dist/${engineName}`
+ let lcaHost = this._lcaHostCache.get(engineInstance.name);
+
+ if (lcaHost) {
+ return lcaHost;
+ }
+
+ if (!engineInstance._packageInfo.isForEngine()) {
+ throw new Error(
+ `[ember-cli] \`findLCAHost\` should only be used for engines; \`${engineInstance.name}\` is not an engine`
+ );
+ }
+
+ const { hostPackageInfo } = this.getHostAddonInfo(engineInstance._packageInfo);
+
+ let curr = engineInstance;
+
+ while (curr && curr.parent) {
+ if (curr.app) {
+ lcaHost = curr.app;
+ break;
+ }
+
+ if (curr._packageInfo === hostPackageInfo) {
+ lcaHost = curr;
+ break;
+ }
+
+ curr = curr.parent;
+ }
+
+ if (lcaHost) {
+ this._lcaHostCache.set(engineInstance.name, lcaHost);
+ return lcaHost;
+ }
+
+ // this should _never_ be triggered
+ throw new Error(
+ `[ember-cli] Could not find an LCA host for: \`${engineInstance.name}\` (located at \`${
+ engineInstance.packageRoot || engineInstance.root
+ }\`)`
+ );
+ }
+}
+
+module.exports = HostInfoCache;
+
+
* No copy is made.
*/
loadAddon(addonInstance) {
- let pkgInfo = this._readPackage(addonInstance.root, addonInstance.pkg);
+ // to maintain backwards compatibility for consumers who create a new instance
+ // of the base addon model class directly and don't set `packageRoot`
+ let pkgInfo = this._readPackage(addonInstance.packageRoot || addonInstance.root, addonInstance.pkg);
// NOTE: the returned pkgInfo may contain errors, or may contain
// other packages that have errors. We will try to process
@@ -593,7 +593,7 @@
lib/models/package-info-cache/index.js
// If we have an ember-addon, check that the main exists and points
// to a valid file.
- if (pkgInfo.isAddon()) {
+ if (pkgInfo.isForAddon()) {
logger.info('%s is an addon', pkg.name);
// Note: when we have both 'main' and ember-addon:main, the latter takes precedence
diff --git a/api/files/lib_models_package-info-cache_node-modules-list.js.html b/api/files/lib_models_package-info-cache_node-modules-list.js.html
index 64dc9be..a864d79 100644
--- a/api/files/lib_models_package-info-cache_node-modules-list.js.html
+++ b/api/files/lib_models_package-info-cache_node-modules-list.js.html
@@ -42,7 +42,6 @@
// not actually be used.
this.valid = true;
- this.mayHaveAddons = isRoot || this.isAddon(); // mayHaveAddons used in index.js
+ this.mayHaveAddons = isRoot || this.isForAddon(); // mayHaveAddons used in index.js
this._hasDumpedInvalidAddonPackages = false;
}
@@ -244,7 +245,7 @@
lib/models/package-info-cache/package-info.js
* been added to the cache.
*
* Note: this is for ALL dependencies, not just addons. To get just
- * addons, filter the result by calling pkgInfo.isAddon().
+ * addons, filter the result by calling pkgInfo.isForAddon().
*
* Note: this is only intended for use from PackageInfoCache._resolveDependencies.
* It is not to be called directly by anything else.
@@ -302,10 +303,59 @@
lib/models/package-info-cache/package-info.js
return packages;
}
- isAddon() {
+ /**
+ * Indicate if this packageInfo is for a project. Should be called only after the project
+ * has been loaded (see {@link PackageInfoCache#loadProject} for details).
+ *
+ * @method isForProject
+ * @return {Boolean} true if this packageInfo is for a Project, false otherwise.
+ */
+ isForProject() {
+ return !!this.project && this.project.isEmberCLIProject && this.project.isEmberCLIProject();
+ }
+
+ /**
+ * Indicate if this packageInfo is for an Addon.
+ *
+ * @method isForAddon
+ * @return {Boolean} true if this packageInfo is for an Addon, false otherwise.
+ */
+ isForAddon() {
return isAddon(this.pkg.keywords);
}
+ /**
+ * Indicate if this packageInfo represents an engine.
+ *
+ * @method isForEngine
+ * @return {Boolean} true if this pkgInfo is configured as an engine & false otherwise
+ */
+ isForEngine() {
+ return isEngine(this.pkg.keywords);
+ }
+
+ /**
+ * Indicate if this packageInfo represents a lazy engine.
+ *
+ * @method isForLazyEngine
+ * @return {Boolean} true if this pkgInfo is configured as an engine and the
+ * module this represents has lazyLoading enabled, false otherwise.
+ */
+ isForLazyEngine() {
+ return this.isForEngine() && isLazyEngine(this._getAddonEntryPoint());
+ }
+
+ /**
+ * For use with the PerBundleAddonCache, is this packageInfo representing a
+ * bundle host (for now, a Project or a lazy engine).
+ *
+ * @method isForBundleHost
+ * @return {Boolean} true if this pkgInfo is for a bundle host, false otherwise.
+ */
+ isForBundleHost() {
+ return this.isForProject() || this.isForLazyEngine();
+ }
+
/**
* Add to a list of child addon PackageInfos for this packageInfo.
*
@@ -356,7 +406,7 @@
* Also, the assumption here is that this PackageInfo really is for an
* Addon, so we don't need to check each time.
*
+ * @private
* @method getAddonConstructor
* @return {AddonConstructor} an instance of a constructor function for the Addon class
* whose package information is stored in this object.
@@ -464,10 +511,7 @@
lib/models/package-info-cache/package-info.js
return this.addonConstructor;
}
- // Load the addon.
- // TODO: Future work - allow a time budget for loading each addon and warn
- // or error for those that take too long.
- let module = require(this.addonMainPath);
+ let module = this._getAddonEntryPoint();
let mainDir = path.dirname(this.addonMainPath);
let ctor;
@@ -475,24 +519,123 @@
lib/models/package-info-cache/package-info.js
if (typeof module === 'function') {
ctor = module;
ctor.prototype.root = ctor.prototype.root || mainDir;
+ ctor.prototype.packageRoot = ctor.prototype.packageRoot || this.realPath;
ctor.prototype.pkg = ctor.prototype.pkg || this.pkg;
} else {
const Addon = require('../addon'); // done here because of circular dependency
- ctor = Addon.extend(Object.assign({ root: mainDir, pkg: this.pkg }, module));
+ ctor = Addon.extend(Object.assign({ root: mainDir, packageRoot: this.realPath, pkg: this.pkg }, module));
}
- // XXX Probably want to store the timings here in PackageInfo,
- // rather than adding _meta_ to the constructor.
- // XXX Will also need to remove calls to it from various places.
ctor._meta_ = {
modulePath: this.addonMainPath,
- lookupDuration: 0,
- initializeIn: 0,
};
return (this.addonConstructor = ctor);
}
+
+ /**
+ * Construct an addon instance.
+ *
+ * NOTE: this does NOT call constructors for the child addons. That is left to
+ * the caller to do, so they can insert any other logic they want.
+ *
+ * @private
+ * @method constructAddonInstance
+ * @param {Project|Addon} parent the parent that directly contains this addon
+ * @param {Project} project the project that is/contains this addon
+ */
+ constructAddonInstance(parent, project) {
+ let start = Date.now();
+
+ let AddonConstructor = this.getAddonConstructor();
+
+ let addonInstance;
+
+ try {
+ addonInstance = new AddonConstructor(parent, project);
+ } catch (e) {
+ if (parent && parent.ui) {
+ parent.ui.writeError(e);
+ }
+
+ const SilentError = require('silent-error');
+ throw new SilentError(`An error occurred in the constructor for ${this.name} at ${this.realPath}`);
+ }
+
+ AddonConstructor._meta_.initializeIn = Date.now() - start;
+ addonInstance.constructor = AddonConstructor;
+
+ return addonInstance;
+ }
+
+ /**
+ * Create an instance of the addon represented by this packageInfo or (if we
+ * are supporting per-bundle caching and this is an allow-caching-per-bundle addon)
+ * check if we should be creating a proxy instead.
+ *
+ * NOTE: we assume that the value of 'allowCachingPerBundle' does not change between
+ * calls to the constructor! A given addon is either allowing or not allowing caching
+ * for an entire run.
+ *
+ * @method getAddonInstance
+ * @param {} parent the addon/project that is to be the direct parent of the
+ * addon instance created here
+ * @param {*} project the project that is to contain this addon instance
+ * @return {Object} the constructed instance of the addon
+ */
+ getAddonInstance(parent, project) {
+ let addonEntryPointModule = this._getAddonEntryPoint();
+ let addonInstance;
+
+ if (
+ PerBundleAddonCache.isEnabled() &&
+ project &&
+ project.perBundleAddonCache.allowCachingPerBundle(addonEntryPointModule)
+ ) {
+ addonInstance = project.perBundleAddonCache.getAddonInstance(parent, this);
+ } else {
+ addonInstance = this.constructAddonInstance(parent, project);
+ this.initChildAddons(addonInstance);
+ }
+
+ return addonInstance;
+ }
+
+ /**
+ * Initialize the child addons array of a newly-created addon instance. Normally when
+ * an addon derives from Addon, child addons will be created during 'setupRegistry' and
+ * this code is essentially unnecessary. But if an addon is created with custom constructors
+ * that don't call 'setupRegistry', any child addons may not yet be initialized.
+ *
+ * @method initChildAddons
+ * @param {Addon} addonInstance
+ */
+ initChildAddons(addonInstance) {
+ if (addonInstance.initializeAddons) {
+ addonInstance.initializeAddons();
+ } else {
+ addonInstance.addons = [];
+ }
+ }
+
+ /**
+ * Gets the addon entry point
+ *
+ * @method _getAddonEntryPoint
+ * @return {Object|Function} The exported addon entry point
+ * @private
+ */
+ _getAddonEntryPoint() {
+ if (!this.addonMainPath) {
+ throw new Error(`${this.pkg.name} at ${this.realPath} is missing its addon main file`);
+ }
+
+ // Load the addon.
+ // TODO: Future work - allow a time budget for loading each addon and warn
+ // or error for those that take too long.
+ return require(this.addonMainPath);
+ }
}
module.exports = PackageInfo;
diff --git a/api/files/lib_models_per-bundle-addon-cache_addon-proxy.js.html b/api/files/lib_models_per-bundle-addon-cache_addon-proxy.js.html
new file mode 100644
index 0000000..30e33c2
--- /dev/null
+++ b/api/files/lib_models_per-bundle-addon-cache_addon-proxy.js.html
@@ -0,0 +1,250 @@
+
+
+
+
+ lib/models/per-bundle-addon-cache/addon-proxy.js - ember-cli
+
+
+
+
+
+
+
+'use strict';
+
+const { TARGET_INSTANCE } = require('./target-instance');
+
+const CACHE_KEY_FOR_TREE_TRACKER = Symbol('CACHE_KEY_FOR_TREE_TRACKER');
+
+/**
+ * Validates that a new cache key for a given tree type matches the previous
+ * cache key for the same tree type. To opt-in to bundle addon caching for
+ * a given addon it's assumed that it returns stable cache keys; specifically
+ * this is because the interplay between bundle addon caching and `ember-engines`
+ * when transitive deduplication is enabled assumes stable cache keys, so we validate
+ * for this case here.
+ *
+ * @method validateCacheKey
+ * @param {Addon} realAddonInstance The real addon instance
+ * @param {string} treeType
+ * @param {string} newCacheKey
+ * @throws {Error} If the new cache key doesn't match the previous cache key
+ */
+function validateCacheKey(realAddonInstance, treeType, newCacheKey) {
+ let cacheKeyTracker = realAddonInstance[CACHE_KEY_FOR_TREE_TRACKER];
+
+ if (!cacheKeyTracker) {
+ cacheKeyTracker = {};
+ realAddonInstance[CACHE_KEY_FOR_TREE_TRACKER] = cacheKeyTracker;
+ }
+
+ cacheKeyTracker[treeType] = treeType in cacheKeyTracker ? cacheKeyTracker[treeType] : newCacheKey;
+
+ if (cacheKeyTracker[treeType] !== newCacheKey) {
+ throw new Error(
+ `[ember-cli] addon bundle caching can only be used on addons that have stable cache keys (previously \`${
+ cacheKeyTracker[treeType]
+ }\`, now \`${newCacheKey}\`; for addon \`${realAddonInstance.name}\` located at \`${
+ realAddonInstance.packageRoot || realAddonInstance.root
+ }\`)`
+ );
+ }
+}
+
+/**
+ * Returns a proxy to a target with specific handling for the
+ * `parent` property, as well has to handle the `app` property;
+ * that is, the proxy should maintain correct local state in
+ * closure scope for the `app` property if it happens to be set
+ * by `ember-cli`. Other than `parent` & `app`, this function also
+ * proxies _almost_ everything to `target[TARGET_INSTANCE] with a few
+ * exceptions: we trap & return `[]` for `addons`, and we don't return
+ * the original `included` (it's already called on the "real" addon
+ * by `ember-cli`).
+ *
+ * Note: the target is NOT the per-bundle cacheable instance of the addon. Rather,
+ * it is a cache entry POJO from PerBundleAddonCache.
+ *
+ * @method getAddonProxy
+ * @param targetCacheEntry the PerBundleAddonCache cache entry we are to proxy. It
+ * has one interesting property, the real addon instance the proxy is forwarding
+ * calls to (that property is not globally exposed).
+ * @param parent the parent object of the proxy being created (the same as
+ * the 'parent' property of a normal addon instance)
+ * @return Proxy
+ */
+function getAddonProxy(targetCacheEntry, parent) {
+ let _app;
+
+ // handle `preprocessJs` separately for Embroider
+ //
+ // some context here:
+ //
+ // Embroider patches `preprocessJs`, so we want to maintain local state within the
+ // proxy rather than allowing a patched `preprocessJs` set on the original addon
+ // instance itself
+ //
+ // for more info as to where this happens, see:
+ // https://github.com/embroider-build/embroider/blob/master/packages/compat/src/v1-addon.ts#L634
+ let _preprocessJs;
+
+ return new Proxy(targetCacheEntry, {
+ get(targetCacheEntry, property) {
+ if (property === 'parent') {
+ return parent;
+ }
+
+ if (property === 'app') {
+ return _app;
+ }
+
+ // only return `_preprocessJs` here if it was previously set to a patched version
+ if (property === 'preprocessJs' && _preprocessJs) {
+ return _preprocessJs;
+ }
+
+ // keep proxies from even trying to set or initialize addons
+ if (property === 'initializeAddons') {
+ return undefined;
+ }
+
+ // See the {@link index.js} file for a discussion of why the proxy 'addons'
+ // property returns an empty array.
+ if (property === 'addons') {
+ return [];
+ }
+
+ // allow access to the property pointing to the real instance.
+ if (property === TARGET_INSTANCE) {
+ return targetCacheEntry[TARGET_INSTANCE];
+ }
+
+ // `included` will be called on the "real" addon, so there's no need for it to be
+ // called again; instead we return a no-op implementation here
+ if (property === 'included') {
+ return () => undefined;
+ }
+
+ if (targetCacheEntry[TARGET_INSTANCE]) {
+ if (property !== 'constructor' && typeof targetCacheEntry[TARGET_INSTANCE][property] === 'function') {
+ // If we fall through to the Reflect.get just below, the 'this' context of the function when
+ // invoked is the proxy, not the original instance (so its local state is incorrect).
+ // Wrap the original methods to maintain the correct 'this' context.
+ return function _originalAddonPropMethodWrapper() {
+ let originalReturnValue = targetCacheEntry[TARGET_INSTANCE][property](...arguments);
+
+ if (property === 'cacheKeyForTree') {
+ const treeType = arguments[0];
+ validateCacheKey(targetCacheEntry[TARGET_INSTANCE], treeType, originalReturnValue);
+ }
+
+ return originalReturnValue;
+ };
+ }
+
+ return Reflect.get(targetCacheEntry[TARGET_INSTANCE], property);
+ }
+
+ return Reflect.get(targetCacheEntry, property);
+ },
+ set(targetCacheEntry, property, value) {
+ if (property === 'app') {
+ _app = value;
+ return true;
+ }
+
+ if (property === 'preprocessJs') {
+ _preprocessJs = value;
+ return true;
+ }
+
+ if (targetCacheEntry[TARGET_INSTANCE]) {
+ return Reflect.set(targetCacheEntry[TARGET_INSTANCE], property, value);
+ }
+
+ return Reflect.set(targetCacheEntry, property, value);
+ },
+ });
+}
+
+module.exports = { getAddonProxy };
+
+
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const isLazyEngine = require('../../utilities/is-lazy-engine');
+const { getAddonProxy } = require('./addon-proxy');
+const logger = require('heimdalljs-logger')('ember-cli:per-bundle-addon-cache');
+const { TARGET_INSTANCE } = require('./target-instance');
+
+function defaultAllowCachingPerBundle({ addonEntryPointModule }) {
+ return (
+ addonEntryPointModule.allowCachingPerBundle ||
+ (addonEntryPointModule.prototype && addonEntryPointModule.prototype.allowCachingPerBundle)
+ );
+}
+
+/**
+ * Resolves the perBundleAddonCacheUtil; this prefers the custom provided version by
+ * the consuming application, and defaults to an internal implementation here.
+ *
+ * @method resolvePerBundleAddonCacheUtil
+ * @param {Project} project
+ * @return {{allowCachingPerBundle: Function}}
+ */
+function resolvePerBundleAddonCacheUtil(project) {
+ const relativePathToUtil =
+ project.pkg && project.pkg['ember-addon'] && project.pkg['ember-addon'].perBundleAddonCacheUtil;
+
+ if (typeof relativePathToUtil === 'string') {
+ const absolutePathToUtil = path.resolve(project.root, relativePathToUtil);
+
+ if (!fs.existsSync(absolutePathToUtil)) {
+ throw new Error(
+ `[ember-cli] the provided \`${relativePathToUtil}\` for \`ember-addon.perBundleAddonCacheUtil\` does not exist`
+ );
+ }
+
+ return require(absolutePathToUtil);
+ }
+
+ return {
+ allowCachingPerBundle: defaultAllowCachingPerBundle,
+ };
+}
+
+/**
+ * For large applications with many addons (and many instances of each, resulting in
+ * potentially many millions of addon instances during a build), the build can become
+ * very, very slow (tens of minutes) partially due to the sheer number of addon instances.
+ * The PerBundleAddonCache deals with this slowness by doing 3 things:
+ *
+ * (1) Making only a single copy of each of certain addons and their dependent addons
+ * (2) Replacing any other instances of those addons with Proxy copies to the single instance
+ * (3) Having the Proxies return an empty array for their dependent addons, rather
+ * than proxying to the contents of the single addon instance. This gives up the
+ * ability of the Proxies to traverse downward into their child addons,
+ * something that many addons do not do anyway, for the huge reduction in duplications
+ * of those child addons. For applications that enable `ember-engines` dedupe logic,
+ * that logic is stateful, and having the Proxies allow access to the child addons array
+ * just breaks everything, because that logic will try multiple times to remove items
+ * it thinks are duplicated, messing up the single copy of the child addon array.
+ * See the explanation of the dedupe logic in
+ * {@link https://github.com/ember-engines/ember-engines/blob/master/packages/ember-engines/lib/utils/deeply-non-duplicated-addon.js}
+ *
+ * What follows are the more technical details of how the PerBundleAddonCache implements
+ * the above 3 behaviors.
+ *
+ * This class supports per-bundle-host (bundle host = project or lazy engine)
+ * caching of addon instances. During addon initialization we cannot add a
+ * cache to each bundle host object AFTER it is instantiated because running the
+ * addon constructor ultimately causes Addon class `setupRegistry` code to
+ * run which instantiates child addons, which need the cache to already be
+ * in place for the parent bundle host.
+ * We handle this by providing a global cache that exists independent of the
+ * bundle host objects. That is this object.
+ *
+ * There are a number of "behaviors" being implemented by this object and
+ * its contents. They are:
+ * (1) Any addon that is a lazy engine has only a single real instance per
+ * project - all other references to the lazy engine are to be proxies. These
+ * lazy engines are compared by name, not by packageInfo.realPath.
+ * (2) Any addon that is not a lazy engine, there is only a single real instance
+ * of the addon per "bundle host" (i.e. lazy engine or project).
+ * (3) An optimization - any addon that is in a lazy engine but that is also
+ * in bundled by its LCA host - the single instance is the one bundled by this
+ * host. All other instances (in any lazy engine) are proxies.
+ *
+ * NOTE: the optimization is only enabled if the environment variable that controls
+ * `ember-engines` transitive deduplication (process.env.EMBER_ENGINES_ADDON_DEDUPE)
+ * is set to a truthy value. For more info, see:
+ * https://github.com/ember-engines/ember-engines/blob/master/packages/ember-engines/lib/engine-addon.js#L396
+ *
+ * @public
+ * @class PerBundleAddonCache
+ */
+class PerBundleAddonCache {
+ constructor(project) {
+ this.project = project;
+
+ // The cache of bundle-host package infos and their individual addon caches.
+ // The cache is keyed by package info (representing a bundle host (project or
+ // lazy engine)) and an addon instance cache to bundle with that bundle host.
+ this.bundleHostCache = new Map();
+
+ // Indicate if ember-engines transitive dedupe is enabled.
+ this.engineAddonTransitiveDedupeEnabled = !!process.env.EMBER_ENGINES_ADDON_DEDUPE;
+ this._perBundleAddonCacheUtil = resolvePerBundleAddonCacheUtil(this.project);
+
+ // For stats purposes, counts on the # addons and proxies created. Addons we
+ // can compare against the bundleHostCache addon caches. Proxies, not so much,
+ // but we'll count them here.
+ this.numAddonInstances = 0;
+ this.numProxies = 0;
+ }
+
+ /**
+ * The default implementation here is to indicate if the original addon entry point has
+ * the `allowCachingPerBundle` flag set either on itself or on its prototype.
+ *
+ * If a consuming application specifies a relative path to a custom utility via the
+ * `ember-addon.perBundleAddonCacheUtil` configuration, we prefer the custom implementation
+ * provided by the consumer.
+ *
+ * @method allowCachingPerBundle
+ * @param {Object|Function} addonEntryPointModule
+ * @return {Boolean} true if the given constructor function or class supports caching per bundle, false otherwise
+ */
+ allowCachingPerBundle(addonEntryPointModule) {
+ return this._perBundleAddonCacheUtil.allowCachingPerBundle({ addonEntryPointModule });
+ }
+
+ /**
+ * Creates a cache entry for the bundleHostCache. Because we want to use the same sort of proxy
+ * for both bundle hosts and for 'regular' addon instances (though their cache entries have
+ * slightly different structures) we'll use the Symbol from getAddonProxy.
+ *
+ * @method createBundleHostCacheEntry
+ * @param {PackageInfo} bundleHostPkgInfo bundle host's pkgInfo.realPath
+ * @return {Object} an object in the form of a bundle-host cache entry
+ */
+ createBundleHostCacheEntry(bundleHostPkgInfo) {
+ return { [TARGET_INSTANCE]: null, realPath: bundleHostPkgInfo.realPath, addonInstanceCache: new Map() };
+ }
+
+ /**
+ * Create a cache entry object for a given (non-bundle-host) addon to put into
+ * an addon cache.
+ *
+ * @method createAddonCacheEntry
+ * @param {Addon} addonInstance the addon instance to cache
+ * @param {String} addonRealPath the addon's pkgInfo.realPath
+ * @return {Object} an object in the form of an addon-cache entry
+ */
+ createAddonCacheEntry(addonInstance, addonRealPath) {
+ return { [TARGET_INSTANCE]: addonInstance, realPath: addonRealPath };
+ }
+
+ /**
+ * Given a parent object of a potential addon (another addon or the project),
+ * go up the 'parent' chain to find the potential addon's bundle host object
+ * (i.e. lazy engine or project.) Because Projects are always bundle hosts,
+ * this should always pass, but we'll throw if somehow it doesn't work.
+ *
+ * @method findBundleHost
+ * @param {Project|Addon} addonParent the direct parent object of a (potential or real) addon.
+ * @param {PackageInfo} addonPkgInfo the PackageInfo for an addon being instantiated. This is only
+ * used for information if an error is going to be thrown.
+ * @return {Object} the object in the 'parent' chain that is a bundle host.
+ * @throws {Error} if there is not bundle host
+ */
+ findBundleHost(addonParent, addonPkgInfo) {
+ let curr = addonParent;
+
+ while (curr) {
+ if (curr === this.project) {
+ return curr;
+ }
+
+ if (isLazyEngine(curr)) {
+ // if we're building a lazy engine in isolation, prefer that the bundle host is
+ // the project, not the lazy engine addon instance
+ if (curr.parent === this.project && curr._packageInfo === this.project._packageInfo) {
+ return this.project;
+ }
+
+ return curr;
+ }
+
+ curr = curr.parent;
+ }
+
+ // the following should not be able to happen given that Projects are always
+ // bundle hosts, but just in case, throw an error if we didn't find one.
+ throw new Error(`Addon at path\n ${addonPkgInfo.realPath}\n has 'allowCachingPerBundle' but has no bundleHost`);
+ }
+
+ /**
+ * An optimization we support from lazy engines is the following:
+ *
+ * If an addon instance is supposed to be bundled with a particular lazy engine, and
+ * same addon is also to be bundled by a common LCA host, prefer the one bundled by the
+ * host (since it's ultimately going to be deduped later by `ember-engines`).
+ *
+ * NOTE: this only applies if this.engineAddonTransitiveDedupeEnabled is truthy. If it is not,
+ * the bundle host always "owns" the addon instance.
+ *
+ * If deduping is enabled and the LCA host also depends on the same addon,
+ * the lazy-engine instances of the addon will all be proxies to the one in
+ * the LCA host. This function indicates whether the bundle host passed in
+ * (either the project or a lazy engine) is really the bundle host to "own" the
+ * new addon.
+ *
+ * @method bundleHostOwnsInstance
+ * @param (Object} bundleHost the project or lazy engine that is trying to "own"
+ * the new addon instance specified by addonPkgInfo
+ * @param {PackageInfo} addonPkgInfo the PackageInfo of the potential new addon instance
+ * @return {Boolean} true if the bundle host is to "own" the instance, false otherwise.
+ */
+ bundleHostOwnsInstance(bundleHost, addonPkgInfo) {
+ if (isLazyEngine(bundleHost)) {
+ return (
+ !this.engineAddonTransitiveDedupeEnabled ||
+ !this.project.hostInfoCache
+ .getHostAddonInfo(bundleHost._packageInfo)
+ .hostAndAncestorBundledPackageInfos.has(addonPkgInfo)
+ );
+ }
+
+ return true;
+ }
+
+ findBundleOwner(bundleHost, addonPkgInfo) {
+ if (bundleHost === this.project._packageInfo) {
+ return bundleHost;
+ }
+
+ let { hostPackageInfo, hostAndAncestorBundledPackageInfos } =
+ this.project.hostInfoCache.getHostAddonInfo(bundleHost);
+
+ if (!hostAndAncestorBundledPackageInfos.has(addonPkgInfo)) {
+ return bundleHost;
+ }
+
+ return this.findBundleOwner(hostPackageInfo, addonPkgInfo);
+ }
+
+ /**
+ * Called from PackageInfo.getAddonInstance(), return an instance of the requested
+ * addon or a Proxy, based on the type of addon and its bundle host.
+ *
+ * @method getAddonInstance
+ * @param {Addon|Project} parent the parent Addon or Project this addon instance is
+ * a child of.
+ * @param {*} addonPkgInfo the PackageInfo for the addon being created.
+ * @return {Addon|Proxy} An addon instance (for the first copy of the addon) or a Proxy.
+ * An addon that is a lazy engine will only ever have a single copy in the cache.
+ * An addon that is not will have 1 copy per bundle host (Project or lazy engine),
+ * except if it is an addon that's also owned by a given LCA host and transitive
+ * dedupe is enabled (`engineAddonTransitiveDedupeEnabled`), in which case it will
+ * only have a single copy in the project's addon cache.
+ */
+ getAddonInstance(parent, addonPkgInfo) {
+ // If the new addon is itself a bundle host (i.e. lazy engine), there is only one
+ // instance of the bundle host, and it's in the entries of the bundleHostCache, outside
+ // of the 'regular' addon caches. Because 'setupBundleHostCache' ran during construction,
+ // we know that an entry is in the cache with this engine name.
+ if (addonPkgInfo.isForBundleHost()) {
+ let cacheEntry = this._getBundleHostCacheEntry(addonPkgInfo);
+
+ if (cacheEntry[TARGET_INSTANCE]) {
+ logger.debug(`About to construct BR PROXY to cache entry for addon at: ${addonPkgInfo.realPath}`);
+ this.numProxies++;
+ return getAddonProxy(cacheEntry, parent);
+ } else {
+ // create an instance, put it in the pre-existing cache entry, then
+ // return it (as the first instance of the lazy engine.)
+ logger.debug(`About to fill in BR EXISTING cache entry for addon at: ${addonPkgInfo.realPath}`);
+ this.numAddonInstances++;
+ let addon = addonPkgInfo.constructAddonInstance(parent, this.project);
+ cacheEntry[TARGET_INSTANCE] = addon; // cache BEFORE initializing child addons
+ addonPkgInfo.initChildAddons(addon);
+ return addon;
+ }
+ }
+
+ // We know now we're asking for a 'regular' (non-bundle-host) addon instance.
+
+ let bundleHost = this.findBundleHost(parent, addonPkgInfo);
+
+ // if the bundle host "owns" the new addon instance
+ // * Do we already have an instance of the addon cached?
+ // * If so, make a proxy for it.
+ // * If not, make a new instance of the addon and cache it in the
+ // bundle host's addon cache.
+ // If not, it means the bundle host is a lazy engine but the LCA host also uses
+ // the addon and deduping is enabled
+ // * If the LCA host already has a cached entry, return a proxy to that
+ // * If it does not, create a 'blank' cache entry and return a proxy to that.
+ // When the addon is encountered later when processing the LCA host's addons,
+ // fill in the instance.
+ if (this.bundleHostOwnsInstance(bundleHost, addonPkgInfo)) {
+ let bundleHostCacheEntry = this._getBundleHostCacheEntry(bundleHost._packageInfo);
+ let addonInstanceCache = bundleHostCacheEntry.addonInstanceCache;
+ let addonCacheEntry = addonInstanceCache.get(addonPkgInfo.realPath);
+ let addonInstance;
+
+ if (addonCacheEntry) {
+ if (addonCacheEntry[TARGET_INSTANCE]) {
+ logger.debug(`About to construct REGULAR ADDON PROXY for addon at: ${addonPkgInfo.realPath}`);
+ this.numProxies++;
+ return getAddonProxy(addonCacheEntry, parent);
+ } else {
+ // the cache entry was created 'empty' by an earlier call, indicating
+ // an addon that is used in a lazy engine but also used by its LCA host,
+ // and we're now creating the instance for the LCA host.
+ // Fill in the entry and return the new instance.
+ logger.debug(`About to fill in REGULAR ADDON EXISTING cache entry for addon at: ${addonPkgInfo.realPath}`);
+ this.numAddonInstances++;
+ addonInstance = addonPkgInfo.constructAddonInstance(parent, this.project);
+ addonCacheEntry[TARGET_INSTANCE] = addonInstance; // cache BEFORE initializing child addons
+ addonPkgInfo.initChildAddons(addonInstance);
+ return addonInstance;
+ }
+ }
+
+ // There is no entry for this addon in the bundleHost's addon cache. Create a new
+ // instance, cache it in the addon cache, and return it.
+ logger.debug(`About to construct REGULAR ADDON NEW cache entry for addon at: ${addonPkgInfo.realPath}`);
+ this.numAddonInstances++;
+ addonInstance = addonPkgInfo.constructAddonInstance(parent, this.project);
+ addonCacheEntry = this.createAddonCacheEntry(addonInstance, addonPkgInfo.realPath);
+ addonInstanceCache.set(addonPkgInfo.realPath, addonCacheEntry); // cache BEFORE initializing child addons
+ addonPkgInfo.initChildAddons(addonInstance);
+ return addonInstance;
+ } else {
+ // The bundleHost is not the project but the some ancestor bundles the addon and
+ // deduping is enabled, so the cache entry needs to go in the bundle owner's cache.
+ // Get/create an empty cache entry and return a proxy to it. The bundle owner will
+ // set the instance later (see above).
+ let bundleHostCacheEntry = this._getBundleHostCacheEntry(
+ this.findBundleOwner(bundleHost._packageInfo, addonPkgInfo)
+ );
+ let addonCacheEntry = bundleHostCacheEntry.addonInstanceCache.get(addonPkgInfo.realPath);
+
+ if (!addonCacheEntry) {
+ logger.debug(`About to construct REGULAR ADDON EMPTY cache entry for addon at: ${addonPkgInfo.realPath}`);
+ addonCacheEntry = this.createAddonCacheEntry(null, addonPkgInfo.realPath);
+ bundleHostCacheEntry.addonInstanceCache.set(addonPkgInfo.realPath, addonCacheEntry);
+ }
+
+ logger.debug(`About to construct REGULAR ADDON PROXY for EMPTY addon at: ${addonPkgInfo.realPath}`);
+ this.numProxies++;
+ return getAddonProxy(addonCacheEntry, parent);
+ }
+ }
+
+ getPathsToAddonsOptedIn() {
+ const addonSet = new Set();
+
+ for (const [, { addonInstanceCache }] of this.bundleHostCache) {
+ Array.from(addonInstanceCache.keys()).forEach((realPath) => {
+ addonSet.add(realPath);
+ });
+ }
+
+ return Array.from(addonSet);
+ }
+
+ _getBundleHostCacheEntry(pkgInfo) {
+ let cacheEntry = this.bundleHostCache.get(pkgInfo);
+
+ if (!cacheEntry) {
+ cacheEntry = this.createBundleHostCacheEntry(pkgInfo);
+ this.bundleHostCache.set(pkgInfo, cacheEntry);
+ }
+
+ return cacheEntry;
+ }
+
+ // Support for per-bundle addon caching is GLOBAL opt OUT (unless you explicitly set
+ // EMBER_CLI_ADDON_INSTANCE_CACHING to false, it will be enabled.) If you opt out, that
+ // overrides setting `allowCachingPerBundle` for any particular addon type to true.
+ // To help make testing easier, we'll expose the setting as a function so it can be
+ // called multiple times and evaluate each time.
+ static isEnabled() {
+ return process.env.EMBER_CLI_ADDON_INSTANCE_CACHING !== 'false';
+ }
+}
+
+module.exports = PerBundleAddonCache;
+
+
+'use strict';
+
+/**
+ * A Symbol constant for sharing between index.js and addon-proxy.js rather than
+ * putting the symbol into the Symbol global cache. The symbol is used in per-bundle
+ * cache entries to refer to the field that points at the real instance that a Proxy
+ * refers to.
+ *
+ * @type Symbol
+ * @private
+ * @final
+ */
+const TARGET_INSTANCE = Symbol('_targetInstance_');
+
+module.exports.TARGET_INSTANCE = TARGET_INSTANCE;
+
+
this.addonPackages = Object.create(null);
this.addons = [];
this.liveReloadFilterPatterns = [];
- this.setupBowerDirectory();
this.configCache = new Map();
+ this.hostInfoCache = new HostInfoCache(this);
/**
Set when the `Watcher.detectWatchman` helper method finishes running,
so that other areas of the system can be aware that watchman is being used.
For example, this information is used in the broccoli build pipeline to know
- if we can watch additional directories (like bower_components) "cheaply".
+ if we can watch additional directories "cheaply".
Contains `enabled` and `version`.
@@ -178,30 +176,10 @@
lib/models/project.js
if (this.packageInfoCache.hasErrors()) {
this.packageInfoCache.showErrors();
}
- }
-
- /**
- * Sets the name of the bower directory for this project
- *
- * @private
- * @method setupBowerDirectory
- */
- setupBowerDirectory() {
- let bowerrcPath = path.join(this.root, '.bowerrc');
-
- logger.info('bowerrc path: %s', bowerrcPath);
- if (fs.existsSync(bowerrcPath)) {
- try {
- this.bowerDirectory = fs.readJsonSync(bowerrcPath).directory;
- } catch (exception) {
- logger.info('failed to parse bowerc: %s', exception);
- this.bowerDirectory = null;
- }
+ if (PerBundleAddonCache.isEnabled()) {
+ this.perBundleAddonCache = new PerBundleAddonCache(this);
}
-
- this.bowerDirectory = this.bowerDirectory || 'bower_components';
- logger.info('bowerDirectory: %s', this.bowerDirectory);
}
// Checks whether the project's npm dependencies are
@@ -285,7 +263,7 @@
lib/models/project.js
@return {Boolean} Whether or not this is an Ember CLI Addon.
*/
isEmberCLIAddon() {
- return !!this.pkg.keywords && this.pkg.keywords.indexOf('ember-addon') > -1;
+ return !!this.pkg && !!this.pkg.keywords && this.pkg.keywords.indexOf('ember-addon') > -1;
}
/**
@@ -321,7 +299,7 @@
return Object.assign({}, devDependencies, pkg['dependencies']);
}
- /**
- Returns the bower dependencies for this project.
-
- @private
- @method bowerDependencies
- @param {String} bower Path to bower.json
- @return {Object} Bower dependencies
- */
- bowerDependencies(bower) {
- if (!bower) {
- let bowerPath = path.join(this.root, 'bower.json');
- bower = fs.existsSync(bowerPath) ? require(bowerPath) : {};
- }
- return Object.assign({}, bower['devDependencies'], bower['dependencies']);
- }
-
/**
Provides the list of paths to consult for addons that may be provided
internally to this project. Used for middleware addons with built-in support.
@@ -507,17 +469,15 @@
lib/models/project.js
*/
discoverAddons() {
if (this._didDiscoverAddons) {
- this._didDiscoverAddons = true;
return;
}
+ this._didDiscoverAddons = true;
- let pkgInfo = this.packageInfoCache.getEntry(this.root);
-
- let addonPackageList = pkgInfo.discoverProjectAddons();
- this.addonPackages = pkgInfo.generateAddonPackages(addonPackageList);
+ let addonPackageList = this._packageInfo.discoverProjectAddons();
+ this.addonPackages = this._packageInfo.generateAddonPackages(addonPackageList);
// in case any child addons are invalid, dump to the console about them.
- pkgInfo.dumpInvalidAddonPackages(addonPackageList);
+ this._packageInfo.dumpInvalidAddonPackages(addonPackageList);
}
/**
@@ -531,6 +491,7 @@
}
/**
- Reloads package.json
+ Reloads package.json of the project. Clears and reloads the packageInfo and
+ per-bundle addon cache, too.
@private
@method reloadPkg
@@ -671,6 +633,15 @@
lib/models/project.js
this.packageInfoCache.reloadProjects();
+ // update `_packageInfo` after reloading projects from the `PackageInfoCache` instance
+ // if we don't do this we get into a state where `_packageInfo` is referencing the old
+ // pkginfo object that hasn't been updated/reloaded
+ this._packageInfo = this.packageInfoCache.loadProject(this);
+
+ if (PerBundleAddonCache.isEnabled()) {
+ this.perBundleAddonCache = new PerBundleAddonCache(this);
+ }
+
return this.pkg;
}
@@ -683,6 +654,7 @@
findAddonByName(name) {
this.initializeAddons();
- return findAddonByName(this.addons, name);
+ return this.addons.find((addon) => addon.pkg?.name === name);
}
/**
Generate test file contents.
This method is supposed to be overwritten by test framework addons
- like `ember-qunit` and `ember-mocha`.
+ like `ember-qunit`.
@public
@method generateTestFile
@@ -725,9 +697,13 @@
lib/models/project.js
}
/**
- Returns a new project based on the first package.json that is found
+ Returns a new project based on the first `package.json` that is found
in `pathName`.
+ If the above `package.json` specifies `ember-addon.projectRoot`, we load
+ the project based on the relative path between this directory and the
+ specified `projectRoot`.
+
@private
@static
@method closestSync
@@ -741,15 +717,26 @@
lib/models/project.js
let ui = ensureUI(_ui);
let directory = findupPath(pathName);
+ let pkg = fs.readJsonSync(path.join(directory, 'package.json'));
logger.info('found package.json at %s', directory);
+ // allow `package.json` files to specify where the actual project lives
+ if (pkg && pkg['ember-addon'] && typeof pkg['ember-addon'].projectRoot === 'string') {
+ if (fs.existsSync(path.join(directory, 'ember-cli-build.js'))) {
+ throw new Error(
+ `Both \`ember-addon.projectRoot\` and \`ember-cli-build.js\` exist as part of \`${directory}\``
+ );
+ }
+
+ return Project.closestSync(path.join(directory, pkg['ember-addon'].projectRoot), _ui, _cli);
+ }
+
let relative = path.relative(directory, pathName);
if (relative.indexOf('tmp') === 0) {
logger.info('ignoring parent project since we are in the tmp folder of the project');
return Project.nullProject(_ui, _cli);
}
- let pkg = fs.readJsonSync(path.join(directory, 'package.json'));
logger.info('project name: %s', pkg && pkg.name);
if (!isEmberCliProject(pkg)) {
diff --git a/api/files/lib_models_task.js.html b/api/files/lib_models_task.js.html
index baaac6f..2dc1458 100644
--- a/api/files/lib_models_task.js.html
+++ b/api/files/lib_models_task.js.html
@@ -42,7 +42,6 @@
if (semver.gte(version, '2.0.0')) {
logger.warn('yarn --version: %s', version);
- this.ui.writeWarnLine(`Yarn v2 is not fully supported. Proceeding with yarn: ${version}`);
+ let yarnConfig = await this.yarn(['config', 'get', 'nodeLinker']);
+ let nodeLinker = yarnConfig.stdout.trim();
+ if (nodeLinker !== 'node-modules') {
+ this.ui.writeWarnLine(`Yarn v2 is not fully supported. Proceeding with yarn: ${version}`);
+ }
} else {
logger.info('yarn --version: %s', version);
}
- this.useYarn = true;
-
- return { yarnVersion: version };
+ return { name: 'yarn', version };
} catch (error) {
logger.error('yarn --version failed: %s', error);
@@ -153,31 +162,35 @@
lib/tasks/npm-task.js
}
}
- async checkNpmVersion() {
+ async checkPNPM() {
try {
- let result = await this.npm(['--version']);
+ let result = await this.pnpm(['--version']);
let version = result.stdout;
- logger.info('npm --version: %s', version);
- let ok = semver.satisfies(version, this.versionConstraints);
- if (!ok) {
- logger.warn('npm --version is outside of version constraint: %s', this.versionConstraints);
+ logger.info('pnpm --version: %s', version);
- let below = semver.ltr(version, this.versionConstraints);
- if (below) {
- throw new SilentError(
- 'Ember CLI is now using the global npm, but your npm version is outdated.\n' +
- 'Please update your global npm version by running: npm install -g npm'
- );
- }
+ return { name: 'pnpm', version };
+ } catch (error) {
+ logger.error('pnpm --version failed: %s', error);
- this.ui.writeWarnLine(
- 'Ember CLI is using the global npm, but your npm version has not yet been ' +
- 'verified to work with the current Ember CLI release.'
+ if (error.code === 'ENOENT') {
+ throw new SilentError(
+ 'Ember CLI is now using pnpm, but was not able to find it.\n' +
+ 'Please install pnpm using the instructions at https://pnpm.io/installation'
);
}
- return { npmVersion: version };
+ throw error;
+ }
+ }
+
+ async checkNpmVersion() {
+ try {
+ let result = await this.npm(['--version']);
+ let version = result.stdout;
+ logger.info('npm --version: %s', version);
+
+ return { name: 'npm', version };
} catch (error) {
logger.error('npm --version failed: %s', error);
@@ -206,37 +219,55 @@
lib/tasks/npm-task.js
* @method findPackageManager
* @return {Promise}
*/
- async findPackageManager() {
- if (this.useYarn === true) {
+ async findPackageManager(packageManager = null) {
+ if (packageManager === 'yarn') {
logger.info('yarn requested -> trying yarn');
return this.checkYarn();
}
- if (this.useYarn === false) {
+ if (packageManager === 'npm') {
logger.info('npm requested -> using npm');
return this.checkNpmVersion();
}
- if (!this.hasYarnLock()) {
- logger.info('yarn.lock not found -> using npm');
- return this.checkNpmVersion();
+ if (packageManager === 'pnpm') {
+ logger.info('pnpm requested -> using pnpm');
+ return this.checkPNPM();
}
- logger.info('yarn.lock found -> trying yarn');
- try {
- const yarnResult = await this.checkYarn();
- logger.info('yarn found -> using yarn');
- return yarnResult;
- } catch (_err) {
- logger.info('yarn not found -> using npm');
- return this.checkNpmVersion();
+ if (this.hasYarnLock()) {
+ logger.info('yarn.lock found -> trying yarn');
+ try {
+ const yarnResult = await this.checkYarn();
+ logger.info('yarn found -> using yarn');
+ return yarnResult;
+ } catch (_err) {
+ logger.info('yarn not found');
+ }
+ } else {
+ logger.info('yarn.lock not found');
}
+
+ if (await this.hasPNPMLock()) {
+ logger.info('pnpm-lock.yaml found -> trying pnpm');
+ try {
+ let result = await this.checkPNPM();
+ logger.info('pnpm found -> using pnpm');
+ return result;
+ } catch (_err) {
+ logger.info('pnpm not found');
+ }
+ } else {
+ logger.info('pnpm-lock.yaml not found');
+ }
+
+ logger.info('using npm');
+ return this.checkNpmVersion();
}
async run(options) {
- this.useYarn = options.useYarn;
+ this.packageManager = await this.findPackageManager(options.packageManager);
- let result = await this.findPackageManager();
let ui = this.ui;
let startMessage = this.formatStartMessage(options.packages);
let completeMessage = this.formatCompleteMessage(options.packages);
@@ -247,31 +278,22 @@
lib/tasks/npm-task.js
ui.writeLine(prependEmoji('🚧', 'Installing packages... This might take a couple of minutes.'));
ui.startProgress(chalk.green(startMessage));
- let promise;
- if (this.useYarn) {
- this.yarnVersion = result.yarnVersion;
- let args = this.toYarnArgs(this.command, options);
- promise = this.yarn(args);
- } else {
- let args = this.toNpmArgs(this.command, options);
- promise = this.npm(args);
-
- // as of 2018-10-09 npm 5 and 6 _break_ the hierarchy of `node_modules`
- // after a `npm install foo` (deletes files/folders other than
- // what was directly installed) in some circumstances, see:
- //
- // * https://github.com/npm/npm/issues/16853
- // * https://github.com/npm/npm/issues/17379
- //
- // this ensures that we run a full `npm install` **after** any `npm
- // install foo` runs to ensure that we have a fully functional
- // node_modules hierarchy
- if (result.npmVersion && semver.lt(result.npmVersion, '5.7.1')) {
- promise = promise.then(() => this.npm(['install']));
+ try {
+ if (this.packageManager.name === 'yarn') {
+ let args = this.toYarnArgs(this.command, options);
+ await this.yarn(args);
+ } else if (this.packageManager.name === 'pnpm') {
+ let args = this.toPNPMArgs(this.command, options);
+ await this.pnpm(args);
+ } else {
+ let args = this.toNpmArgs(this.command, options);
+ await this.npm(args);
}
+ } finally {
+ ui.stopProgress();
}
- return promise.finally(() => ui.stopProgress()).then(() => ui.writeLine(chalk.green(completeMessage)));
+ ui.writeLine(chalk.green(completeMessage));
}
toNpmArgs(command, options) {
@@ -294,9 +316,9 @@
let app = config.app;
let options = config.options;
let watcher = options.watcher;
-
- let baseURL = options.rootURL === '' ? '/' : cleanBaseURL(options.rootURL || options.baseURL);
+ let rootURL = options.rootURL === '' ? '/' : cleanBaseURL(options.rootURL);
app.use(async (req, _, next) => {
try {
- const results = await watcher;
+ let results;
+ try {
+ results = await watcher;
+ } catch (e) {
+ // This means there was a build error, so we won't actually be serving
+ // index.html, and we have nothing to do. We have to catch it here,
+ // though, or it will go uncaught and cause the process to exit.
+ return;
+ }
+
if (this.shouldHandleRequest(req, options)) {
- let assetPath = req.path.slice(baseURL.length);
+ let assetPath = req.path.slice(rootURL.length);
let isFile = false;
try {
@@ -142,7 +148,7 @@
module.exports = class TestsServerAddon {
/**
* This addon is used to serve the QUnit or Mocha test runner
- * at `baseURL + '/tests'`.
+ * at `rootURL + '/tests'`.
*
* @class TestsServerAddon
* @constructor
@@ -104,9 +102,8 @@
lib/tasks/server/middleware/tests-server/index.js
let app = config.app;
let options = config.options;
let watcher = options.watcher;
-
- let baseURL = options.rootURL === '' ? '/' : cleanBaseURL(options.rootURL || options.baseURL);
- let testsPath = `${baseURL}tests`;
+ let rootURL = options.rootURL === '' ? '/' : cleanBaseURL(options.rootURL);
+ let testsPath = `${rootURL}tests`;
app.use(async (req, _, next) => {
let results;
@@ -124,7 +121,7 @@
logger.info('isForTests: %o', isForTests);
if (isForTests && (hasHTMLHeader || hasWildcardHeader) && req.method === 'GET') {
- let assetPath = req.path.slice(baseURL.length);
+ let assetPath = req.path.slice(rootURL.length);
let filePath = path.join(directory, assetPath);
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
- // N.B., `baseURL` will end with a slash as it went through `cleanBaseURL`
- let newURL = `${baseURL}tests/index.html`;
+ // N.B., `rootURL` will end with a slash as it went through `cleanBaseURL`
+ let newURL = `${rootURL}tests/index.html`;
logger.info('url: %s resolved to path: %s which is not a file. Assuming %s instead', req.path, filePath, newURL);
req.url = newURL;
}
diff --git a/api/files/lib_tasks_test-server.js.html b/api/files/lib_tasks_test-server.js.html
index 3322c7d..8817a87 100644
--- a/api/files/lib_tasks_test-server.js.html
+++ b/api/files/lib_tasks_test-server.js.html
@@ -42,7 +42,6 @@
'use strict';
-const shimAmd = require('./amd-shim');
-
class AmdTransformAddon {
/**
* This addon is used to register a custom AMD transform for app and addons to use.
@@ -97,6 +93,8 @@
return JSON.stringify(env || {});
}
-/**
- * Returns the <base> tag for index.html.
- *
- * @method calculateBaseTag
- * @param {String} baseURL
- * @param {String} locationType 'history', 'none' or 'hash'.
- * @return {String} Base tag or an empty string
- */
-function calculateBaseTag(baseURL, locationType) {
- let normalizedBaseUrl = cleanBaseURL(baseURL);
-
- if (locationType === 'hash') {
- return '';
- }
-
- return normalizedBaseUrl ? `<base href="${normalizedBaseUrl}" />` : '';
-}
-
/**
* Returns the content for a specific type (section) for index.html.
*
@@ -172,8 +152,6 @@
lib/utilities/ember-app-utils.js
switch (type) {
case 'head':
- content.push(calculateBaseTag(config.baseURL, config.locationType));
-
if (options.storeConfigInMeta) {
content.push(
`<meta name="${config.modulePrefix}/config/environment" content="${encodeURIComponent(
@@ -211,7 +189,13 @@
lib/utilities/ember-app-utils.js
break;
case 'test-body-footer':
content.push(
- `<script>Ember.assert('The tests file was not loaded. Make sure your tests index.html includes "assets/tests.js".', EmberENV.TESTS_FILE_LOADED);</script>`
+ `<script>
+document.addEventListener('DOMContentLoaded', function() {
+ if (!EmberENV.TESTS_FILE_LOADED) {
+ throw new Error('The tests file was not loaded. Make sure your tests index.html includes "assets/tests.js".');
+ }
+});
+</script>`
);
break;
@@ -271,7 +255,7 @@
+'use strict';
+
+/**
+ * Indicate if a given object is a constructor function or class or an instance of an Addon.
+ *
+ * @private
+ * @param {Object} addonCtorOrInstance the constructor function/class or an instance of an Addon.
+ * @return {Boolean} True if the addonCtorOrInstance is a lazy engine, False otherwise.
+ */
+module.exports = function isLazyEngine(addonCtorOrInstance) {
+ if (!addonCtorOrInstance) {
+ return false;
+ }
+
+ if (addonCtorOrInstance.lazyLoading) {
+ return addonCtorOrInstance.lazyLoading.enabled === true;
+ } else if (addonCtorOrInstance.options) {
+ return !!(addonCtorOrInstance.options.lazyLoading && addonCtorOrInstance.options.lazyLoading.enabled === true);
+ } else if (addonCtorOrInstance.prototype) {
+ if (addonCtorOrInstance.prototype.lazyLoading) {
+ return addonCtorOrInstance.prototype.lazyLoading.enabled === true;
+ } else if (addonCtorOrInstance.prototype.options) {
+ return !!(
+ addonCtorOrInstance.prototype.options.lazyLoading &&
+ addonCtorOrInstance.prototype.options.lazyLoading.enabled === true
+ );
+ }
+ }
+
+ return false;
+};
+
+