From 45bdb1016340beef5a4ef5dd824065258abd5a9b Mon Sep 17 00:00:00 2001 From: Chris Garrett Date: Tue, 16 Mar 2021 15:29:54 -0700 Subject: [PATCH] Disable the modules API polyfill on latest Ember Disables the modules API polyfill on the latest versions of Ember. --- lib/colocated-babel-plugin.js | 57 ++- lib/ember-addon-main.js | 15 +- lib/template-compiler-plugin.js | 49 ++- lib/utils.js | 21 +- node-tests/addon-test.js | 1 + node-tests/colocated-babel-plugin-test.js | 489 +++++++++++++++------- node-tests/colocated-test.js | 394 ++++++++++++++++- node-tests/template_compiler_test.js | 362 +++++++++++----- package.json | 2 +- yarn.lock | 9 + 10 files changed, 1109 insertions(+), 290 deletions(-) diff --git a/lib/colocated-babel-plugin.js b/lib/colocated-babel-plugin.js index 544c2cb0..51de9c1a 100644 --- a/lib/colocated-babel-plugin.js +++ b/lib/colocated-babel-plugin.js @@ -3,7 +3,11 @@ module.exports = function (babel) { let t = babel.types; - function makeSetComponentTemplateMemberExpression() { + function makeSetComponentTemplateExpression(state) { + if (!state.opts.requiresModuleApiPolyfill) { + return state._colocationEnsureImport('setComponentTemplate', '@ember/component'); + } + return t.memberExpression(t.identifier('Ember'), t.identifier('_setComponentTemplate')); } @@ -15,6 +19,53 @@ module.exports = function (babel) { name: 'ember-cli-htmlbars-colocation-template', visitor: { + Program(path, state) { + let allAddedImports = {}; + + state._colocationEnsureImport = (exportName, moduleName) => { + let addedImports = (allAddedImports[moduleName] = allAddedImports[moduleName] || {}); + + if (addedImports[exportName]) return addedImports[exportName]; + + let importDeclarations = path.get('body').filter((n) => n.type === 'ImportDeclaration'); + + let preexistingImportDeclaration = importDeclarations.find( + (n) => n.get('source').get('value').node === moduleName + ); + + if (preexistingImportDeclaration) { + let importSpecifier = preexistingImportDeclaration + .get('specifiers') + .find(({ node }) => { + return exportName === 'default' + ? t.isImportDefaultSpecifier(node) + : node.imported.name === exportName; + }); + + if (importSpecifier) { + addedImports[exportName] = importSpecifier.node.local; + } + } + + if (!addedImports[exportName]) { + let uid = path.scope.generateUidIdentifier( + exportName === 'default' ? moduleName : exportName + ); + addedImports[exportName] = uid; + + let newImportSpecifier = + exportName === 'default' + ? t.importDefaultSpecifier(uid) + : t.importSpecifier(uid, t.identifier(exportName)); + + let newImport = t.importDeclaration([newImportSpecifier], t.stringLiteral(moduleName)); + path.unshiftContainer('body', newImport); + } + + return addedImports[exportName]; + }; + }, + VariableDeclarator(path, state) { if (path.node.id.name === '__COLOCATED_TEMPLATE__') { state.colocatedTemplateFound = true; @@ -38,7 +89,7 @@ module.exports = function (babel) { state.setComponentTemplateInjected = true; let defaultExportDeclaration = path.node.declaration; - let setComponentTemplateMemberExpression = makeSetComponentTemplateMemberExpression(); + let setComponentTemplateMemberExpression = makeSetComponentTemplateExpression(state); let colocatedTemplateIdentifier = makeColocatedTemplateIdentifier(); if (defaultExportDeclaration.type === 'ClassDeclaration') { @@ -85,7 +136,7 @@ module.exports = function (babel) { state.setComponentTemplateInjected = true; path.parent.body.push( t.expressionStatement( - t.callExpression(makeSetComponentTemplateMemberExpression(), [ + t.callExpression(makeSetComponentTemplateExpression(state), [ makeColocatedTemplateIdentifier(), defaultSpecifier.local, ]) diff --git a/lib/ember-addon-main.js b/lib/ember-addon-main.js index 13bad063..ca7ca7ea 100644 --- a/lib/ember-addon-main.js +++ b/lib/ember-addon-main.js @@ -3,6 +3,7 @@ const path = require('path'); const SilentError = require('silent-error'); const utils = require('./utils'); +const VersionChecker = require('ember-cli-version-checker'); let registryInvocationCounter = 0; @@ -62,7 +63,7 @@ module.exports = { transpileTree(inputTree, htmlbarsOptions) { const TemplateCompiler = require('./template-compiler-plugin'); - return new TemplateCompiler(inputTree, htmlbarsOptions); + return new TemplateCompiler(inputTree, htmlbarsOptions, this._requiresModuleApiPolyfill); }, setupPreprocessorRegistry(type, registry) { @@ -169,6 +170,9 @@ module.exports = { let isProduction = process.env.EMBER_ENV === 'production'; + let checker = new VersionChecker(this.parent).for('ember-source', 'npm'); + this._requiresModuleApiPolyfill = checker.exists() && checker.lt('3.27.0-alpha.1'); + // This is an option intended to be used only be `ember-template-imports`. // DO NOT USE THIS let customModules = @@ -190,7 +194,8 @@ module.exports = { this.projectConfig(), templateCompilerPath, isProduction, - customModules + customModules, + this._requiresModuleApiPolyfill ); babelPlugins.push(htmlbarsInlinePrecompilePlugin); @@ -205,6 +210,7 @@ module.exports = { projectConfig: this.projectConfig(), templateCompilerPath, modules: customModules, + requiresModuleApiPolyfill: this._requiresModuleApiPolyfill, }); babelPlugins.push(htmlBarsPlugin); @@ -212,7 +218,10 @@ module.exports = { } if (this._shouldColocateTemplates() && !utils.isColocatedBabelPluginRegistered(babelPlugins)) { - babelPlugins.push(require.resolve('./colocated-babel-plugin')); + babelPlugins.push([ + require.resolve('./colocated-babel-plugin'), + { requiresModuleApiPolyfill: this._requiresModuleApiPolyfill }, + ]); } }, diff --git a/lib/template-compiler-plugin.js b/lib/template-compiler-plugin.js index 2847706d..74bf8578 100644 --- a/lib/template-compiler-plugin.js +++ b/lib/template-compiler-plugin.js @@ -24,7 +24,7 @@ function rethrowBuildError(error) { } class TemplateCompiler extends Filter { - constructor(inputTree, _options) { + constructor(inputTree, _options, requiresModuleApiPolyfill) { let options = _options || {}; if (!('persist' in options)) { @@ -35,6 +35,7 @@ class TemplateCompiler extends Filter { this.options = options; this.inputTree = inputTree; + this.requiresModuleApiPolyfill = requiresModuleApiPolyfill; // TODO: do we need this? this.precompile = this.options.templateCompiler.precompile; @@ -51,6 +52,7 @@ class TemplateCompiler extends Filter { processString(string, relativePath) { let srcDir = this.inputPaths[0]; let srcName = path.join(srcDir, relativePath); + try { // we have to reverse these for reasons that are a bit bonkers. the initial // version of this system used `registeredPlugin` from @@ -63,25 +65,23 @@ class TemplateCompiler extends Filter { // sooooooo...... we are forced to maintain that **absolutely bonkers** ordering let astPlugins = this.options.plugins ? [...this.options.plugins.ast].reverse() : []; - let result = - 'export default ' + - utils.template(this.options.templateCompiler, stripBom(string), { - contents: string, - isProduction: this.options.isProduction, - moduleName: relativePath, - parseOptions: { - srcName: srcName, - }, - - // intentionally not using `plugins: this.options.plugins` here - // because if we do, Ember will mutate the shared plugins object (adding - // all of the built in AST transforms into plugins.ast, which breaks - // persistent caching) - plugins: { - ast: astPlugins, - }, - }) + - ';'; + let precompiled = this.options.templateCompiler.precompile(stripBom(string), { + contents: string, + isProduction: this.options.isProduction, + moduleName: relativePath, + parseOptions: { + srcName: srcName, + }, + + // intentionally not using `plugins: this.options.plugins` here + // because if we do, Ember will mutate the shared plugins object (adding + // all of the built in AST transforms into plugins.ast, which breaks + // persistent caching) + plugins: { + ast: astPlugins, + }, + }); + if (this.options.dependencyInvalidation) { let plugins = pluginsWithDependencies(this.options.plugins.ast); let dependencies = []; @@ -91,7 +91,12 @@ class TemplateCompiler extends Filter { } this.dependencies.setDependencies(relativePath, dependencies); } - return result; + + if (this.requiresModuleApiPolyfill) { + return `export default Ember.HTMLBars.template(${precompiled});`; + } else { + return `import { createTemplateFactory } from '@ember/template-factory';\n\nexport default createTemplateFactory(${precompiled});`; + } } catch (error) { rethrowBuildError(error); } @@ -106,6 +111,8 @@ class TemplateCompiler extends Filter { } } + strippedOptions._requiresModuleApiPolyfill = this.requiresModuleApiPolyfill; + return strippedOptions; } diff --git a/lib/utils.js b/lib/utils.js index dc1bcf73..7fd0e01a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -50,9 +50,11 @@ function isInlinePrecompileBabelPluginRegistered(plugins) { } function isColocatedBabelPluginRegistered(plugins) { - return plugins.some( - (plugin) => typeof plugin === 'string' && plugin === require.resolve('./colocated-babel-plugin') - ); + return plugins.some((plugin) => { + let path = Array.isArray(plugin) ? plugin[0] : plugin; + + return typeof path === 'string' && path === require.resolve('./colocated-babel-plugin'); + }); } function buildParalleizedBabelPlugin( @@ -60,7 +62,8 @@ function buildParalleizedBabelPlugin( projectConfig, templateCompilerPath, isProduction, - customModules + customModules, + requiresModuleApiPolyfill ) { let parallelBabelInfo = { requireFile: require.resolve('./require-from-worker'), @@ -71,6 +74,7 @@ function buildParalleizedBabelPlugin( projectConfig, parallelConfigs: pluginInfo.parallelConfigs, modules: Object.assign({}, customModules, INLINE_PRECOMPILE_MODULES), + requiresModuleApiPolyfill, }, }; @@ -199,12 +203,6 @@ function initializeEmberENV(templateCompiler, EmberENV) { } } -function template(templateCompiler, string, options) { - let precompiled = templateCompiler.precompile(string, options); - - return 'Ember.HTMLBars.template(' + precompiled + ')'; -} - function setup(pluginInfo, options) { let projectConfig = options.projectConfig || {}; let templateCompilerPath = options.templateCompilerPath; @@ -261,7 +259,7 @@ function setup(pluginInfo, options) { { precompile, isProduction: options.isProduction, - ensureModuleApiPolyfill: true, + ensureModuleApiPolyfill: options.requiresModuleApiPolyfill, modules: Object.assign({}, options.modules, INLINE_PRECOMPILE_MODULES), }, 'ember-cli-htmlbars:inline-precompile', @@ -355,7 +353,6 @@ function setupPlugins(wrappers) { module.exports = { buildOptions, initializeEmberENV, - template, setup, makeCacheKey, setupPlugins, diff --git a/node-tests/addon-test.js b/node-tests/addon-test.js index 3b81566a..de4b9a4e 100644 --- a/node-tests/addon-test.js +++ b/node-tests/addon-test.js @@ -34,6 +34,7 @@ describe('ember-cli-htmlbars addon', function () { project, parent: project, ui: this.ui, + _requiresModuleApiPolyfill: true, }); project.addons.push(this.addon); diff --git a/node-tests/colocated-babel-plugin-test.js b/node-tests/colocated-babel-plugin-test.js index 01b8ce78..764cf846 100644 --- a/node-tests/colocated-babel-plugin-test.js +++ b/node-tests/colocated-babel-plugin-test.js @@ -22,161 +22,350 @@ const RuntimePlugin = [ describe('ColocatedBabelPlugin', function () { this.slow(500); - it('can be used with decorators', function () { - let { code } = babel.transformSync( - stripIndent` - import Component from '@glimmer/component'; - const __COLOCATED_TEMPLATE__ = 'ok'; - - export default class MyComponent extends Component { - @tracked data = null; - }; - `, - { plugins: [RuntimePlugin, ColocatedBabelPlugin, DecoratorsPlugin, ClassPropertiesPlugin] } - ); - - assert.strictEqual( - code, - stripIndent` - import _initializerDefineProperty from "@babel/runtime/helpers/esm/initializerDefineProperty"; - import _applyDecoratedDescriptor from "@babel/runtime/helpers/esm/applyDecoratedDescriptor"; - import _initializerWarningHelper from "@babel/runtime/helpers/esm/initializerWarningHelper"; - - var _class, _descriptor, _temp; - - import Component from '@glimmer/component'; - const __COLOCATED_TEMPLATE__ = 'ok'; - let MyComponent = (_class = (_temp = class MyComponent extends Component { - constructor(...args) { - super(...args); - - _initializerDefineProperty(this, "data", _descriptor, this); - } - - }, _temp), (_descriptor = _applyDecoratedDescriptor(_class.prototype, "data", [tracked], { - configurable: true, - enumerable: true, - writable: true, - initializer: function () { - return null; - } - })), _class); - export { MyComponent as default }; - ; - - Ember._setComponentTemplate(__COLOCATED_TEMPLATE__, MyComponent); - ` - ); - }); + describe('requiresModuleApiPolyfill: true', function () { + const ColocatedBabelPluginOptions = [ColocatedBabelPlugin, { requiresModuleApiPolyfill: true }]; - it('can be used with TypeScript merged declarations', function () { - let { code } = babel.transformSync( - stripIndent` - import Component from 'somewhere'; - const __COLOCATED_TEMPLATE__ = 'ok'; - type MyArgs = { required: string; optional?: number }; - - export default interface MyComponent extends MyArgs {} - export default class MyComponent extends Component {} - `, - { plugins: [ColocatedBabelPlugin, TypeScriptPlugin] } - ); - - assert.strictEqual( - code, - stripIndent` - import Component from 'somewhere'; - const __COLOCATED_TEMPLATE__ = 'ok'; - export default class MyComponent extends Component {} - - Ember._setComponentTemplate(__COLOCATED_TEMPLATE__, MyComponent); - ` - ); - }); + it('can be used with decorators', function () { + let { code } = babel.transformSync( + stripIndent` + import Component from '@glimmer/component'; + const __COLOCATED_TEMPLATE__ = 'ok'; - it('sets the template for non-class default exports', function () { - let { code } = babel.transformSync( - stripIndent` - import MyComponent from 'other-module'; - const __COLOCATED_TEMPLATE__ = 'ok'; - export default MyComponent; - `, - { plugins: [ColocatedBabelPlugin] } - ); - - assert.strictEqual( - code, - stripIndent` - import MyComponent from 'other-module'; - const __COLOCATED_TEMPLATE__ = 'ok'; - export default Ember._setComponentTemplate(__COLOCATED_TEMPLATE__, MyComponent); - ` - ); - }); + export default class MyComponent extends Component { + @tracked data = null; + }; + `, + { + plugins: [ + RuntimePlugin, + ColocatedBabelPluginOptions, + DecoratorsPlugin, + ClassPropertiesPlugin, + ], + } + ); - it('sets the template for named class default exports', function () { - let { code } = babel.transformSync( - stripIndent` - import Component from 'somewhere'; - const __COLOCATED_TEMPLATE__ = 'ok'; - export default class MyComponent extends Component {} - `, - { plugins: [ColocatedBabelPlugin] } - ); - - assert.strictEqual( - code, - stripIndent` - import Component from 'somewhere'; - const __COLOCATED_TEMPLATE__ = 'ok'; - export default class MyComponent extends Component {} - - Ember._setComponentTemplate(__COLOCATED_TEMPLATE__, MyComponent); - ` - ); - }); + assert.strictEqual( + code, + stripIndent` + import _initializerDefineProperty from "@babel/runtime/helpers/esm/initializerDefineProperty"; + import _applyDecoratedDescriptor from "@babel/runtime/helpers/esm/applyDecoratedDescriptor"; + import _initializerWarningHelper from "@babel/runtime/helpers/esm/initializerWarningHelper"; + + var _class, _descriptor, _temp; + + import Component from '@glimmer/component'; + const __COLOCATED_TEMPLATE__ = 'ok'; + let MyComponent = (_class = (_temp = class MyComponent extends Component { + constructor(...args) { + super(...args); + + _initializerDefineProperty(this, "data", _descriptor, this); + } + + }, _temp), (_descriptor = _applyDecoratedDescriptor(_class.prototype, "data", [tracked], { + configurable: true, + enumerable: true, + writable: true, + initializer: function () { + return null; + } + })), _class); + export { MyComponent as default }; + ; + + Ember._setComponentTemplate(__COLOCATED_TEMPLATE__, MyComponent); + ` + ); + }); + + it('can be used with TypeScript merged declarations', function () { + let { code } = babel.transformSync( + stripIndent` + import Component from 'somewhere'; + const __COLOCATED_TEMPLATE__ = 'ok'; + type MyArgs = { required: string; optional?: number }; + + export default interface MyComponent extends MyArgs {} + export default class MyComponent extends Component {} + `, + { plugins: [ColocatedBabelPluginOptions, TypeScriptPlugin] } + ); + + assert.strictEqual( + code, + stripIndent` + import Component from 'somewhere'; + const __COLOCATED_TEMPLATE__ = 'ok'; + export default class MyComponent extends Component {} + + Ember._setComponentTemplate(__COLOCATED_TEMPLATE__, MyComponent); + ` + ); + }); + + it('sets the template for non-class default exports', function () { + let { code } = babel.transformSync( + stripIndent` + import MyComponent from 'other-module'; + const __COLOCATED_TEMPLATE__ = 'ok'; + export default MyComponent; + `, + { plugins: [ColocatedBabelPluginOptions] } + ); + + assert.strictEqual( + code, + stripIndent` + import MyComponent from 'other-module'; + const __COLOCATED_TEMPLATE__ = 'ok'; + export default Ember._setComponentTemplate(__COLOCATED_TEMPLATE__, MyComponent); + ` + ); + }); + + it('sets the template for named class default exports', function () { + let { code } = babel.transformSync( + stripIndent` + import Component from 'somewhere'; + const __COLOCATED_TEMPLATE__ = 'ok'; + export default class MyComponent extends Component {} + `, + { plugins: [ColocatedBabelPluginOptions] } + ); + + assert.strictEqual( + code, + stripIndent` + import Component from 'somewhere'; + const __COLOCATED_TEMPLATE__ = 'ok'; + export default class MyComponent extends Component {} + + Ember._setComponentTemplate(__COLOCATED_TEMPLATE__, MyComponent); + ` + ); + }); + + it('sets the template for anonymous class default exports', function () { + let { code } = babel.transformSync( + stripIndent` + import Component from 'somewhere'; + const __COLOCATED_TEMPLATE__ = 'ok'; + export default class extends Component {} + `, + { plugins: [ColocatedBabelPluginOptions] } + ); + + assert.strictEqual( + code, + stripIndent` + import Component from 'somewhere'; + const __COLOCATED_TEMPLATE__ = 'ok'; + export default Ember._setComponentTemplate(__COLOCATED_TEMPLATE__, class extends Component {}); + ` + ); + }); + + it('sets the template for identifier `as default` exports', function () { + let { code } = babel.transformSync( + stripIndent` + import Component from 'somewhere'; + const __COLOCATED_TEMPLATE__ = 'ok'; + const MyComponent = class extends Component {}; + export { MyComponent as default }; + `, + { plugins: [ColocatedBabelPluginOptions] } + ); + + assert.strictEqual( + code, + stripIndent` + import Component from 'somewhere'; + const __COLOCATED_TEMPLATE__ = 'ok'; + const MyComponent = class extends Component {}; + export { MyComponent as default }; - it('sets the template for anonymous class default exports', function () { - let { code } = babel.transformSync( - stripIndent` - import Component from 'somewhere'; - const __COLOCATED_TEMPLATE__ = 'ok'; - export default class extends Component {} - `, - { plugins: [ColocatedBabelPlugin] } - ); - - assert.strictEqual( - code, - stripIndent` - import Component from 'somewhere'; - const __COLOCATED_TEMPLATE__ = 'ok'; - export default Ember._setComponentTemplate(__COLOCATED_TEMPLATE__, class extends Component {}); - ` - ); + Ember._setComponentTemplate(__COLOCATED_TEMPLATE__, MyComponent); + ` + ); + }); }); - it('sets the template for identifier `as default` exports', function () { - let { code } = babel.transformSync( - stripIndent` - import Component from 'somewhere'; - const __COLOCATED_TEMPLATE__ = 'ok'; - const MyComponent = class extends Component {}; - export { MyComponent as default }; - `, - { plugins: [ColocatedBabelPlugin] } - ); - - assert.strictEqual( - code, - stripIndent` - import Component from 'somewhere'; - const __COLOCATED_TEMPLATE__ = 'ok'; - const MyComponent = class extends Component {}; - export { MyComponent as default }; - - Ember._setComponentTemplate(__COLOCATED_TEMPLATE__, MyComponent); - ` - ); + describe('requiresModuleApiPolyfill: false', function () { + const ColocatedBabelPluginOptions = [ + ColocatedBabelPlugin, + { requiresModuleApiPolyfill: false }, + ]; + + it('can be used with decorators', function () { + let { code } = babel.transformSync( + stripIndent` + import Component from '@glimmer/component'; + const __COLOCATED_TEMPLATE__ = 'ok'; + + export default class MyComponent extends Component { + @tracked data = null; + }; + `, + { + plugins: [ + RuntimePlugin, + ColocatedBabelPluginOptions, + DecoratorsPlugin, + ClassPropertiesPlugin, + ], + } + ); + + assert.strictEqual( + code, + stripIndent` + import _initializerDefineProperty from "@babel/runtime/helpers/esm/initializerDefineProperty"; + import _applyDecoratedDescriptor from "@babel/runtime/helpers/esm/applyDecoratedDescriptor"; + import _initializerWarningHelper from "@babel/runtime/helpers/esm/initializerWarningHelper"; + + var _class, _descriptor, _temp; + + import { setComponentTemplate as _setComponentTemplate } from "@ember/component"; + import Component from '@glimmer/component'; + const __COLOCATED_TEMPLATE__ = 'ok'; + let MyComponent = (_class = (_temp = class MyComponent extends Component { + constructor(...args) { + super(...args); + + _initializerDefineProperty(this, "data", _descriptor, this); + } + + }, _temp), (_descriptor = _applyDecoratedDescriptor(_class.prototype, "data", [tracked], { + configurable: true, + enumerable: true, + writable: true, + initializer: function () { + return null; + } + })), _class); + export { MyComponent as default }; + ; + + _setComponentTemplate(__COLOCATED_TEMPLATE__, MyComponent); + ` + ); + }); + + it('can be used with TypeScript merged declarations', function () { + let { code } = babel.transformSync( + stripIndent` + import Component from 'somewhere'; + const __COLOCATED_TEMPLATE__ = 'ok'; + type MyArgs = { required: string; optional?: number }; + + export default interface MyComponent extends MyArgs {} + export default class MyComponent extends Component {} + `, + { plugins: [ColocatedBabelPluginOptions, TypeScriptPlugin] } + ); + + assert.strictEqual( + code, + stripIndent` + import { setComponentTemplate as _setComponentTemplate } from "@ember/component"; + import Component from 'somewhere'; + const __COLOCATED_TEMPLATE__ = 'ok'; + export default class MyComponent extends Component {} + + _setComponentTemplate(__COLOCATED_TEMPLATE__, MyComponent); + ` + ); + }); + + it('sets the template for non-class default exports', function () { + let { code } = babel.transformSync( + stripIndent` + import MyComponent from 'other-module'; + const __COLOCATED_TEMPLATE__ = 'ok'; + export default MyComponent; + `, + { plugins: [ColocatedBabelPluginOptions] } + ); + + assert.strictEqual( + code, + stripIndent` + import { setComponentTemplate as _setComponentTemplate } from "@ember/component"; + import MyComponent from 'other-module'; + const __COLOCATED_TEMPLATE__ = 'ok'; + export default _setComponentTemplate(__COLOCATED_TEMPLATE__, MyComponent); + ` + ); + }); + + it('sets the template for named class default exports', function () { + let { code } = babel.transformSync( + stripIndent` + import Component from 'somewhere'; + const __COLOCATED_TEMPLATE__ = 'ok'; + export default class MyComponent extends Component {} + `, + { plugins: [ColocatedBabelPluginOptions] } + ); + + assert.strictEqual( + code, + stripIndent` + import { setComponentTemplate as _setComponentTemplate } from "@ember/component"; + import Component from 'somewhere'; + const __COLOCATED_TEMPLATE__ = 'ok'; + export default class MyComponent extends Component {} + + _setComponentTemplate(__COLOCATED_TEMPLATE__, MyComponent); + ` + ); + }); + + it('sets the template for anonymous class default exports', function () { + let { code } = babel.transformSync( + stripIndent` + import Component from 'somewhere'; + const __COLOCATED_TEMPLATE__ = 'ok'; + export default class extends Component {} + `, + { plugins: [ColocatedBabelPluginOptions] } + ); + + assert.strictEqual( + code, + stripIndent` + import { setComponentTemplate as _setComponentTemplate } from "@ember/component"; + import Component from 'somewhere'; + const __COLOCATED_TEMPLATE__ = 'ok'; + export default _setComponentTemplate(__COLOCATED_TEMPLATE__, class extends Component {}); + ` + ); + }); + + it('sets the template for identifier `as default` exports', function () { + let { code } = babel.transformSync( + stripIndent` + import Component from 'somewhere'; + const __COLOCATED_TEMPLATE__ = 'ok'; + const MyComponent = class extends Component {}; + export { MyComponent as default }; + `, + { plugins: [ColocatedBabelPluginOptions] } + ); + + assert.strictEqual( + code, + stripIndent` + import { setComponentTemplate as _setComponentTemplate } from "@ember/component"; + import Component from 'somewhere'; + const __COLOCATED_TEMPLATE__ = 'ok'; + const MyComponent = class extends Component {}; + export { MyComponent as default }; + + _setComponentTemplate(__COLOCATED_TEMPLATE__, MyComponent); + ` + ); + }); }); }); diff --git a/node-tests/colocated-test.js b/node-tests/colocated-test.js index 18949b3b..bdc4b4e8 100644 --- a/node-tests/colocated-test.js +++ b/node-tests/colocated-test.js @@ -32,7 +32,7 @@ class BabelTranspiler extends BroccoliPersistentFilter { } } -describe('Colocation - Broccoli + Babel Integration', function () { +describe('Colocation - Broccoli + Babel Integration (modules API: true)', function () { this.timeout(10000); let input, output; @@ -57,7 +57,7 @@ describe('Colocation - Broccoli + Babel Integration', function () { }); let babelTree = new BabelTranspiler(colocatedTree, { - plugins: [...plugins, ColocatedBabelPlugin], + plugins: [...plugins, [ColocatedBabelPlugin, { requiresModuleApiPolyfill: true }]], }); output = _createBuilder(babelTree); @@ -416,3 +416,393 @@ describe('Colocation - Broccoli + Babel Integration', function () { it('does not break class decorator usage'); }); + +describe('Colocation - Broccoli + Babel Integration (modules API: false)', function () { + this.timeout(10000); + + let input, output; + + beforeEach(async function () { + input = await createTempDir(); + }); + + afterEach(async function () { + await input.dispose(); + + if (output) { + await output.dispose(); + } + }); + + function createBuilder(plugins = []) { + let colocatedTree = new ColocatedTemplateCompiler(input.path(), { + precompile(template) { + return JSON.stringify({ template }); + }, + }); + + let babelTree = new BabelTranspiler(colocatedTree, { + plugins: [...plugins, [ColocatedBabelPlugin, { requiresModuleApiPolyfill: false }]], + }); + + output = _createBuilder(babelTree); + + return output; + } + + it('works for template only component', async function () { + input.write({ + 'app-name-here': { + components: { + 'foo.hbs': `{{yield}}`, + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }); + + createBuilder(); + + await output.build(); + + assert.deepStrictEqual(output.read(), { + 'app-name-here': { + components: { + 'foo.js': stripIndent` + import { setComponentTemplate as _setComponentTemplate } from "@ember/component"; + import { hbs } from 'ember-cli-htmlbars'; + + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", { + "contents": "{{yield}}", + "moduleName": "app-name-here/components/foo.hbs", + "parseOptions": { + "srcName": "app-name-here/components/foo.hbs" + } + }); + + import templateOnly from '@ember/component/template-only'; + export default _setComponentTemplate(__COLOCATED_TEMPLATE__, templateOnly()); + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }); + }); + + it('works for component with template and class', async function () { + input.write({ + 'app-name-here': { + components: { + 'foo.hbs': `{{yield}}`, + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }); + + createBuilder(); + + await output.build(); + + assert.deepStrictEqual(output.read(), { + 'app-name-here': { + components: { + 'foo.js': stripIndent` + import { setComponentTemplate as _setComponentTemplate } from "@ember/component"; + import { hbs } from 'ember-cli-htmlbars'; + + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", { + "contents": "{{yield}}", + "moduleName": "app-name-here/components/foo.hbs", + "parseOptions": { + "srcName": "app-name-here/components/foo.hbs" + } + }); + + import Component from '@glimmer/component'; + export default class FooComponent extends Component {} + + _setComponentTemplate(__COLOCATED_TEMPLATE__, FooComponent); + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }); + }); + + it('works for typescript component class with template', async function () { + input.write({ + 'app-name-here': { + components: { + 'foo.hbs': `{{yield}}`, + 'foo.ts': stripIndent` + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }); + + createBuilder([TypescriptTransform]); + + await output.build(); + + assert.deepStrictEqual(output.read(), { + 'app-name-here': { + components: { + 'foo.js': stripIndent` + import { setComponentTemplate as _setComponentTemplate } from "@ember/component"; + import { hbs } from 'ember-cli-htmlbars'; + + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", { + "contents": "{{yield}}", + "moduleName": "app-name-here/components/foo.hbs", + "parseOptions": { + "srcName": "app-name-here/components/foo.hbs" + } + }); + + import Component from '@glimmer/component'; + export default class FooComponent extends Component {} + + _setComponentTemplate(__COLOCATED_TEMPLATE__, FooComponent); + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }); + }); + + it('works for scoped addon using template only component', async function () { + input.write({ + '@scope-name': { + 'addon-name-here': { + components: { + 'foo.hbs': `{{yield}}`, + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }, + }); + + createBuilder(); + + await output.build(); + + assert.deepStrictEqual(output.read(), { + '@scope-name': { + 'addon-name-here': { + components: { + 'foo.js': stripIndent` + import { setComponentTemplate as _setComponentTemplate } from "@ember/component"; + import { hbs } from 'ember-cli-htmlbars'; + + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", { + "contents": "{{yield}}", + "moduleName": "@scope-name/addon-name-here/components/foo.hbs", + "parseOptions": { + "srcName": "@scope-name/addon-name-here/components/foo.hbs" + } + }); + + import templateOnly from '@ember/component/template-only'; + export default _setComponentTemplate(__COLOCATED_TEMPLATE__, templateOnly()); + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + }); + }); + + it('works for scoped addon using component with template and class', async function () { + input.write({ + '@scope-name': { + 'addon-name-here': { + components: { + 'foo.hbs': `{{yield}}`, + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }, + }); + + createBuilder(); + + await output.build(); + + assert.deepStrictEqual(output.read(), { + '@scope-name': { + 'addon-name-here': { + components: { + 'foo.js': stripIndent` + import { setComponentTemplate as _setComponentTemplate } from "@ember/component"; + import { hbs } from 'ember-cli-htmlbars'; + + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", { + "contents": "{{yield}}", + "moduleName": "@scope-name/addon-name-here/components/foo.hbs", + "parseOptions": { + "srcName": "@scope-name/addon-name-here/components/foo.hbs" + } + }); + + import Component from '@glimmer/component'; + export default class FooComponent extends Component {} + + _setComponentTemplate(__COLOCATED_TEMPLATE__, FooComponent); + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + }); + }); + + it('does nothing for "classic" location components', async function () { + input.write({ + 'app-name-here': { + components: { + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': `{{outlet}}`, + components: { + 'foo.hbs': `{{yield}}`, + }, + }, + }, + }); + + createBuilder(); + + await output.build(); + + assert.deepStrictEqual(output.read(), input.read()); + }); + + it('does nothing for "pod" location templates', async function () { + input.write({ + 'addon-name-here': { + components: { + foo: { + 'template.hbs': `{{yield}}`, + }, + }, + }, + }); + + createBuilder(); + await output.build(); + + assert.deepStrictEqual(output.read(), input.read()); + }); + + it('it works if there are no input files', async function () { + input.write({}); + + createBuilder(); + await output.build(); + + assert.deepStrictEqual(output.read(), {}); + }); + + it('it works if input is manually using setComponentTemplate - no colocated template exists', async function () { + input.write({ + 'app-name-here': { + components: { + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + import { setComponentTemplate } from '@ember/component'; + import hbs from 'ember-cli-htmlbars-inline-precompile'; + export default class FooComponent extends Component {} + setComponentTemplate(FooComponent, hbs\`sometemplate\`); + `, + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }); + + createBuilder(); + await output.build(); + + assert.deepStrictEqual(output.read(), { + 'app-name-here': { + components: { + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + import { setComponentTemplate } from '@ember/component'; + import hbs from 'ember-cli-htmlbars-inline-precompile'; + export default class FooComponent extends Component {} + setComponentTemplate(FooComponent, hbs\`sometemplate\`); + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }); + }); + + it('emits an error when a default export is not present in a component JS file', async function () { + input.write({ + 'app-name-here': { + components: { + 'foo.hbs': `{{yield}}`, + 'foo.js': stripIndent` + export function whatever() {} + `, + }, + }, + }); + + createBuilder(); + await output.build(); + + assert.deepStrictEqual(output.read(), { + 'app-name-here': { + components: { + 'foo.js': stripIndent` + export function whatever() {}\nthrow new Error("\`app-name-here/components/foo.hbs\` does not contain a \`default export\`. Did you forget to export the component class?"); + `, + }, + }, + }); + }); + + it('does not break class decorator usage'); +}); diff --git a/node-tests/template_compiler_test.js b/node-tests/template_compiler_test.js index b41741fa..76bd4601 100644 --- a/node-tests/template_compiler_test.js +++ b/node-tests/template_compiler_test.js @@ -5,6 +5,7 @@ const TemplateCompiler = require('../lib/template-compiler-plugin'); const co = require('co'); const { createTempDir, createBuilder } = require('broccoli-test-helper'); const fixturify = require('fixturify'); +const { stripIndent } = require('common-tags'); describe('TemplateCompiler', function () { this.timeout(10000); @@ -47,144 +48,309 @@ describe('TemplateCompiler', function () { htmlbarsPrecompile = htmlbarsOptions.templateCompiler.precompile; }); - it( - 'precompiles templates into htmlbars', - co.wrap(function* () { - let tree = new TemplateCompiler(input.path(), htmlbarsOptions); + describe('requiresModuleApiPolyfill: true', function () { + it( + 'precompiles templates into htmlbars', + co.wrap(function* () { + let tree = new TemplateCompiler(input.path(), htmlbarsOptions, true); + + output = createBuilder(tree); + yield output.build(); + + let source = input.readText('template.hbs'); + let expected = `export default Ember.HTMLBars.template(${htmlbarsPrecompile(source, { + moduleName: 'template.hbs', + })});`; + assert.strictEqual(output.readText('template.js'), expected); + }) + ); + + it('invokes AST plugins', async function () { + let source = '{{foo-bar}}'; + input.write({ + 'template.hbs': source, + }); + let plugin = (env) => { + return { + name: 'fake-ast-plugin', + + visitor: { + MustacheStatement() { + return env.syntax.builders.text('Huzzah!'); + }, + }, + }; + }; + + htmlbarsOptions.plugins.ast.push(plugin); + + let tree = new TemplateCompiler(input.path(), htmlbarsOptions, true); output = createBuilder(tree); - yield output.build(); + await output.build(); - let source = input.readText('template.hbs'); let expected = `export default Ember.HTMLBars.template(${htmlbarsPrecompile(source, { moduleName: 'template.hbs', + plugins: { + ast: [plugin], + }, })});`; - assert.strictEqual(output.readText('template.js'), expected); - }) - ); - it('invokes AST plugins', async function () { - let source = '{{foo-bar}}'; - input.write({ - 'template.hbs': source, + let outputString = output.readText('template.js'); + assert.strictEqual(outputString, expected); + assert.ok(outputString.includes('Huzzah!')); }); - let plugin = (env) => { - return { - name: 'fake-ast-plugin', - visitor: { - MustacheStatement() { - return env.syntax.builders.text('Huzzah!'); - }, - }, - }; - }; + it('AST Plugins have access to `isProduction` status', async function () { + let source = '{{foo-bar}}'; + input.write({ + 'template.hbs': source, + }); - htmlbarsOptions.plugins.ast.push(plugin); + let wasProduction = false; + let plugin = (env) => { + wasProduction = env.isProduction; - let tree = new TemplateCompiler(input.path(), htmlbarsOptions); + return { + name: 'fake-ast-plugin', - output = createBuilder(tree); - await output.build(); + visitor: {}, + }; + }; - let expected = `export default Ember.HTMLBars.template(${htmlbarsPrecompile(source, { - moduleName: 'template.hbs', - plugins: { - ast: [plugin], - }, - })});`; + htmlbarsOptions.isProduction = true; + htmlbarsOptions.plugins.ast.push(plugin); - let outputString = output.readText('template.js'); - assert.strictEqual(outputString, expected); - assert.ok(outputString.includes('Huzzah!')); - }); + let tree = new TemplateCompiler(input.path(), htmlbarsOptions, true); + + output = createBuilder(tree); + await output.build(); - it('AST Plugins have access to `isProduction` status', async function () { - let source = '{{foo-bar}}'; - input.write({ - 'template.hbs': source, + assert.ok(wasProduction); }); - let wasProduction = false; - let plugin = (env) => { - wasProduction = env.isProduction; + it( + 'ignores utf-8 byte order marks', + co.wrap(function* () { + let tree = new TemplateCompiler(input.path(), htmlbarsOptions, true); - return { - name: 'fake-ast-plugin', + output = createBuilder(tree); + yield output.build(); - visitor: {}, - }; - }; + let source = input.readText('template.hbs'); + let expected = `export default Ember.HTMLBars.template(${htmlbarsPrecompile(source, { + moduleName: 'template-with-bom.hbs', + })});`; + + assert.strictEqual(output.readText('template-with-bom.js'), expected); + }) + ); + + it( + 'passes FEATURES to compiler when provided as `FEATURES` [DEPRECATED]', + co.wrap(function* () { + htmlbarsOptions.FEATURES = { + 'ember-htmlbars-component-generation': true, + }; + + let tree = new TemplateCompiler(input.path(), htmlbarsOptions, true); + + output = createBuilder(tree); + yield output.build(); + + let source = input.readText('web-component-template.hbs'); + let expected = `export default Ember.HTMLBars.template(${htmlbarsPrecompile(source, { + moduleName: 'web-component-template.hbs', + })});`; + + assert.strictEqual(output.readText('web-component-template.js'), expected); + }) + ); - htmlbarsOptions.isProduction = true; - htmlbarsOptions.plugins.ast.push(plugin); + it( + 'passes FEATURES to compiler when provided as `EmberENV.FEATURES`', + co.wrap(function* () { + htmlbarsOptions.EmberENV = { + FEATURES: { + 'ember-htmlbars-component-generation': true, + }, + }; + + let tree = new TemplateCompiler(input.path(), htmlbarsOptions, true); - let tree = new TemplateCompiler(input.path(), htmlbarsOptions); + output = createBuilder(tree); + yield output.build(); - output = createBuilder(tree); - await output.build(); + let source = input.readText('web-component-template.hbs'); + let expected = `export default Ember.HTMLBars.template(${htmlbarsPrecompile(source, { + moduleName: 'web-component-template.hbs', + })});`; - assert.ok(wasProduction); + assert.strictEqual(output.readText('web-component-template.js'), expected); + }) + ); }); - it( - 'ignores utf-8 byte order marks', - co.wrap(function* () { - let tree = new TemplateCompiler(input.path(), htmlbarsOptions); + describe('requiresModuleApiPolyfill: false', function () { + it( + 'precompiles templates into htmlbars', + co.wrap(function* () { + let tree = new TemplateCompiler(input.path(), htmlbarsOptions, false); + + output = createBuilder(tree); + yield output.build(); + + let source = input.readText('template.hbs'); + let expected = stripIndent` + import { createTemplateFactory } from '@ember/template-factory'; + + export default createTemplateFactory(${htmlbarsPrecompile(source, { + moduleName: 'template.hbs', + })}); + `; + assert.strictEqual(output.readText('template.js'), expected); + }) + ); + + it('invokes AST plugins', async function () { + let source = '{{foo-bar}}'; + input.write({ + 'template.hbs': source, + }); + let plugin = (env) => { + return { + name: 'fake-ast-plugin', + + visitor: { + MustacheStatement() { + return env.syntax.builders.text('Huzzah!'); + }, + }, + }; + }; + + htmlbarsOptions.plugins.ast.push(plugin); + + let tree = new TemplateCompiler(input.path(), htmlbarsOptions, false); output = createBuilder(tree); - yield output.build(); + await output.build(); - let source = input.readText('template.hbs'); - let expected = `export default Ember.HTMLBars.template(${htmlbarsPrecompile(source, { - moduleName: 'template-with-bom.hbs', - })});`; + let expected = stripIndent` + import { createTemplateFactory } from '@ember/template-factory'; - assert.strictEqual(output.readText('template-with-bom.js'), expected); - }) - ); + export default createTemplateFactory(${htmlbarsPrecompile(source, { + moduleName: 'template.hbs', + plugins: { + ast: [plugin], + }, + })}); + `; - it( - 'passes FEATURES to compiler when provided as `FEATURES` [DEPRECATED]', - co.wrap(function* () { - htmlbarsOptions.FEATURES = { - 'ember-htmlbars-component-generation': true, + let outputString = output.readText('template.js'); + assert.strictEqual(outputString, expected); + assert.ok(outputString.includes('Huzzah!')); + }); + + it('AST Plugins have access to `isProduction` status', async function () { + let source = '{{foo-bar}}'; + input.write({ + 'template.hbs': source, + }); + + let wasProduction = false; + let plugin = (env) => { + wasProduction = env.isProduction; + + return { + name: 'fake-ast-plugin', + + visitor: {}, + }; }; - let tree = new TemplateCompiler(input.path(), htmlbarsOptions); + htmlbarsOptions.isProduction = true; + htmlbarsOptions.plugins.ast.push(plugin); + + let tree = new TemplateCompiler(input.path(), htmlbarsOptions, false); output = createBuilder(tree); - yield output.build(); + await output.build(); - let source = input.readText('web-component-template.hbs'); - let expected = `export default Ember.HTMLBars.template(${htmlbarsPrecompile(source, { - moduleName: 'web-component-template.hbs', - })});`; + assert.ok(wasProduction); + }); - assert.strictEqual(output.readText('web-component-template.js'), expected); - }) - ); + it( + 'ignores utf-8 byte order marks', + co.wrap(function* () { + let tree = new TemplateCompiler(input.path(), htmlbarsOptions, false); - it( - 'passes FEATURES to compiler when provided as `EmberENV.FEATURES`', - co.wrap(function* () { - htmlbarsOptions.EmberENV = { - FEATURES: { + output = createBuilder(tree); + yield output.build(); + + let source = input.readText('template.hbs'); + let expected = stripIndent` + import { createTemplateFactory } from '@ember/template-factory'; + + export default createTemplateFactory(${htmlbarsPrecompile(source, { + moduleName: 'template-with-bom.hbs', + })}); + `; + + assert.strictEqual(output.readText('template-with-bom.js'), expected); + }) + ); + + it( + 'passes FEATURES to compiler when provided as `FEATURES` [DEPRECATED]', + co.wrap(function* () { + htmlbarsOptions.FEATURES = { 'ember-htmlbars-component-generation': true, - }, - }; + }; - let tree = new TemplateCompiler(input.path(), htmlbarsOptions); + let tree = new TemplateCompiler(input.path(), htmlbarsOptions, false); - output = createBuilder(tree); - yield output.build(); + output = createBuilder(tree); + yield output.build(); - let source = input.readText('web-component-template.hbs'); - let expected = `export default Ember.HTMLBars.template(${htmlbarsPrecompile(source, { - moduleName: 'web-component-template.hbs', - })});`; + let source = input.readText('web-component-template.hbs'); + let expected = stripIndent` + import { createTemplateFactory } from '@ember/template-factory'; - assert.strictEqual(output.readText('web-component-template.js'), expected); - }) - ); + export default createTemplateFactory(${htmlbarsPrecompile(source, { + moduleName: 'web-component-template.hbs', + })}); + `; + + assert.strictEqual(output.readText('web-component-template.js'), expected); + }) + ); + + it( + 'passes FEATURES to compiler when provided as `EmberENV.FEATURES`', + co.wrap(function* () { + htmlbarsOptions.EmberENV = { + FEATURES: { + 'ember-htmlbars-component-generation': true, + }, + }; + + let tree = new TemplateCompiler(input.path(), htmlbarsOptions, false); + + output = createBuilder(tree); + yield output.build(); + + let source = input.readText('web-component-template.hbs'); + let expected = stripIndent` + import { createTemplateFactory } from '@ember/template-factory'; + + export default createTemplateFactory(${htmlbarsPrecompile(source, { + moduleName: 'web-component-template.hbs', + })}); + `; + + assert.strictEqual(output.readText('web-component-template.js'), expected); + }) + ); + }); }); diff --git a/package.json b/package.json index e17103aa..aecc18eb 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "broccoli-plugin": "^4.0.3", "common-tags": "^1.8.0", "ember-cli-babel-plugin-helpers": "^1.1.1", + "ember-cli-version-checker": "^5.1.2", "fs-tree-diff": "^2.0.1", "hash-for-dep": "^1.5.1", "heimdalljs-logger": "^0.1.10", @@ -69,7 +70,6 @@ "ember-cli-babel": "^7.23.1", "ember-cli-dependency-checker": "^3.2.0", "ember-cli-inject-live-reload": "^2.0.2", - "ember-cli-version-checker": "^5.1.1", "ember-compatibility-helpers": "^1.2.2", "ember-export-application-global": "^2.0.1", "ember-load-initializers": "^2.1.1", diff --git a/yarn.lock b/yarn.lock index 4d399693..c2226cf5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4456,6 +4456,15 @@ ember-cli-version-checker@^5.0.2, ember-cli-version-checker@^5.1.1: semver "^7.3.2" silent-error "^1.1.1" +ember-cli-version-checker@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/ember-cli-version-checker/-/ember-cli-version-checker-5.1.2.tgz#649c7b6404902e3b3d69c396e054cea964911ab0" + integrity sha512-rk7GY+FmLn/2e22HsZs0Ycrz8HQ1W3Fv+2TFOuEFW9optnDXDgkntPBIl6gact/LHsfBM5RKbM3dHsIIeLgl0Q== + dependencies: + resolve-package-path "^3.1.0" + semver "^7.3.4" + silent-error "^1.1.1" + ember-cli@~3.25.2: version "3.25.2" resolved "https://registry.yarnpkg.com/ember-cli/-/ember-cli-3.25.2.tgz#e48203adfb2e6ff8f93d88b602f2c7c3b1633be2"