diff --git a/ember-addon-main.js b/ember-addon-main.js index 9e718b84..47f1445f 100644 --- a/ember-addon-main.js +++ b/ember-addon-main.js @@ -3,13 +3,78 @@ const path = require('path'); const utils = require('./utils'); const hashForDep = require('hash-for-dep'); +const VersionChecker = require('ember-cli-version-checker'); + +// used as a way to memoize canAvoidCacheBusting so that we can avoid many many +// top down searches of the addon heirarchy +const NEEDS_CACHE_BUSTING = new WeakMap(); + +function canAvoidCacheBusting(project) { + if (NEEDS_CACHE_BUSTING.has(project)) { + return NEEDS_CACHE_BUSTING.get(project); + } + + let checker = new VersionChecker(project); + let emberVersion = checker.forEmber(); + + let hasLegacyHTMLBarsAddons = checkAddonsForLegacyVersion(project); + + let needsCacheBusting = hasLegacyHTMLBarsAddons || emberVersion.lt('1.13.0'); + NEEDS_CACHE_BUSTING.set(project, !!needsCacheBusting); + + return needsCacheBusting; +} + +function checkAddonsForLegacyVersion(addon) { + let registry = addon.registry; + + let plugins = registry && registry.load('htmlbars-ast-plugin'); + let hasPlugins = plugins && plugins.length > 0; + + if (hasPlugins) { + let htmlbarsInstance = addon.addons.find(addon => addon.name === 'ember-cli-htmlbars'); + if (!htmlbarsInstance || htmlbarsInstance.legacyPluginRegistrationCacheBustingRequired !== false) { + return true; + } + + let inlineHTMLBarsCompileInstance = addon.addons.find(addon => addon.name === 'ember-cli-htmlbars-inline-precompile'); + if (!inlineHTMLBarsCompileInstance || inlineHTMLBarsCompileInstance.legacyPluginRegistrationCacheBustingRequired !== false) { + return true; + } + } + + return addon.addons.some(checkAddonsForLegacyVersion); +} module.exports = { name: require('./package').name, + init() { + this._super.init && this._super.init.apply(this, arguments); + + // default to `true` + this.legacyPluginRegistrationCacheBustingRequired = true; + }, + + included() { + this._super.included.apply(this, arguments); + + // populated lazily to ensure that the registries throughout the entire + // project are populated + // + // population happens in setupPreprocessorRegistry hook, which is called in + // `lib/broccoli/ember-app.js` constructor and `lib/models/addon.js` + // constructor in ember-cli itself + this.legacyPluginRegistrationCacheBustingRequired = canAvoidCacheBusting(this.project); + }, + parentRegistry: null, purgeModule(templateCompilerPath) { + // do nothing if we are operating in the "new world" and avoiding + // global state mutations... + if (this.legacyPluginRegistrationCacheBustingRequired === false) { return; } + // ensure we get a fresh templateCompilerModuleInstance per ember-addon // instance NOTE: this is a quick hack, and will only work as long as // templateCompilerPath is a single file bundle @@ -43,13 +108,20 @@ module.exports = { toTree(tree) { let htmlbarsOptions = this._addon.htmlbarsOptions(); let TemplateCompiler = require('./index'); + return new TemplateCompiler(tree, htmlbarsOptions); }, - precompile(string) { + precompile(string, _options) { let htmlbarsOptions = this._addon.htmlbarsOptions(); let templateCompiler = htmlbarsOptions.templateCompiler; - return utils.template(templateCompiler, string); + + let options = Object.assign({}, _options, { + contents: string, + plugins: htmlbarsOptions.plugins, + }); + + return utils.template(templateCompiler, string, options); } }); @@ -83,18 +155,24 @@ module.exports = { }, htmlbarsOptions() { + if (this._htmlbarsOptions && this.legacyPluginRegistrationCacheBustingRequired === false) { + return this._htmlbarsOptions; + } + let projectConfig = this.projectConfig() || {}; let EmberENV = projectConfig.EmberENV || {}; let templateCompilerPath = this.templateCompilerPath(); + let pluginInfo = this.astPlugins(); this.purgeModule(templateCompilerPath); // do a full clone of the EmberENV (it is guaranteed to be structured - // cloneable) to prevent ember-template-compiler.js from mutating - // the shared global config + // cloneable) to prevent ember-template-compiler.js from mutating the + // shared global config (the cloning can be removed after we drop support + // for Ember < 3.2). let clonedEmberENV = JSON.parse(JSON.stringify(EmberENV)); global.EmberENV = clonedEmberENV; // Needed for eval time feature flag checks - let pluginInfo = this.astPlugins(); + let htmlbarsOptions = { isHTMLBars: true, @@ -109,12 +187,24 @@ module.exports = { pluginCacheKey: pluginInfo.cacheKeys }; + if (this.legacyPluginRegistrationCacheBustingRequired !== false) { + let plugins = pluginInfo.plugins; + + if (plugins) { + for (let type in plugins) { + for (let i = 0, l = plugins[type].length; i < l; i++) { + htmlbarsOptions.templateCompiler.registerPlugin(type, plugins[type][i]); + } + } + } + } + this.purgeModule(templateCompilerPath); delete global.Ember; delete global.EmberENV; - return htmlbarsOptions; + return this._htmlbarsOptions = htmlbarsOptions; }, astPlugins() { diff --git a/index.js b/index.js index 8efbc156..c018bbd1 100644 --- a/index.js +++ b/index.js @@ -35,9 +35,7 @@ class TemplateCompiler extends Filter { this.inputTree = inputTree; this.precompile = this.options.templateCompiler.precompile; - this.registerPlugin = this.options.templateCompiler.registerPlugin; - this.registerPlugins(); this.initializeFeatures(); } @@ -45,18 +43,6 @@ class TemplateCompiler extends Filter { return __dirname; } - registerPlugins() { - let plugins = this.options.plugins; - - if (plugins) { - for (let type in plugins) { - for (let i = 0, l = plugins[type].length; i < l; i++) { - this.registerPlugin(type, plugins[type][i]); - } - } - } - } - initializeFeatures() { let EmberENV = this.options.EmberENV; let FEATURES = this.options.FEATURES; @@ -76,7 +62,8 @@ class TemplateCompiler extends Filter { try { return 'export default ' + utils.template(this.options.templateCompiler, stripBom(string), { contents: string, - moduleName: relativePath + moduleName: relativePath, + plugins: this.options.plugins, }) + ';'; } catch(error) { rethrowBuildError(error); diff --git a/node-tests/purge-module-test.js b/node-tests/purge-module-test.js index f6396138..37ddfe2d 100644 --- a/node-tests/purge-module-test.js +++ b/node-tests/purge-module-test.js @@ -1,11 +1,15 @@ 'use strict'; -const purgeModule = require('../ember-addon-main').purgeModule; +const _purgeModule = require('../ember-addon-main').purgeModule; const expect = require('chai').expect; describe('purgeModule', function() { const FIXTURE_COMPILER_PATH = require.resolve('./fixtures/compiler'); + function purgeModule(modulePath) { + return _purgeModule.call({}, modulePath); + } + it('it works correctly', function() { expect(purgeModule('asdfasdfasdfaf-unknown-file')).to.eql(undefined); diff --git a/node-tests/template_compiler_test.js b/node-tests/template_compiler_test.js index 76f2c5b2..911c489f 100644 --- a/node-tests/template_compiler_test.js +++ b/node-tests/template_compiler_test.js @@ -5,12 +5,21 @@ const TemplateCompiler = require('../index'); const co = require('co'); const { createTempDir, createBuilder } = require('broccoli-test-helper'); const fixturify = require('fixturify'); +const MergeTrees = require('broccoli-merge-trees'); describe('TemplateCompiler', function(){ this.timeout(10000); let input, output, builder; + function buildHTMLBarsOptions(plugins) { + return { + plugins, + isHTMLBars: true, + templateCompiler: require('ember-source/dist/ember-template-compiler.js'), + }; + } + beforeEach(co.wrap(function*() { input = yield createTempDir(); input.write(fixturify.readSync(`${__dirname}/fixtures`)); @@ -31,11 +40,7 @@ describe('TemplateCompiler', function(){ let htmlbarsOptions, htmlbarsPrecompile; beforeEach(function() { - htmlbarsOptions = { - isHTMLBars: true, - templateCompiler: require('ember-source/dist/ember-template-compiler.js') - }; - + htmlbarsOptions = buildHTMLBarsOptions(); htmlbarsPrecompile = htmlbarsOptions.templateCompiler.precompile; }); @@ -95,4 +100,54 @@ describe('TemplateCompiler', function(){ assert.strictEqual(output.readText('web-component-template.js'), expected); })); + + describe('multiple instances', function() { + let first, second; + + beforeEach(co.wrap(function*() { + first = yield createTempDir(); + second = yield createTempDir(); + })); + + it('allows each instance to have separate AST plugins', co.wrap(function*() { + first.write({ + 'first': { + 'foo.hbs': `LOLOL`, + } + }); + + second.write({ + 'second': { + 'bar.hbs': `LOLOL`, + } + }); + + class SillyPlugin { + constructor(syntax) { + this.syntax = syntax; + } + + transform(ast) { + this.syntax.traverse(ast, { + TextNode(node) { + node.chars = 'NOT FUNNY!'; + } + }); + + return ast; + } + } + + let firstTree = new TemplateCompiler(first.path(), buildHTMLBarsOptions({ + ast: [SillyPlugin], + })); + let secondTree = new TemplateCompiler(second.path(), buildHTMLBarsOptions()); + + output = createBuilder(new MergeTrees([firstTree, secondTree])); + yield output.build(); + + assert.ok(output.readText('first/foo.js').includes('NOT FUNNY'), 'first was transformed'); + assert.ok(output.readText('second/bar.js').includes('LOLOL'), 'second was not transformed'); + })); + }); }); diff --git a/package.json b/package.json index cd3d923c..2da4e8e0 100644 --- a/package.json +++ b/package.json @@ -34,12 +34,14 @@ }, "dependencies": { "broccoli-persistent-filter": "^2.1.0", + "ember-cli-version-checker": "^2.1.2", "hash-for-dep": "^1.2.3", "json-stable-stringify": "^1.0.1", "strip-bom": "^3.0.0" }, "devDependencies": { "@ember/optional-features": "^0.7.0", + "broccoli-merge-trees": "^3.0.1", "broccoli-test-helper": "^1.5.0", "chai": "^4.2.0", "co": "^4.6.0",