Skip to content

Commit

Permalink
Merge pull request #673 from ember-cli/backport-sandbox-changes
Browse files Browse the repository at this point in the history
  • Loading branch information
rwjblue authored Mar 3, 2021
2 parents 3eb40a4 + b97ad94 commit 653e0d2
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 197 deletions.
52 changes: 26 additions & 26 deletions lib/template-compiler-plugin.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use strict';

const fs = require('fs');
const path = require('path');
const utils = require('./utils');
const Filter = require('broccoli-persistent-filter');
Expand Down Expand Up @@ -40,33 +39,30 @@ class TemplateCompiler extends Filter {
// TODO: do we need this?
this.precompile = this.options.templateCompiler.precompile;

let { templateCompiler, plugins, EmberENV } = options;
let { templateCompiler, EmberENV } = options;

utils.registerPlugins(templateCompiler, plugins);
utils.initializeEmberENV(templateCompiler, EmberENV);
}

baseDir() {
return __dirname;
}

unregisterPlugins() {
let { templateCompiler, plugins } = this.options;

utils.unregisterPlugins(templateCompiler, plugins);
}

registeredASTPlugins() {
// This is a super obtuse way to get access to the plugins we've registered
// it also returns other plugins that are registered by ember itself.
let options = this.options.templateCompiler.compileOptions();
return (options.plugins && options.plugins.ast) || [];
}

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
// `ember-template-compiler.js` to set up these plugins (because Ember ~ 1.13
// only had `registerPlugin`, and there was no way to pass plugins directly
// to the call to `compile`/`precompile`). calling `registerPlugin`
// unfortunately **inverted** the order of plugins (it essentially did
// `PLUGINS = [plugin, ...PLUGINS]`).
//
// sooooooo...... we are forced to maintain that **absolutely bonkers** ordering
let astPlugins = this.options.plugins ? [].concat(this.options.plugins.ast).reverse() : [];

