ember-cli-htmlbars
Version:
A library for adding htmlbars to ember CLI
353 lines (295 loc) • 10.7 kB
JavaScript
;
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',
'ember-cli-htmlbars-inline-precompile': 'default',
'htmlbars-inline-precompile': 'default',
'@ember/template-compilation': {
export: 'precompileTemplate',
disableTemplateLiteral: true,
shouldParseScope: true,
},
});
function isInlinePrecompileBabelPluginRegistered(plugins) {
return plugins.some((plugin) => {
if (Array.isArray(plugin)) {
let [pluginPathOrInstance, options, key] = plugin;
let isHTMLBarsInlinePrecompile =
pluginPathOrInstance === require.resolve('babel-plugin-htmlbars-inline-precompile') &&
typeof options.modules === 'object' &&
options.modules['ember-cli-htmlbars'] === 'hbs';
let isEmberTemplateCompilation =
pluginPathOrInstance === require.resolve('babel-plugin-ember-template-compilation') &&
key === 'ember-cli-htmlbars:inline-precompile';
return isHTMLBarsInlinePrecompile || isEmberTemplateCompilation;
} else if (
plugin !== null &&
typeof plugin === 'object' &&
plugin._parallelBabel !== undefined
) {
return plugin._parallelBabel.requireFile === require.resolve('./require-from-worker');
} else {
return false;
}
});
}
function isColocatedBabelPluginRegistered(plugins) {
return plugins.some((plugin) => {
let path = Array.isArray(plugin) ? plugin[0] : plugin;
return typeof path === 'string' && path === require.resolve('./colocated-babel-plugin');
});
}
function buildParalleizedBabelPlugin(
pluginInfo,
projectConfig,
templateCompilerPath,
isProduction,
requiresModuleApiPolyfill
) {
let parallelBabelInfo = {
requireFile: require.resolve('./require-from-worker'),
buildUsing: 'build',
params: {
templateCompilerPath,
isProduction,
projectConfig,
parallelConfigs: pluginInfo.parallelConfigs,
requiresModuleApiPolyfill,
},
};
// parallelBabelInfo will not be used in the cache unless it is explicitly included
let cacheKey;
return {
_parallelBabel: parallelBabelInfo,
baseDir: () => __dirname,
cacheKey: () => {
if (cacheKey === undefined) {
cacheKey = makeCacheKey(
templateCompilerPath,
pluginInfo,
JSON.stringify(parallelBabelInfo)
);
}
return cacheKey;
},
};
}
function buildOptions(projectConfig, templateCompilerPath, pluginInfo) {
let EmberENV = projectConfig.EmberENV || {};
let htmlbarsOptions = {
isHTMLBars: true,
EmberENV: EmberENV,
templateCompiler: getTemplateCompiler(templateCompilerPath, EmberENV),
templateCompilerPath: templateCompilerPath,
pluginNames: pluginInfo.pluginNames,
plugins: {
ast: pluginInfo.plugins,
},
dependencyInvalidation: pluginInfo.dependencyInvalidation,
pluginCacheKey: pluginInfo.cacheKeys,
};
return htmlbarsOptions;
}
const hasGlobalThis = (function () {
try {
let context = vm.createContext();
// we must create a sandboxed context to test if `globalThis` will be
// present _within_ it because in some contexts a globalThis polyfill has
// been evaluated. In that case globalThis would be available on the
// current global context but **would not** be inherited to the global
// contexts created by `vm.createContext`
let type = vm.runInContext(`typeof globalThis`, context);
return type !== 'undefined';
} catch (e) {
return false;
}
})();
function getTemplateCompiler(templateCompilerPath, EmberENV = {}) {
let templateCompilerFullPath = require.resolve(templateCompilerPath);
let cacheData = TemplateCompilerCache.get(templateCompilerFullPath);
if (cacheData === undefined) {
let templateCompilerContents = fs.readFileSync(templateCompilerFullPath);
let templateCompilerCacheKey = crypto
.createHash('md5')
.update(templateCompilerContents)
.digest('hex');
cacheData = {
script: new vm.Script(templateCompilerContents.toString(), {
filename: templateCompilerPath,
}),
templateCompilerCacheKey,
};
TemplateCompilerCache.set(templateCompilerFullPath, cacheData);
}
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,
console,
// Older versions of ember-template-compiler (up until ember-source@3.1.0)
// 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 (!hasGlobalThis) {
sandbox.global = sandbox;
}
let context = vm.createContext(sandbox);
script.runInContext(context);
return context.module.exports;
}
function setup(pluginInfo, options) {
let projectConfig = options.projectConfig || {};
let templateCompilerPath = options.templateCompilerPath;
let htmlbarsOptions = buildOptions(projectConfig, templateCompilerPath, pluginInfo);
let { templateCompiler } = htmlbarsOptions;
if (options.requiresModuleApiPolyfill) {
let templatePrecompile = templateCompiler.precompile;
let precompile = (template, _options) => {
// 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 = [...pluginInfo.plugins].reverse();
let options = {
plugins: {
ast: astPlugins,
},
..._options,
};
return templatePrecompile(template, options);
};
precompile.baseDir = () => path.resolve(__dirname, '..');
let cacheKey;
precompile.cacheKey = () => {
if (cacheKey === undefined) {
cacheKey = makeCacheKey(templateCompilerPath, pluginInfo);
}
cacheKey;
};
return [
require.resolve('babel-plugin-htmlbars-inline-precompile'),
{
precompile,
isProduction: options.isProduction,
ensureModuleApiPolyfill: options.requiresModuleApiPolyfill,
modules: INLINE_PRECOMPILE_MODULES,
},
'ember-cli-htmlbars:inline-precompile',
];
} else {
return [
require.resolve('babel-plugin-ember-template-compilation'),
{
// As above, we present the AST transforms in reverse order
transforms: [...pluginInfo.plugins].reverse(),
compilerPath: require.resolve(templateCompilerPath),
enableLegacyModules: [
'ember-cli-htmlbars',
'ember-cli-htmlbars-inline-precompile',
'htmlbars-inline-precompile',
],
},
'ember-cli-htmlbars:inline-precompile',
];
}
}
function getTemplateCompilerCacheKey(templateCompilerPath) {
let templateCompilerFullPath = require.resolve(templateCompilerPath);
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('|');
}
function setupPlugins(wrappers) {
let plugins = [];
let cacheKeys = [];
let pluginNames = [];
let parallelConfigs = [];
let unparallelizableWrappers = [];
let dependencyInvalidation = false;
let canParallelize = true;
for (let i = 0; i < wrappers.length; i++) {
let wrapper = wrappers[i];
if (wrapper.requireFile) {
const plugin = require(wrapper.requireFile);
wrapper = plugin[wrapper.buildUsing](wrapper.params);
}
pluginNames.push(wrapper.name ? wrapper.name : 'unknown plugin');
if (wrapper.parallelBabel) {
parallelConfigs.push(wrapper.parallelBabel);
} else {
unparallelizableWrappers.push(wrapper.name);
canParallelize = false;
}
dependencyInvalidation = dependencyInvalidation || wrapper.dependencyInvalidation;
plugins.push(addDependencyTracker(wrapper.plugin, wrapper.dependencyInvalidation));
let providesBaseDir = typeof wrapper.baseDir === 'function';
let augmentsCacheKey = typeof wrapper.cacheKey === 'function';
// TODO: investigate if `wrapper.dependencyInvalidation` should actually prevent the warning
if (providesBaseDir || augmentsCacheKey || wrapper.dependencyInvalidation) {
if (providesBaseDir) {
let pluginHashForDep = hashForDep(wrapper.baseDir());
cacheKeys.push(pluginHashForDep);
}
if (augmentsCacheKey) {
cacheKeys.push(wrapper.cacheKey());
}
} else {
logger.debug(
'ember-cli-htmlbars is opting out of caching due to an AST plugin that does not provide a caching strategy: `' +
wrapper.name +
'`.'
);
cacheKeys.push(new Date().getTime() + '|' + Math.random());
}
}
return {
plugins,
pluginNames,
cacheKeys,
parallelConfigs,
canParallelize,
unparallelizableWrappers,
dependencyInvalidation: !!dependencyInvalidation,
};
}
module.exports = {
buildOptions,
setup,
setupPlugins,
isColocatedBabelPluginRegistered,
isInlinePrecompileBabelPluginRegistered,
buildParalleizedBabelPlugin,
};