let result =
'export default ' +
utils.template(this.options.templateCompiler, stripBom(string), {
Expand All @@ -76,10 +72,18 @@ class TemplateCompiler extends Filter {
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.registeredASTPlugins());
let plugins = pluginsWithDependencies(this.options.plugins.ast);
let dependencies = [];
for (let i = 0; i < plugins.length; i++) {
let pluginDeps = plugins[i].getDependencies(relativePath);
Expand All @@ -105,20 +109,16 @@ class TemplateCompiler extends Filter {
return strippedOptions;
}

_templateCompilerContents() {
if (this.options.templateCompilerPath) {
return fs.readFileSync(this.options.templateCompilerPath, { encoding: 'utf8' });
} else {
return '';
}
}

optionsHash() {
if (!this._optionsHash) {
let templateCompilerCacheKey = utils.getTemplateCompilerCacheKey(
this.options.templateCompilerPath
);

this._optionsHash = crypto
.createHash('md5')
.update(stringify(this._buildOptionsForHash()), 'utf8')
.update(stringify(this._templateCompilerContents()), 'utf8')
.update(templateCompilerCacheKey, 'utf8')
.digest('hex');
}

Expand Down
147 changes: 86 additions & 61 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
'use strict';

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const hashForDep = require('hash-for-dep');
const debugGenerator = require('heimdalljs-logger');
const logger = debugGenerator('ember-cli-htmlbars:utils');
const addDependencyTracker = require('./addDependencyTracker');
const vm = require('vm');

const TemplateCompilerCache = new Map();

const INLINE_PRECOMPILE_MODULES = Object.freeze({
'ember-cli-htmlbars': 'hbs',
Expand Down Expand Up @@ -79,18 +83,10 @@ function buildParalleizedBabelPlugin(pluginInfo, templateCompilerPath, isProduct
function buildOptions(projectConfig, templateCompilerPath, pluginInfo) {
let EmberENV = projectConfig.EmberENV || {};

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
let clonedEmberENV = JSON.parse(JSON.stringify(EmberENV));
global.EmberENV = clonedEmberENV; // Needed for eval time feature flag checks

let htmlbarsOptions = {
isHTMLBars: true,
EmberENV: EmberENV,
templateCompiler: require(templateCompilerPath),
templateCompiler: getTemplateCompiler(templateCompilerPath, EmberENV),
templateCompilerPath: templateCompilerPath,

plugins: {
Expand All @@ -102,57 +98,64 @@ function buildOptions(projectConfig, templateCompilerPath, pluginInfo) {
pluginCacheKey: pluginInfo.cacheKeys,
};

purgeModule(templateCompilerPath);

delete global.Ember;
delete global.EmberENV;

return htmlbarsOptions;
}

function purgeModule(templateCompilerPath) {
// 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
//
// (╯°□°)╯︵ ɹǝqɯǝ
//
// we will also fix this in ember for future releases

// Module will be cached in .parent.children as well. So deleting from require.cache alone is not sufficient.
let mod = require.cache[templateCompilerPath];
if (mod && mod.parent) {
let index = mod.parent.children.indexOf(mod);
if (index >= 0) {
mod.parent.children.splice(index, 1);
} else {
throw new TypeError(
`ember-cli-htmlbars attempted to purge '${templateCompilerPath}' but something went wrong.`
);
}
}
function getTemplateCompiler(templateCompilerPath, EmberENV = {}) {
let templateCompilerFullPath = require.resolve(templateCompilerPath);
let cacheData = TemplateCompilerCache.get(templateCompilerFullPath);

delete require.cache[templateCompilerPath];
}
if (cacheData === undefined) {
let templateCompilerContents = fs.readFileSync(templateCompilerFullPath, { encoding: 'utf-8' });
let templateCompilerCacheKey = crypto
.createHash('md5')
.update(templateCompilerContents)
.digest('hex');

function registerPlugins(templateCompiler, plugins) {
if (plugins) {
for (let type in plugins) {
for (let i = 0, l = plugins[type].length; i < l; i++) {
templateCompiler.registerPlugin(type, plugins[type][i]);
}
}
cacheData = {
script: new vm.Script(templateCompilerContents, {
filename: templateCompilerPath,
}),

templateCompilerCacheKey,
};

TemplateCompilerCache.set(templateCompilerFullPath, cacheData);
}
}

function unregisterPlugins(templateCompiler, plugins) {
if (plugins) {
for (let type in plugins) {
for (let i = 0, l = plugins[type].length; i < l; i++) {
templateCompiler.unregisterPlugin(type, plugins[type][i]);
}
}
let { script } = cacheData;

// 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
let clonedEmberENV = JSON.parse(JSON.stringify(EmberENV));

let sandbox = {
EmberENV: clonedEmberENV,

// Older versions of ember-template-compiler (up until [email protected])
// eagerly access `setTimeout` without checking via `typeof` first
setTimeout,
clearTimeout,

// fake the module into thinking we are running inside a Node context
module: { require, exports: {} },
require,
};

// if we are running on a Node version _without_ a globalThis
// we must provide a `global`
//
// this is due to https://git.io/Jtb7s (Ember 3.27+)
if (typeof globalThis === 'undefined') {
sandbox.global = sandbox;
}

let context = vm.createContext(sandbox);

script.runInContext(context);

return context.module.exports;
}

function initializeEmberENV(templateCompiler, EmberENV) {
Expand Down Expand Up @@ -196,11 +199,22 @@ function setup(pluginInfo, options) {
let htmlbarsOptions = buildOptions(projectConfig, templateCompilerPath, pluginInfo);
let { templateCompiler } = htmlbarsOptions;

registerPlugins(templateCompiler, {
ast: pluginInfo.plugins,
});
let templatePrecompile = templateCompiler.precompile;

let precompile = (template, options) => {
let plugins = pluginInfo.plugins || [];
// concat so we ensure we don't mutate the original plugins
// reverse to ensure that original AST plugin ordering is preserved
let astPlugins = [].concat(plugins).reverse();

options = options || {};
options.plugins = {
ast: astPlugins,
};

return templatePrecompile(template, options);
};

let { precompile } = templateCompiler;
precompile.baseDir = () => path.resolve(__dirname, '..');

let cacheKey;
Expand All @@ -220,10 +234,22 @@ function setup(pluginInfo, options) {
return plugin;
}

function makeCacheKey(templateCompilerPath, pluginInfo, extra) {
function getTemplateCompilerCacheKey(templateCompilerPath) {
let templateCompilerFullPath = require.resolve(templateCompilerPath);
let templateCompilerCacheKey = fs.readFileSync(templateCompilerFullPath, { encoding: 'utf-8' });
let cacheData = TemplateCompilerCache.get(templateCompilerFullPath);

if (cacheData === undefined) {
getTemplateCompiler(templateCompilerFullPath);
cacheData = TemplateCompilerCache.get(templateCompilerFullPath);
}

return cacheData.templateCompilerCacheKey;
}

function makeCacheKey(templateCompilerPath, pluginInfo, extra) {
let templateCompilerCacheKey = getTemplateCompilerCacheKey(templateCompilerPath);
let cacheItems = [templateCompilerCacheKey, extra].concat(pluginInfo.cacheKeys.sort());

// extra may be undefined
return cacheItems.filter(Boolean).join('|');
}
Expand Down Expand Up @@ -288,9 +314,6 @@ function setupPlugins(wrappers) {

module.exports = {
buildOptions,
purgeModule,
registerPlugins,
unregisterPlugins,
initializeEmberENV,
template,
setup,
Expand All @@ -299,4 +322,6 @@ module.exports = {
isColocatedBabelPluginRegistered,
isInlinePrecompileBabelPluginRegistered,
buildParalleizedBabelPlugin,
getTemplateCompiler,
getTemplateCompilerCacheKey,
};
Loading

0 comments on commit 653e0d2

Please sign in to comment.