From 26d17e1ca6b533d869b97695a5264add1db5759b Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Sat, 23 Oct 2021 11:38:09 -0400 Subject: [PATCH 1/8] beginning to standardize all template compilation as JS That is, instead of having different code paths for standalone hbs files vs templates embedded inside Javascript, we do a small transportation to the standalone hbs files to make them into JS first, and then share all the template compilation machinery. This is also setting us up to take advantage of lexical scope in non-strict-mode which landed in ember-source 3.28.4. --- packages/core/src/template-compiler-common.ts | 18 ++++---------- packages/core/src/template-compiler-node.ts | 20 +--------------- .../src/template-colocation-plugin.ts | 2 +- packages/webpack/src/ember-webpack.ts | 8 ++++++- yarn.lock | 24 +++++++++---------- 5 files changed, 26 insertions(+), 46 deletions(-) diff --git a/packages/core/src/template-compiler-common.ts b/packages/core/src/template-compiler-common.ts index a64f8b170..f12d3b6a4 100644 --- a/packages/core/src/template-compiler-common.ts +++ b/packages/core/src/template-compiler-common.ts @@ -4,6 +4,7 @@ import { join, sep } from 'path'; import type { PluginItem } from '@babel/core'; import { Memoize } from 'typescript-memoize'; import wrapLegacyHbsPluginIfNeeded from 'wrap-legacy-hbs-plugin-if-needed'; +import jsStringEscape from 'js-string-escape'; export interface Plugins { ast?: unknown[]; @@ -163,19 +164,10 @@ export class TemplateCompiler { } // Compiles all the way from a template string to a javascript module string. - compile(moduleName: string, contents: string) { - let { compiled, dependencies } = this.precompile(contents, { filename: moduleName }); - let lines = []; - let counter = 0; - for (let { runtimeName, path } of dependencies) { - lines.push(`import a${counter} from "${path.split(sep).join('/')}";`); - lines.push(`window.define('${runtimeName}', function(){ return a${counter++}});`); - } - - lines.push(`import { createTemplateFactory } from '@ember/template-factory';`); - lines.push(`export default createTemplateFactory(${compiled});`); - - return lines.join('\n'); + compile(_moduleName: string, contents: string) { + return [`import { hbs } from 'ember-cli-htmlbars';`, `export default hbs("${jsStringEscape(contents)}")`].join( + '\n' + ); } // Applies all custom AST transforms and emits the results still as diff --git a/packages/core/src/template-compiler-node.ts b/packages/core/src/template-compiler-node.ts index e78746a6d..06ef4a908 100644 --- a/packages/core/src/template-compiler-node.ts +++ b/packages/core/src/template-compiler-node.ts @@ -1,11 +1,10 @@ import { Resolver } from './resolver'; import { join } from 'path'; -import { PluginItem, transform } from '@babel/core'; +import { PluginItem } from '@babel/core'; import type { Params as InlineBabelParams } from './babel-plugin-stage1-inline-hbs-node'; import { Plugins } from './ember-template-compiler-types'; import { getEmberExports } from './load-ember-template-compiler'; import { TemplateCompiler, matchesSourceFile } from './template-compiler-common'; -import adjustImportsPlugin, { Options as AdjustImportsOptions } from './babel-plugin-adjust-imports'; export interface NodeTemplateCompilerParams { compilerPath: string; @@ -25,23 +24,6 @@ export class NodeTemplateCompiler extends TemplateCompiler { }); } - compile(moduleName: string, contents: string) { - let src = super.compile(moduleName, contents); - let resolver = this.params.resolver; - if (resolver) { - let opts: AdjustImportsOptions = resolver.adjustImportsOptions; - return transform(src, { - filename: moduleName, - generatorOpts: { - compact: false, - }, - plugins: [[adjustImportsPlugin, opts]], - })!.code!; - } else { - return src; - } - } - // Use applyTransforms on the contents of inline hbs template strings inside // Javascript. inlineTransformsBabelPlugin(): PluginItem { diff --git a/packages/shared-internals/src/template-colocation-plugin.ts b/packages/shared-internals/src/template-colocation-plugin.ts index 261f83356..2b6157dcb 100644 --- a/packages/shared-internals/src/template-colocation-plugin.ts +++ b/packages/shared-internals/src/template-colocation-plugin.ts @@ -50,7 +50,7 @@ export default function main(babel: typeof Babel) { } let hbsFilename = filename.replace(/\.\w{1,3}$/, '') + '.hbs'; - if (existsSync(hbsFilename)) { + if (hbsFilename !== filename && existsSync(hbsFilename)) { state.colocatedTemplate = hbsFilename; } }, diff --git a/packages/webpack/src/ember-webpack.ts b/packages/webpack/src/ember-webpack.ts index ef089ed7f..728ee34a1 100644 --- a/packages/webpack/src/ember-webpack.ts +++ b/packages/webpack/src/ember-webpack.ts @@ -160,7 +160,13 @@ const Webpack: PackagerConstructor = class Webpack implements Packager { test: /\.hbs$/, use: nonNullArray([ - maybeThreadLoader(templateCompiler.isParallelSafe, this.extraThreadLoaderOptions), + maybeThreadLoader(babel.isParallelSafe, this.extraThreadLoaderOptions), + babelLoaderOptions( + babel.majorVersion, + variant, + join(this.pathToVanillaApp, babel.filename), + this.extraBabelLoaderOptions + ), { loader: require.resolve('@embroider/hbs-loader'), options: hbsOptions, diff --git a/yarn.lock b/yarn.lock index 702ba4c2a..7110a7171 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2066,13 +2066,6 @@ dependencies: babel-plugin-debug-macros "^0.3.4" -"@glimmer/vm-babel-plugins@0.78.2": - version "0.78.2" - resolved "https://registry.yarnpkg.com/@glimmer/vm-babel-plugins/-/vm-babel-plugins-0.78.2.tgz#b530a19f54da385c7099a22cf348e9062d186838" - integrity sha512-GSEf16h6OCtKx7PsSvD21cLXZuVc6swW2rSOAvfLeZco1DEWMRgYTwkCkColydKZcQ3gvwbPBeYwTC2K6tlnjg== - dependencies: - babel-plugin-debug-macros "^0.3.4" - "@glimmer/vm-babel-plugins@0.79.3": version "0.79.3" resolved "https://registry.yarnpkg.com/@glimmer/vm-babel-plugins/-/vm-babel-plugins-0.79.3.tgz#a8e6949f8ecc10786831d188f1001eb80927297f" @@ -2080,6 +2073,13 @@ dependencies: babel-plugin-debug-macros "^0.3.4" +"@glimmer/vm-babel-plugins@0.80.3": + version "0.80.3" + resolved "https://registry.yarnpkg.com/@glimmer/vm-babel-plugins/-/vm-babel-plugins-0.80.3.tgz#434b62172318cac43830d3ac29818cf2c5f111c1" + integrity sha512-9ej6xlm5MzHBJ5am2l0dbbn8Z0wJoYoMpM8FcrGMlUP6SPMLWxvxpMsApgQo8u6dvZRCjR3/bw3fdf7GOy0AFw== + dependencies: + babel-plugin-debug-macros "^0.3.4" + "@glimmer/vm@^0.42.2": version "0.42.2" resolved "https://registry.yarnpkg.com/@glimmer/vm/-/vm-0.42.2.tgz#492a4f05eac587c3a37371b3c62593f20bef553d" @@ -8850,16 +8850,16 @@ ember-source-channel-url@^3.0.0: node-fetch "^2.6.0" "ember-source-latest@npm:ember-source@latest": - version "3.27.5" - resolved "https://registry.yarnpkg.com/ember-source/-/ember-source-3.27.5.tgz#8e9ce24c17e7a16dc3c2b128d3d3e24ea79e6726" - integrity sha512-oSGM9mD6BuOcGilYqU+F2MtCferQhKWO3REX1P9qgN1Wzfa5kXjbjBBdPNWfBtg7bZLGM27H8JgiV6+t3uGegA== + version "3.28.4" + resolved "https://registry.yarnpkg.com/ember-source/-/ember-source-3.28.4.tgz#b6ac2b1e369ef533d05164c65078b4ceafdb6390" + integrity sha512-s7kVy0E08erAHUTI/8SZZvXt3an/xb2g5K+m4Rybvo8Tr/noMk3lIdtyQkSvmgMZ/BbvoW8spS630sO0/JN4Eg== dependencies: "@babel/helper-module-imports" "^7.8.3" "@babel/plugin-transform-block-scoping" "^7.8.3" "@babel/plugin-transform-object-assign" "^7.8.3" "@ember/edition-utils" "^1.2.0" - "@glimmer/vm-babel-plugins" "0.78.2" - babel-plugin-debug-macros "^0.3.3" + "@glimmer/vm-babel-plugins" "0.80.3" + babel-plugin-debug-macros "^0.3.4" babel-plugin-filter-imports "^4.0.0" broccoli-concat "^4.2.4" broccoli-debug "^0.6.4" From 237e42bdf7782e9c1f458cb5c26d988411fe1c2f Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Thu, 11 Nov 2021 10:18:06 -0500 Subject: [PATCH 2/8] dropping lots of standalone template-compiler stuff that won't be needed --- packages/addon-dev/src/rollup-hbs-plugin.ts | 6 +- packages/compat/src/audit.ts | 54 +++------------ .../src/template-compiler-broccoli-plugin.ts | 15 ++-- packages/compat/src/v1-addon.ts | 2 +- packages/compat/tests/audit.test.ts | 68 +++++++++---------- packages/core/src/app.ts | 14 +--- .../core/src/babel-plugin-inline-hbs-deps.ts | 6 +- .../core/src/babel-plugin-inline-hbs-node.ts | 7 ++ packages/core/src/babel-plugin-inline-hbs.ts | 2 +- packages/core/src/index.ts | 2 - packages/core/src/packager.ts | 6 -- packages/core/src/template-compiler-common.ts | 12 ---- packages/core/src/write-template-compiler.ts | 20 ------ packages/hbs-loader/src/index.ts | 16 +---- packages/shared-internals/package.json | 2 + packages/shared-internals/src/hbs-to-js.ts | 7 ++ packages/shared-internals/src/index.ts | 1 + packages/shared-internals/src/metadata.ts | 4 -- packages/webpack/src/ember-webpack.ts | 13 +--- test-packages/support/build.ts | 12 +--- 20 files changed, 78 insertions(+), 191 deletions(-) delete mode 100644 packages/core/src/write-template-compiler.ts create mode 100644 packages/shared-internals/src/hbs-to-js.ts diff --git a/packages/addon-dev/src/rollup-hbs-plugin.ts b/packages/addon-dev/src/rollup-hbs-plugin.ts index 3c62d71e5..4da492352 100644 --- a/packages/addon-dev/src/rollup-hbs-plugin.ts +++ b/packages/addon-dev/src/rollup-hbs-plugin.ts @@ -1,7 +1,7 @@ import { createFilter } from '@rollup/pluginutils'; import type { Plugin } from 'rollup'; import { readFileSync } from 'fs'; -const backtick = '`'; +import { hbsToJS } from '@embroider/shared-internals'; export default function rollupHbsPlugin(): Plugin { const filter = createFilter('**/*.hbs'); @@ -11,9 +11,7 @@ export default function rollupHbsPlugin(): Plugin { load(id: string) { if (!filter(id)) return; let input = readFileSync(id, 'utf8'); - let code = - `import { hbs } from 'ember-cli-htmlbars';\n` + - `export default hbs${backtick}${input}${backtick};`; + let code = hbsToJS(input); return { code, id: id + '.js', diff --git a/packages/compat/src/audit.ts b/packages/compat/src/audit.ts index a3f3a1e54..6740bf031 100644 --- a/packages/compat/src/audit.ts +++ b/packages/compat/src/audit.ts @@ -1,7 +1,7 @@ import { readFileSync, readJSONSync } from 'fs-extra'; import { dirname, join, resolve as resolvePath } from 'path'; import resolveModule from 'resolve'; -import { applyVariantToTemplateCompiler, AppMeta, explicitRelative } from '@embroider/core'; +import { AppMeta, explicitRelative, hbsToJS } from '@embroider/core'; import { Memoize } from 'typescript-memoize'; import chalk from 'chalk'; import jsdom from 'jsdom'; @@ -17,7 +17,6 @@ import { } from './audit/babel-visitor'; import { AuditBuildOptions, AuditOptions } from './audit/options'; import { buildApp, BuildError, isBuildError } from './audit/build'; -import CompatResolver from './resolver'; const { JSDOM } = jsdom; @@ -201,7 +200,7 @@ export class Audit { let audit = new this(dir, options); if (options['reuse-build']) { - if (!audit.meta.babel.isParallelSafe || !audit.meta['template-compiler'].isParallelSafe) { + if (!audit.meta.babel.isParallelSafe) { throw new BuildError( `You can't use the ${chalk.red( '--reuse-build' @@ -244,31 +243,13 @@ export class Audit { let config = require(join(this.appDir, this.meta.babel.filename)); config = Object.assign({}, config); config.plugins = config.plugins.filter((p: any) => !isMacrosPlugin(p)); - config.ast = true; - return config; - } - - @Memoize() - private get templateSetup(): { compile: (filename: string, content: string) => string; resolver: CompatResolver } { - // eslint-disable-next-line @typescript-eslint/no-require-imports - let templateCompiler = require(join(this.appDir, this.meta['template-compiler'].filename)); - let resolver = templateCompiler.params.resolver as CompatResolver; - resolver.enableAuditMode(); + // TODO: find the template compiler params inside the babel config and + // maniuplate them to turn on the resolver's enableAuditMode. Or create some + // other out-of-band way to do that. - let compile = applyVariantToTemplateCompiler( - { name: 'default', runtime: 'all', optimizeForProduction: false }, - templateCompiler.compile - ); - return { compile, resolver }; - } - - private get templateCompiler(): (filename: string, content: string) => string { - return this.templateSetup.compile; - } - - private get templateResolver(): CompatResolver { - return this.templateSetup.resolver; + config.ast = true; + return config; } private debug(message: string, ...args: any[]) { @@ -503,26 +484,7 @@ export class Audit { content: Buffer | string ): Promise { let rawSource = content.toString('utf8'); - let js; - try { - js = this.templateCompiler(filename, rawSource); - } catch (err) { - return [ - { - filename, - message: `failed to compile template`, - detail: err.toString().replace(filename, explicitRelative(this.appDir, filename)), - }, - ]; - } - for (let err of this.templateResolver.errorsIn(filename)) { - this.pushFinding({ - filename, - message: err.message, - detail: err.detail, - codeFrame: this.frames.render(this.frames.forSource(rawSource)(err)), - }); - } + let js = hbsToJS(rawSource); return this.visitJS(filename, js); } diff --git a/packages/compat/src/template-compiler-broccoli-plugin.ts b/packages/compat/src/template-compiler-broccoli-plugin.ts index fbcb3953d..582f32c66 100644 --- a/packages/compat/src/template-compiler-broccoli-plugin.ts +++ b/packages/compat/src/template-compiler-broccoli-plugin.ts @@ -4,26 +4,19 @@ import type { TemplateCompiler } from '@embroider/core'; import { join } from 'path'; export default class TemplateCompileTree extends Filter { - constructor(inputTree: Node, private templateCompiler: TemplateCompiler, private stage: 1 | 3) { + constructor(inputTree: Node, private templateCompiler: TemplateCompiler) { super(inputTree, { - name: `embroider-template-compile-stage${stage}`, + name: `embroider-template-compile-stage1`, persist: true, extensions: ['hbs', 'handlebars'], - // in stage3 we are changing the file extensions from hbs to js. In - // stage1, we are just keeping hbs. - targetExtension: stage === 3 ? 'js' : undefined, }); } processString(source: string, relativePath: string) { - if (this.stage === 1) { - return this.templateCompiler.applyTransforms(relativePath, source); - } else { - return this.templateCompiler.compile(relativePath, source); - } + return this.templateCompiler.applyTransforms(relativePath, source); } cacheKeyProcessString(source: string, relativePath: string) { - return `${this.stage}-${this.templateCompiler.cacheKey}` + super.cacheKeyProcessString(source, relativePath); + return `1-${this.templateCompiler.cacheKey}` + super.cacheKeyProcessString(source, relativePath); } baseDir() { return join(__dirname, '..'); diff --git a/packages/compat/src/v1-addon.ts b/packages/compat/src/v1-addon.ts index 348cd8199..5d60e6df4 100644 --- a/packages/compat/src/v1-addon.ts +++ b/packages/compat/src/v1-addon.ts @@ -187,7 +187,7 @@ export default class V1Addon { _addon: this, toTree(this: { _addon: V1Addon }, tree: Node): Node { if (this._addon.templateCompiler) { - return new TemplateCompilerBroccoliPlugin(tree, this._addon.templateCompiler, 1); + return new TemplateCompilerBroccoliPlugin(tree, this._addon.templateCompiler); } else { // when there are no custom AST transforms, we don't need to do // anything at all. diff --git a/packages/compat/tests/audit.test.ts b/packages/compat/tests/audit.test.ts index 1275b8e63..20a25e166 100644 --- a/packages/compat/tests/audit.test.ts +++ b/packages/compat/tests/audit.test.ts @@ -1,9 +1,8 @@ -import { emberTemplateCompilerPath, Project } from '@embroider/test-support'; -import { AppMeta, templateCompilerModule, throwOnWarnings } from '@embroider/core'; +import { Project } from '@embroider/test-support'; +import { AppMeta, throwOnWarnings } from '@embroider/core'; import merge from 'lodash/merge'; import fromPairs from 'lodash/fromPairs'; import { Audit, Finding } from '../src/audit'; -import CompatResolver from '../src/resolver'; describe('audit', function () { throwOnWarnings(); @@ -21,32 +20,6 @@ describe('audit', function () { const resolvableExtensions = ['.js', '.hbs']; - let templateCompiler = templateCompilerModule( - { - compilerPath: emberTemplateCompilerPath(), - compilerChecksum: `mock-compiler-checksum${Math.random()}`, - EmberENV: {}, - plugins: { ast: [] }, - resolver: new CompatResolver({ - root: app.baseDir, - modulePrefix: 'audit-this-app', - options: { staticComponents: false, staticHelpers: false, allowUnsafeDynamicComponents: false }, - activePackageRules: [], - adjustImportsOptions: { - renamePackages: {}, - renameModules: {}, - extraImports: [], - externalsDir: '/tmp/embroider-externals', - activeAddons: {}, - relocatedFiles: {}, - resolvableExtensions, - emberNeedsModulesPolyfill: true, - }, - }), - }, - [] - ); - merge(app.files, { 'index.html': ``, 'app.js': `import Hello from './hello.hbs';`, @@ -55,7 +28,6 @@ describe('audit', function () { babelrc: false, plugins: [], }`, - 'template_compiler.js': templateCompiler.src, }); let appMeta: AppMeta = { type: 'app', @@ -69,10 +41,6 @@ describe('audit', function () { }, 'resolvable-extensions': resolvableExtensions, 'root-url': '/', - 'template-compiler': { - filename: 'template_compiler.js', - isParallelSafe: true, - }, }; merge(app.pkg, { 'ember-addon': appMeta, @@ -290,7 +258,7 @@ describe('audit', function () { expect(Object.keys(result.modules).length).toBe(2); }); - test('finds missing component', async function () { + test('finds missing component in standalone hbs', async function () { merge(app.files, { 'hello.hbs': ``, }); @@ -306,6 +274,25 @@ describe('audit', function () { expect(Object.keys(result.modules).length).toBe(3); }); + test('finds missing component in inline hbs', async function () { + merge(app.files, { + 'app.js': ` + import { hbs } from 'ember-cli-htmlbars'; + hbs(""); + `, + }); + let result = await audit(); + expect(withoutCodeFrames(result.findings)).toEqual([ + { + message: 'Missing component', + detail: 'NoSuchThing', + filename: './app.js', + }, + ]); + expect(result.findings[0].codeFrame).toBeDefined(); + expect(Object.keys(result.modules).length).toBe(3); + }); + test('traverse through template even when it has some errors', async function () { merge(app.files, { 'hello.hbs': ``, @@ -338,6 +325,17 @@ describe('audit', function () { ]); expect(Object.keys(result.modules).length).toBe(4); }); + + test('failure to parse HBS is reported and does not cause cascading errors', async function () { + merge(app.files, { + 'hello.hbs': `{{broken`, + }); + let result = await audit(); + expect(result.findings.map(f => ({ filename: f.filename, message: f.message }))).toEqual([ + { filename: './hello.hbs', message: 'failed to compile template' }, + ]); + expect(Object.keys(result.modules).length).toBe(3); + }); }); function withoutCodeFrames(findings: Finding[]): Finding[] { diff --git a/packages/core/src/app.ts b/packages/core/src/app.ts index 63791a7bf..1d19eef6b 100644 --- a/packages/core/src/app.ts +++ b/packages/core/src/app.ts @@ -27,7 +27,6 @@ import { PluginItem, TransformOptions } from '@babel/core'; import { makePortable } from './portable-babel-config'; import { TemplateCompilerPlugins } from '.'; import type { NodeTemplateCompilerParams } from './template-compiler-node'; -import { templateCompilerModule } from './write-template-compiler'; import { Resolver } from './resolver'; import { Options as AdjustImportsOptions } from './babel-plugin-adjust-imports'; import { mangledEngineRoot } from './engine-mangler'; @@ -398,7 +397,7 @@ export class AppBuilder { ]); } else { // on newer ember versions that don't need the modules-api-polyfill, we - // can compose with the newer babel-plugin-htmlbars-inline-precompile, and + // can compose with the newer babel-plugin-ember-template-compilation, and // use a simpler plugin that only needs to handle inserting discovered // dependencies. babel.plugins.push([ @@ -890,7 +889,6 @@ export class AppBuilder { let finalAssets = await this.updateAssets(assets, appFiles, emberENV); let templateCompiler = this.templateCompiler(emberENV); let babelConfig = this.babelConfig(templateCompiler, appFiles); - let templateCompilerIsParallelSafe = this.addTemplateCompiler(templateCompiler); this.addBabelConfig(babelConfig); let assetPaths = assets.map(asset => asset.relativePath); @@ -913,10 +911,6 @@ export class AppBuilder { type: 'app', version: 2, assets: assetPaths, - 'template-compiler': { - filename: '_template_compiler_.js', - isParallelSafe: templateCompilerIsParallelSafe, - }, babel: { filename: '_babel_config_.js', isParallelSafe: babelConfig.isParallelSafe, @@ -990,12 +984,6 @@ export class AppBuilder { }); } - private addTemplateCompiler(params: NodeTemplateCompilerParams): boolean { - let mod = templateCompilerModule(params, this.portableHints); - writeFileSync(join(this.root, '_template_compiler_.js'), mod.src, 'utf8'); - return mod.isParallelSafe; - } - private addBabelConfig(pconfig: { config: TransformOptions; isParallelSafe: boolean }) { if (!pconfig.isParallelSafe) { warn('Your build is slower because some babel plugins are non-serializable'); diff --git a/packages/core/src/babel-plugin-inline-hbs-deps.ts b/packages/core/src/babel-plugin-inline-hbs-deps.ts index 4d222b797..6eb6f7dd9 100644 --- a/packages/core/src/babel-plugin-inline-hbs-deps.ts +++ b/packages/core/src/babel-plugin-inline-hbs-deps.ts @@ -8,7 +8,7 @@ import { templateCompilationModules } from '@embroider/shared-internals'; import { ImportUtil } from 'babel-import-util'; /* - In order to coordinate with babel-plugin-htmlbars-inline-precompile, we need + In order to coordinate with babel-plugin-ember-template-compilation, we need to give it a `precompile` function that, as a side-effect, captures the dependencies needed within the current file. We do this coordination via this module-scoped variable, which is safe given Javascript's single-threaded @@ -18,12 +18,12 @@ let currentState: State | undefined; /* This is the precompile function you should pass to - babel-plugin-htmlbars-inline-precompile. + babel-plugin-ember-template-compilation. */ export function precompile(templateSource: string, options: Record) { if (!currentState) { throw new Error( - `bug: babel-plugin-htmlbars-inline-precompile and babel-plugin-inline-hbs-deps aren't coordinating correctly` + `bug: babel-plugin-ember-template-compilation and babel-plugin-inline-hbs-deps aren't coordinating correctly` ); } let { compiled, dependencies } = compiler(currentState).precompile(templateSource, { diff --git a/packages/core/src/babel-plugin-inline-hbs-node.ts b/packages/core/src/babel-plugin-inline-hbs-node.ts index 362a6e6f5..bc5a46044 100644 --- a/packages/core/src/babel-plugin-inline-hbs-node.ts +++ b/packages/core/src/babel-plugin-inline-hbs-node.ts @@ -1,3 +1,10 @@ +/* + This plugin is used only for Ember < 3.27. For newer Ember's we have a + different implementation that shares the standard + babel-plugin-ember-template-compilation and supports passing Javascript + lexically scoped names into templates. +*/ + import { NodeTemplateCompiler, NodeTemplateCompilerParams } from './template-compiler-node'; import make from './babel-plugin-inline-hbs'; import type * as Babel from '@babel/core'; diff --git a/packages/core/src/babel-plugin-inline-hbs.ts b/packages/core/src/babel-plugin-inline-hbs.ts index 8517412b8..b74d7c75a 100644 --- a/packages/core/src/babel-plugin-inline-hbs.ts +++ b/packages/core/src/babel-plugin-inline-hbs.ts @@ -1,7 +1,7 @@ /* This plugin is used only for Ember < 3.27. For newer Ember's we have a different implementation that shares the standard - babel-plugin-htmlbars-inline-precompile and supports passing Javascript + babel-plugin-ember-template-compilation and supports passing Javascript lexically scoped names into templates. */ diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 968e5461a..61440ddcb 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,7 +3,6 @@ export { PackagerConstructor, Variant, applyVariantToBabelConfig, - applyVariantToTemplateCompiler, getAppMeta, getPackagerCacheDir, } from './packager'; @@ -12,7 +11,6 @@ export { Resolver } from './resolver'; export { default as Stage } from './stage'; export { NodeTemplateCompiler, NodeTemplateCompilerParams } from './template-compiler-node'; export { TemplateCompiler, TemplateCompilerParams } from './template-compiler-common'; -export { templateCompilerModule } from './write-template-compiler'; export { Plugins as TemplateCompilerPlugins } from './ember-template-compiler-types'; export { Asset, EmberAsset, ImplicitAssetPaths } from './asset'; export { default as Options, optionsWithDefaults } from './options'; diff --git a/packages/core/src/packager.ts b/packages/core/src/packager.ts index 9758c8263..0f4a74bd2 100644 --- a/packages/core/src/packager.ts +++ b/packages/core/src/packager.ts @@ -89,12 +89,6 @@ export function applyVariantToBabelConfig(variant: Variant, babelConfig: any) { return babelConfig; } -export function applyVariantToTemplateCompiler(_variant: Variant, templateCompiler: any) { - // TODO: we don't actually consume the variant in the template macros yet, but - // Packagers must call this function anyway because we will. - return templateCompiler; -} - /** * Get the app meta-data for a package */ diff --git a/packages/core/src/template-compiler-common.ts b/packages/core/src/template-compiler-common.ts index f12d3b6a4..e8c76bd6e 100644 --- a/packages/core/src/template-compiler-common.ts +++ b/packages/core/src/template-compiler-common.ts @@ -4,7 +4,6 @@ import { join, sep } from 'path'; import type { PluginItem } from '@babel/core'; import { Memoize } from 'typescript-memoize'; import wrapLegacyHbsPluginIfNeeded from 'wrap-legacy-hbs-plugin-if-needed'; -import jsStringEscape from 'js-string-escape'; export interface Plugins { ast?: unknown[]; @@ -91,10 +90,6 @@ export class TemplateCompiler { this.resolver = params.resolver; this.EmberENV = params.EmberENV; this.plugins = params.plugins; - - // stage3 packagers don't need to know about our instance, they can just - // grab the compile function and use it. - this.compile = this.compile.bind(this); } private get syntax(): GlimmerSyntax { @@ -163,13 +158,6 @@ export class TemplateCompiler { return { compiled, dependencies }; } - // Compiles all the way from a template string to a javascript module string. - compile(_moduleName: string, contents: string) { - return [`import { hbs } from 'ember-cli-htmlbars';`, `export default hbs("${jsStringEscape(contents)}")`].join( - '\n' - ); - } - // Applies all custom AST transforms and emits the results still as // handlebars. applyTransforms(moduleName: string, contents: string): string { diff --git a/packages/core/src/write-template-compiler.ts b/packages/core/src/write-template-compiler.ts deleted file mode 100644 index ce05fa16a..000000000 --- a/packages/core/src/write-template-compiler.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { resolve } from 'path'; -import { Portable, PortableHint } from './portable'; -import type { NodeTemplateCompilerParams } from './template-compiler-node'; -import jsStringEscape from 'js-string-escape'; - -export function templateCompilerModule(params: NodeTemplateCompilerParams, hints: PortableHint[]) { - let p = new Portable({ hints }); - let result = p.dehydrate(params); - return { - src: [ - `const { NodeTemplateCompiler } = require("${jsStringEscape( - resolve(__dirname, './template-compiler-node.js') - )}");`, - `const { Portable } = require("${jsStringEscape(resolve(__dirname, './portable.js'))}");`, - `let p = new Portable({ hints: ${JSON.stringify(hints, null, 2)} });`, - `module.exports = new NodeTemplateCompiler(p.hydrate(${JSON.stringify(result.value, null, 2)}))`, - ].join('\n'), - isParallelSafe: result.isParallelSafe, - }; -} diff --git a/packages/hbs-loader/src/index.ts b/packages/hbs-loader/src/index.ts index 9cc8b0047..d3b74790d 100644 --- a/packages/hbs-loader/src/index.ts +++ b/packages/hbs-loader/src/index.ts @@ -1,19 +1,9 @@ -import { applyVariantToTemplateCompiler, Variant } from '@embroider/core'; import type { LoaderContext } from 'webpack'; +import { hbsToJS } from '@embroider/core'; -export interface HbsLoaderConfig { - variant: Variant; - templateCompilerFile: string; -} - -export default function hbsLoader(this: LoaderContext, templateContent: string) { - let { templateCompilerFile, variant } = this.getOptions(); - - // eslint-disable-next-line @typescript-eslint/no-require-imports - let templateCompiler = applyVariantToTemplateCompiler(variant, require(templateCompilerFile)).compile; - +export default function hbsLoader(this: LoaderContext<{}>, templateContent: string) { try { - return templateCompiler(this.resourcePath, templateContent); + return hbsToJS(templateContent); } catch (error) { error.type = 'Template Compiler Error'; error.file = this.resourcePath; diff --git a/packages/shared-internals/package.json b/packages/shared-internals/package.json index b831b234a..c31008669 100644 --- a/packages/shared-internals/package.json +++ b/packages/shared-internals/package.json @@ -31,6 +31,7 @@ "dependencies": { "babel-import-util": "^0.2.0", "ember-rfc176-data": "^0.3.17", + "js-string-escape": "^1.0.1", "resolve-package-path": "^4.0.1", "typescript-memoize": "^1.0.1", "fs-extra": "^9.1.0", @@ -39,6 +40,7 @@ }, "devDependencies": { "@embroider/test-support": "0.36.0", + "@types/js-string-escape": "^1.0.0", "@types/semver": "^7.3.6", "@types/tmp": "^0.1.0", "fixturify": "^2.1.1", diff --git a/packages/shared-internals/src/hbs-to-js.ts b/packages/shared-internals/src/hbs-to-js.ts new file mode 100644 index 000000000..abc4f83de --- /dev/null +++ b/packages/shared-internals/src/hbs-to-js.ts @@ -0,0 +1,7 @@ +import jsStringEscape from 'js-string-escape'; + +export function hbsToJS(hbsContents: string): string { + return [`import { hbs } from 'ember-cli-htmlbars';`, `export default hbs("${jsStringEscape(hbsContents)}")`].join( + '\n' + ); +} diff --git a/packages/shared-internals/src/index.ts b/packages/shared-internals/src/index.ts index f36cf2d27..245264dde 100644 --- a/packages/shared-internals/src/index.ts +++ b/packages/shared-internals/src/index.ts @@ -8,3 +8,4 @@ export { default as packageName } from './package-name'; export { default as tmpdir } from './tmpdir'; export * from './ember-cli-models'; export * from './ember-standard-modules'; +export { hbsToJS } from './hbs-to-js'; diff --git a/packages/shared-internals/src/metadata.ts b/packages/shared-internals/src/metadata.ts index 2806816d3..1075c3333 100644 --- a/packages/shared-internals/src/metadata.ts +++ b/packages/shared-internals/src/metadata.ts @@ -17,10 +17,6 @@ export interface AppMeta { }; 'resolvable-extensions': string[]; 'root-url': string; - 'template-compiler': { - filename: string; - isParallelSafe: boolean; - }; version: 2; } diff --git a/packages/webpack/src/ember-webpack.ts b/packages/webpack/src/ember-webpack.ts index 728ee34a1..0042c1bb3 100644 --- a/packages/webpack/src/ember-webpack.ts +++ b/packages/webpack/src/ember-webpack.ts @@ -33,7 +33,6 @@ import { format } from 'util'; import { warmup as threadLoaderWarmup } from 'thread-loader'; import { Options, BabelLoaderOptions } from './options'; import crypto from 'crypto'; -import type { HbsLoaderConfig } from '@embroider/hbs-loader'; import semverSatisfies from 'semver/functions/satisfies'; import supportsColor from 'supports-color'; @@ -47,7 +46,6 @@ import type { MinifyOptions } from 'terser'; interface AppInfo { entrypoints: HTMLEntrypoint[]; otherAssets: string[]; - templateCompiler: AppMeta['template-compiler']; babel: AppMeta['babel']; rootURL: AppMeta['root-url']; publicAssetURL: string; @@ -105,7 +103,6 @@ const Webpack: PackagerConstructor = class Webpack implements Packager private examineApp(): AppInfo { let meta = getAppMeta(this.pathToVanillaApp); - let templateCompiler = meta['template-compiler']; let rootURL = meta['root-url']; let babel = meta['babel']; let resolvableExtensions = meta['resolvable-extensions']; @@ -121,11 +118,11 @@ const Webpack: PackagerConstructor = class Webpack implements Packager } } - return { entrypoints, otherAssets, templateCompiler, babel, rootURL, resolvableExtensions, publicAssetURL }; + return { entrypoints, otherAssets, babel, rootURL, resolvableExtensions, publicAssetURL }; } private configureWebpack( - { entrypoints, templateCompiler, babel, resolvableExtensions, publicAssetURL }: AppInfo, + { entrypoints, babel, resolvableExtensions, publicAssetURL }: AppInfo, variant: Variant ): Configuration { let entry: { [name: string]: string } = {}; @@ -135,11 +132,6 @@ const Webpack: PackagerConstructor = class Webpack implements Packager } } - let hbsOptions: HbsLoaderConfig = { - templateCompilerFile: join(this.pathToVanillaApp, templateCompiler.filename), - variant, - }; - return { mode: variant.optimizeForProduction ? 'production' : 'development', context: this.pathToVanillaApp, @@ -169,7 +161,6 @@ const Webpack: PackagerConstructor = class Webpack implements Packager ), { loader: require.resolve('@embroider/hbs-loader'), - options: hbsOptions, }, ]), }, diff --git a/test-packages/support/build.ts b/test-packages/support/build.ts index 890d9c6dc..32d47b69b 100644 --- a/test-packages/support/build.ts +++ b/test-packages/support/build.ts @@ -12,7 +12,7 @@ import MockUI from 'console-ui/mock'; import { TransformOptions, transform } from '@babel/core'; import { Options } from '../../packages/compat/src'; import { BoundExpectFile } from './file-assertions'; -import { TemplateCompiler, AppMeta } from '@embroider/core'; +import { AppMeta, hbsToJS } from '@embroider/core'; import { Memoize } from 'typescript-memoize'; import { stableWorkspaceDir } from '@embroider/compat/src/default-pipeline'; @@ -75,13 +75,13 @@ export default class BuildResult { } async cleanup() { - await this.project.dispose(); + this.project.dispose(); await this.builder.cleanup(); } transpile(contents: string, fileAssert: BoundExpectFile): string { if (fileAssert.path.endsWith('.hbs')) { - return this.templateCompiler.compile(fileAssert.fullPath, contents); + return transform(hbsToJS(contents), Object.assign({ filename: fileAssert.fullPath }, this.babelConfig))!.code!; } else if (fileAssert.path.endsWith('.js')) { return transform(contents, Object.assign({ filename: fileAssert.fullPath }, this.babelConfig))!.code!; } else { @@ -116,12 +116,6 @@ export default class BuildResult { return this.pkgJSON['ember-addon'] as AppMeta; } - @Memoize() - private get templateCompiler() { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require(join(this.outputPath, this.emberMeta['template-compiler'].filename)) as TemplateCompiler; - } - @Memoize() private get babelConfig() { if (this.emberMeta['babel'].majorVersion !== 7) { From 2cba998e83dd77aa356bbef038ec54a0c8d70a49 Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Tue, 23 Nov 2021 15:10:50 -0500 Subject: [PATCH 3/8] progress on updating audit test --- packages/compat/tests/audit.test.ts | 49 ++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/compat/tests/audit.test.ts b/packages/compat/tests/audit.test.ts index 20a25e166..ad656d1d4 100644 --- a/packages/compat/tests/audit.test.ts +++ b/packages/compat/tests/audit.test.ts @@ -1,8 +1,12 @@ -import { Project } from '@embroider/test-support'; +import { emberTemplateCompilerPath, Project } from '@embroider/test-support'; import { AppMeta, throwOnWarnings } from '@embroider/core'; import merge from 'lodash/merge'; import fromPairs from 'lodash/fromPairs'; import { Audit, Finding } from '../src/audit'; +import CompatResolver from '../src/resolver'; +import { join } from 'path'; +import type { TransformOptions } from '@babel/core'; +import type { Options as InlinePrecompileOptions } from 'babel-plugin-ember-template-compilation'; describe('audit', function () { throwOnWarnings(); @@ -20,14 +24,49 @@ describe('audit', function () { const resolvableExtensions = ['.js', '.hbs']; + let templateCompilerParams = { + compilerPath: emberTemplateCompilerPath(), + compilerChecksum: `mock-compiler-checksum${Math.random()}`, + EmberENV: {}, + plugins: { ast: [] }, + resolver: new CompatResolver({ + root: app.baseDir, + modulePrefix: 'audit-this-app', + options: { staticComponents: false, staticHelpers: false, allowUnsafeDynamicComponents: false }, + activePackageRules: [], + adjustImportsOptions: { + renamePackages: {}, + renameModules: {}, + extraImports: [], + externalsDir: '/tmp/embroider-externals', + activeAddons: {}, + relocatedFiles: {}, + resolvableExtensions, + emberNeedsModulesPolyfill: true, + }, + }), + }; + let babel: TransformOptions = { + babelrc: false, + plugins: [], + }; + + babel.plugins!.push([ + join(__dirname, '../src/babel-plugin-inline-hbs-deps-node.js'), + { templateCompiler: templateCompilerParams }, + ]); + babel.plugins!.push([ + require.resolve('babel-plugin-ember-template-compilation'), + { + precompilerPath: join(__dirname, '../src/babel-plugin-inline-hbs-deps-node.js'), + } as InlinePrecompileOptions, + ]); + merge(app.files, { 'index.html': ``, 'app.js': `import Hello from './hello.hbs';`, 'hello.hbs': ``, - 'babel_config.js': `module.exports = { - babelrc: false, - plugins: [], - }`, + 'babel_config.js': `module.exports = ${JSON.stringify(babel)}`, }); let appMeta: AppMeta = { type: 'app', From d857257eeb86b0fd9e6d4fde2aa3da0c08d871af Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Tue, 15 Mar 2022 16:33:57 -0400 Subject: [PATCH 4/8] clarifying type only imports --- packages/core/src/babel-plugin-inline-hbs-deps.ts | 2 +- packages/core/src/babel-plugin-inline-hbs.ts | 2 +- packages/core/src/babel-plugin-stage1-inline-hbs.ts | 2 +- packages/core/src/resolver.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/babel-plugin-inline-hbs-deps.ts b/packages/core/src/babel-plugin-inline-hbs-deps.ts index 6eb6f7dd9..4b583a1f1 100644 --- a/packages/core/src/babel-plugin-inline-hbs-deps.ts +++ b/packages/core/src/babel-plugin-inline-hbs-deps.ts @@ -2,7 +2,7 @@ import type { NodePath } from '@babel/traverse'; import type * as Babel from '@babel/core'; import type { types as t } from '@babel/core'; import { join } from 'path'; -import { TemplateCompiler } from './template-compiler-common'; +import type { TemplateCompiler } from './template-compiler-common'; import { ResolvedDep } from './resolver'; import { templateCompilationModules } from '@embroider/shared-internals'; import { ImportUtil } from 'babel-import-util'; diff --git a/packages/core/src/babel-plugin-inline-hbs.ts b/packages/core/src/babel-plugin-inline-hbs.ts index b74d7c75a..72ebe03e3 100644 --- a/packages/core/src/babel-plugin-inline-hbs.ts +++ b/packages/core/src/babel-plugin-inline-hbs.ts @@ -9,7 +9,7 @@ import type { types as t } from '@babel/core'; import type * as Babel from '@babel/core'; import type { NodePath } from '@babel/traverse'; import { join } from 'path'; -import { TemplateCompiler } from './template-compiler-common'; +import type { TemplateCompiler } from './template-compiler-common'; import { parse } from '@babel/core'; import { ResolvedDep } from './resolver'; import { ImportUtil } from 'babel-import-util'; diff --git a/packages/core/src/babel-plugin-stage1-inline-hbs.ts b/packages/core/src/babel-plugin-stage1-inline-hbs.ts index 7b713c9a5..3d6380af8 100644 --- a/packages/core/src/babel-plugin-stage1-inline-hbs.ts +++ b/packages/core/src/babel-plugin-stage1-inline-hbs.ts @@ -4,7 +4,7 @@ */ import { join } from 'path'; -import { TemplateCompiler } from './template-compiler-common'; +import type { TemplateCompiler } from './template-compiler-common'; import type { NodePath } from '@babel/traverse'; import type * as Babel from '@babel/core'; import type { types as t } from '@babel/core'; diff --git a/packages/core/src/resolver.ts b/packages/core/src/resolver.ts index 8137689de..984dd5534 100644 --- a/packages/core/src/resolver.ts +++ b/packages/core/src/resolver.ts @@ -1,4 +1,4 @@ -import { TemplateCompiler } from './template-compiler-common'; +import type { TemplateCompiler } from './template-compiler-common'; import { Options } from './babel-plugin-adjust-imports'; export interface ResolvedDep { From 14e8604cb557c6461e11fbb23a7ecdb6b9bc1c3b Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Wed, 16 Mar 2022 17:07:41 -0400 Subject: [PATCH 5/8] cleaning up duplication This code was duplicated *and* doesn't really belong on TemplateCompiler anyway --- packages/compat/src/detect-babel-plugins.ts | 38 +++++++++++++++++ packages/compat/src/v1-addon.ts | 9 +++- packages/compat/src/v1-app.ts | 5 +-- .../tests/inline-precompile-detect.test.ts | 31 ++++++++++++++ packages/core/src/template-compiler-common.ts | 41 +------------------ packages/core/src/template-compiler-node.ts | 25 +---------- .../tests/template-compiler-common.test.ts | 31 -------------- 7 files changed, 80 insertions(+), 100 deletions(-) create mode 100644 packages/compat/tests/inline-precompile-detect.test.ts delete mode 100644 packages/core/tests/template-compiler-common.test.ts diff --git a/packages/compat/src/detect-babel-plugins.ts b/packages/compat/src/detect-babel-plugins.ts index 4e596811a..dbcd663e7 100644 --- a/packages/compat/src/detect-babel-plugins.ts +++ b/packages/compat/src/detect-babel-plugins.ts @@ -39,3 +39,41 @@ export function isColocationPlugin(item: PluginItem): boolean { return pluginPath.includes(join('ember-cli-htmlbars', 'lib', 'colocated-babel-plugin', sep)); } + +// tests for the classic ember-cli-htmlbars-inline-precompile babel plugin +export function isInlinePrecompilePlugin(item: PluginItem) { + if (typeof item === 'string') { + return matchesSourceFile(item); + } + if (hasProperties(item) && (item as any)._parallelBabel) { + return matchesSourceFile((item as any)._parallelBabel.requireFile); + } + if (Array.isArray(item) && item.length > 0) { + if (typeof item[0] === 'string') { + return matchesSourceFile(item[0]); + } + if (hasProperties(item[0]) && (item[0] as any)._parallelBabel) { + return matchesSourceFile((item[0] as any)._parallelBabel.requireFile); + } + } + return false; +} + +function matchesSourceFile(filename: string) { + return Boolean(htmlbarPathMatches.find(match => filename.endsWith(match))); +} + +function hasProperties(item: any) { + return item && (typeof item === 'object' || typeof item === 'function'); +} + +const htmlbarPathMatches = [ + ['htmlbars-inline-precompile', 'index.js'].join(sep), + ['htmlbars-inline-precompile', 'lib', 'require-from-worker.js'].join(sep), + ['htmlbars-inline-precompile', 'index'].join(sep), + ['htmlbars-inline-precompile', 'lib', 'require-from-worker'].join(sep), + ['ember-cli-htmlbars', 'index.js'].join(sep), + ['ember-cli-htmlbars', 'lib', 'require-from-worker.js'].join(sep), + ['ember-cli-htmlbars', 'index'].join(sep), + ['ember-cli-htmlbars', 'lib', 'require-from-worker'].join(sep), +]; diff --git a/packages/compat/src/v1-addon.ts b/packages/compat/src/v1-addon.ts index 2d5d53e8c..c7b69ef9d 100644 --- a/packages/compat/src/v1-addon.ts +++ b/packages/compat/src/v1-addon.ts @@ -31,7 +31,12 @@ import V1App from './v1-app'; import modulesCompat from './modules-compat'; import writeFile from 'broccoli-file-creator'; import SynthesizeTemplateOnlyComponents from './synthesize-template-only-components'; -import { isEmberAutoImportDynamic, isCompactReexports, isColocationPlugin } from './detect-babel-plugins'; +import { + isEmberAutoImportDynamic, + isCompactReexports, + isColocationPlugin, + isInlinePrecompilePlugin, +} from './detect-babel-plugins'; import { ResolvedDep } from '@embroider/core/src/resolver'; import TemplateCompilerBroccoliPlugin from './template-compiler-broccoli-plugin'; import { fromPairs } from 'lodash'; @@ -1089,7 +1094,7 @@ function babelPluginAllowedInStage1(plugin: PluginItem) { return false; } - if (NodeTemplateCompiler.isInlinePrecompilePlugin(plugin)) { + if (isInlinePrecompilePlugin(plugin)) { // Similarly, the inline precompile plugin must not run in stage1. We // want all templates uncompiled. Instead, we will be adding our own // plugin that only runs custom AST transforms inside inline diff --git a/packages/compat/src/v1-app.ts b/packages/compat/src/v1-app.ts index 5fe07ed87..1d12d2396 100644 --- a/packages/compat/src/v1-app.ts +++ b/packages/compat/src/v1-app.ts @@ -9,7 +9,6 @@ import { Node } from 'broccoli-node-api'; import { V1Config, WriteV1Config } from './v1-config'; import { WriteV1AppBoot, ReadV1AppBoot } from './v1-appboot'; import { - TemplateCompiler, TemplateCompilerPlugins, AddonMeta, Package, @@ -27,7 +26,7 @@ import resolvePackagePath from 'resolve-package-path'; import Concat from 'broccoli-concat'; import mapKeys from 'lodash/mapKeys'; import SynthesizeTemplateOnlyComponents from './synthesize-template-only-components'; -import { isEmberAutoImportDynamic } from './detect-babel-plugins'; +import { isEmberAutoImportDynamic, isInlinePrecompilePlugin } from './detect-babel-plugins'; import prepHtmlbarsAstPluginsForUnwrap from './prepare-htmlbars-ast-plugins'; import { readFileSync } from 'fs'; import type { Options as HTMLBarsOptions } from 'ember-cli-htmlbars'; @@ -262,7 +261,7 @@ export default class V1App { // always-installed version of that (v2 addons are allowed to assume it // will be present in the final app build, the app doesn't get to turn // that off or configure it.) - !TemplateCompiler.isInlinePrecompilePlugin(p) && + !isInlinePrecompilePlugin(p) && !isEmberAutoImportDynamic(p) ); }); diff --git a/packages/compat/tests/inline-precompile-detect.test.ts b/packages/compat/tests/inline-precompile-detect.test.ts new file mode 100644 index 000000000..f46a3a6c8 --- /dev/null +++ b/packages/compat/tests/inline-precompile-detect.test.ts @@ -0,0 +1,31 @@ +import { isInlinePrecompilePlugin } from '../src/detect-babel-plugins'; + +describe('isInlinePrecompilePlugin', () => { + test('that matchesSourceFile correctly matches paths for both Windows and Unix', () => { + expect(isInlinePrecompilePlugin('/htmlbars-inline-precompile/index.js')).toBeTruthy; + expect(isInlinePrecompilePlugin('/htmlbars-inline-precompile/index')).toBeTruthy; + expect(isInlinePrecompilePlugin('/htmlbars-inline-precompile/lib/require-from-worker.js')).toBeTruthy; + expect(isInlinePrecompilePlugin('/htmlbars-inline-precompile/lib/require-from-worker')).toBeTruthy; + + expect(isInlinePrecompilePlugin('/ember-cli-htmlbars/index.js')).toBeTruthy; + expect(isInlinePrecompilePlugin('/ember-cli-htmlbars/index')).toBeTruthy; + expect(isInlinePrecompilePlugin('/ember-cli-htmlbars/lib/require-from-worker.js')).toBeTruthy; + expect(isInlinePrecompilePlugin('/ember-cli-htmlbars/lib/require-from-worker')).toBeTruthy; + + // Windows paths + expect(isInlinePrecompilePlugin('\\htmlbars-inline-precompile\\index.js')).toBeTruthy; + expect(isInlinePrecompilePlugin('\\htmlbars-inline-precompile\\index')).toBeTruthy; + expect(isInlinePrecompilePlugin('\\htmlbars-inline-precompile\\lib\\require-from-worker.js')).toBeTruthy; + expect(isInlinePrecompilePlugin('\\htmlbars-inline-precompile\\lib\\require-from-worker')).toBeTruthy; + + expect(isInlinePrecompilePlugin('\\ember-cli-htmlbars\\index.js')).toBeTruthy; + expect(isInlinePrecompilePlugin('\\ember-cli-htmlbars\\index')).toBeTruthy; + expect(isInlinePrecompilePlugin('\\ember-cli-htmlbars\\lib\\require-from-worker.js')).toBeTruthy; + expect(isInlinePrecompilePlugin('\\ember-cli-htmlbars\\lib\\require-from-worker')).toBeTruthy; + + expect(isInlinePrecompilePlugin('/ember-cli-htmlbars/')).toBeFalsy; + expect(isInlinePrecompilePlugin('/htmlbars-inline-precompile/')).toBeFalsy; + expect(isInlinePrecompilePlugin('')).toBeFalsy; + expect(isInlinePrecompilePlugin('badstring')).toBeFalsy; + }); +}); diff --git a/packages/core/src/template-compiler-common.ts b/packages/core/src/template-compiler-common.ts index e8c76bd6e..0f776e37b 100644 --- a/packages/core/src/template-compiler-common.ts +++ b/packages/core/src/template-compiler-common.ts @@ -1,7 +1,6 @@ import stripBom from 'strip-bom'; import { Resolver, ResolvedDep } from './resolver'; -import { join, sep } from 'path'; -import type { PluginItem } from '@babel/core'; +import { join } from 'path'; import { Memoize } from 'typescript-memoize'; import wrapLegacyHbsPluginIfNeeded from 'wrap-legacy-hbs-plugin-if-needed'; @@ -58,17 +57,6 @@ export interface GlimmerSyntax { _Ember: { FEATURES: any; ENV: any }; } -const htmlbarPathMatches = [ - ['htmlbars-inline-precompile', 'index.js'].join(sep), - ['htmlbars-inline-precompile', 'lib', 'require-from-worker.js'].join(sep), - ['htmlbars-inline-precompile', 'index'].join(sep), - ['htmlbars-inline-precompile', 'lib', 'require-from-worker'].join(sep), - ['ember-cli-htmlbars', 'index.js'].join(sep), - ['ember-cli-htmlbars', 'lib', 'require-from-worker.js'].join(sep), - ['ember-cli-htmlbars', 'index'].join(sep), - ['ember-cli-htmlbars', 'lib', 'require-from-worker'].join(sep), -]; - export interface TemplateCompilerParams { // this should be the exports object from ember-template-compiler.js. It's // "unknown" here because it changes shape in different ember versions, we @@ -197,33 +185,6 @@ export class TemplateCompiler { baseDir() { return join(__dirname, '..'); } - - // tests for the classic ember-cli-htmlbars-inline-precompile babel plugin - static isInlinePrecompilePlugin(item: PluginItem) { - if (typeof item === 'string') { - return matchesSourceFile(item); - } - if (hasProperties(item) && (item as any)._parallelBabel) { - return matchesSourceFile((item as any)._parallelBabel.requireFile); - } - if (Array.isArray(item) && item.length > 0) { - if (typeof item[0] === 'string') { - return matchesSourceFile(item[0]); - } - if (hasProperties(item[0]) && (item[0] as any)._parallelBabel) { - return matchesSourceFile((item[0] as any)._parallelBabel.requireFile); - } - } - return false; - } -} - -export function matchesSourceFile(filename: string) { - return Boolean(htmlbarPathMatches.find(match => filename.endsWith(match))); -} - -function hasProperties(item: any) { - return item && (typeof item === 'object' || typeof item === 'function'); } // this matches the setup done by ember-cli-htmlbars: https://git.io/JtbN6 diff --git a/packages/core/src/template-compiler-node.ts b/packages/core/src/template-compiler-node.ts index 06ef4a908..4cea81b27 100644 --- a/packages/core/src/template-compiler-node.ts +++ b/packages/core/src/template-compiler-node.ts @@ -4,7 +4,7 @@ import { PluginItem } from '@babel/core'; import type { Params as InlineBabelParams } from './babel-plugin-stage1-inline-hbs-node'; import { Plugins } from './ember-template-compiler-types'; import { getEmberExports } from './load-ember-template-compiler'; -import { TemplateCompiler, matchesSourceFile } from './template-compiler-common'; +import { TemplateCompiler } from './template-compiler-common'; export interface NodeTemplateCompilerParams { compilerPath: string; @@ -38,27 +38,4 @@ export class NodeTemplateCompiler extends TemplateCompiler { baseDir() { return join(__dirname, '..'); } - - // tests for the classic ember-cli-htmlbars-inline-precompile babel plugin - static isInlinePrecompilePlugin(item: PluginItem) { - if (typeof item === 'string') { - return matchesSourceFile(item); - } - if (hasProperties(item) && (item as any)._parallelBabel) { - return matchesSourceFile((item as any)._parallelBabel.requireFile); - } - if (Array.isArray(item) && item.length > 0) { - if (typeof item[0] === 'string') { - return matchesSourceFile(item[0]); - } - if (hasProperties(item[0]) && (item[0] as any)._parallelBabel) { - return matchesSourceFile((item[0] as any)._parallelBabel.requireFile); - } - } - return false; - } -} - -function hasProperties(item: any) { - return item && (typeof item === 'object' || typeof item === 'function'); } diff --git a/packages/core/tests/template-compiler-common.test.ts b/packages/core/tests/template-compiler-common.test.ts deleted file mode 100644 index 9720ae54c..000000000 --- a/packages/core/tests/template-compiler-common.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { matchesSourceFile } from '../src/template-compiler-common'; - -describe('template-compiler-common', () => { - test('that matchesSourceFile correctly matches paths for both Windows and Unix', () => { - expect(matchesSourceFile('/htmlbars-inline-precompile/index.js')).toBeTruthy; - expect(matchesSourceFile('/htmlbars-inline-precompile/index')).toBeTruthy; - expect(matchesSourceFile('/htmlbars-inline-precompile/lib/require-from-worker.js')).toBeTruthy; - expect(matchesSourceFile('/htmlbars-inline-precompile/lib/require-from-worker')).toBeTruthy; - - expect(matchesSourceFile('/ember-cli-htmlbars/index.js')).toBeTruthy; - expect(matchesSourceFile('/ember-cli-htmlbars/index')).toBeTruthy; - expect(matchesSourceFile('/ember-cli-htmlbars/lib/require-from-worker.js')).toBeTruthy; - expect(matchesSourceFile('/ember-cli-htmlbars/lib/require-from-worker')).toBeTruthy; - - // Windows paths - expect(matchesSourceFile('\\htmlbars-inline-precompile\\index.js')).toBeTruthy; - expect(matchesSourceFile('\\htmlbars-inline-precompile\\index')).toBeTruthy; - expect(matchesSourceFile('\\htmlbars-inline-precompile\\lib\\require-from-worker.js')).toBeTruthy; - expect(matchesSourceFile('\\htmlbars-inline-precompile\\lib\\require-from-worker')).toBeTruthy; - - expect(matchesSourceFile('\\ember-cli-htmlbars\\index.js')).toBeTruthy; - expect(matchesSourceFile('\\ember-cli-htmlbars\\index')).toBeTruthy; - expect(matchesSourceFile('\\ember-cli-htmlbars\\lib\\require-from-worker.js')).toBeTruthy; - expect(matchesSourceFile('\\ember-cli-htmlbars\\lib\\require-from-worker')).toBeTruthy; - - expect(matchesSourceFile('/ember-cli-htmlbars/')).toBeFalsy; - expect(matchesSourceFile('/htmlbars-inline-precompile/')).toBeFalsy; - expect(matchesSourceFile('')).toBeFalsy; - expect(matchesSourceFile('badstring')).toBeFalsy; - }); -}); From 4b2683555ac359c00eef63f6ba6e56db5c1a1cac Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Wed, 16 Mar 2022 18:15:11 -0400 Subject: [PATCH 6/8] starting to build syntax for scope argument --- .../core/src/babel-plugin-inline-hbs-deps.ts | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/core/src/babel-plugin-inline-hbs-deps.ts b/packages/core/src/babel-plugin-inline-hbs-deps.ts index 4b583a1f1..653462e47 100644 --- a/packages/core/src/babel-plugin-inline-hbs-deps.ts +++ b/packages/core/src/babel-plugin-inline-hbs-deps.ts @@ -121,12 +121,19 @@ export default function make(getCompiler: (opts: any) => TemplateCompiler) { throw path.buildCodeFrameError('placeholders inside a tagged template string are not supported'); } let template = path.node.quasi.quasis.map(quasi => quasi.value.cooked).join(''); + let args: t.Expression[] = [t.stringLiteral(template)]; + + let locals: t.Identifier[] = [ + // TODO: this is where lexically scoped dependencies go + ]; + let opts = precompileOpts(locals, t); + if (opts) { + args.push(opts); + } + let newCallExpression = t.callExpression( state.adder.import(path, '@ember/template-compilation', 'precompileTemplate'), - [ - t.stringLiteral(template), - // TODO: here is where we will put scope once ember support that - ] + args ); state.emittedCallExpressions.add(newCallExpression); @@ -142,6 +149,20 @@ export default function make(getCompiler: (opts: any) => TemplateCompiler) { path.replaceWith(newCallExpression); } + function precompileOpts(locals: t.Identifier[], t: typeof Babel.types) { + if (locals.length > 0) { + return t.objectExpression([ + t.objectProperty( + t.identifier('scope'), + t.arrowFunctionExpression( + [], + t.objectExpression(locals.map(name => t.objectProperty(name, name, false, true))) + ) + ), + ]); + } + } + function amdDefine(runtimeName: string, importCounter: number, t: typeof Babel.types) { return t.expressionStatement( t.callExpression(t.memberExpression(t.identifier('window'), t.identifier('define')), [ From 9a47b22999db7ccfaa2cff1d62a1300ad89cd64d Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Wed, 16 Mar 2022 18:23:52 -0400 Subject: [PATCH 7/8] fix plugin resolution in audit test --- packages/compat/tests/audit.test.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/compat/tests/audit.test.ts b/packages/compat/tests/audit.test.ts index 35133a1d4..e0725af13 100644 --- a/packages/compat/tests/audit.test.ts +++ b/packages/compat/tests/audit.test.ts @@ -4,7 +4,7 @@ import merge from 'lodash/merge'; import fromPairs from 'lodash/fromPairs'; import { Audit, Finding } from '../src/audit'; import CompatResolver from '../src/resolver'; -import { join } from 'path'; +import { dirname, join } from 'path'; import type { TransformOptions } from '@babel/core'; import type { Options as InlinePrecompileOptions } from 'babel-plugin-ember-template-compilation'; @@ -57,14 +57,16 @@ describe('audit', function () { plugins: [], }; - babel.plugins!.push([ - join(__dirname, '../src/babel-plugin-inline-hbs-deps-node.js'), - { templateCompiler: templateCompilerParams }, - ]); + let hbsDepsPluginPath = join( + dirname(require.resolve('@embroider/core/package.json')), + 'src/babel-plugin-inline-hbs-deps-node.js' + ); + + babel.plugins!.push([hbsDepsPluginPath, { templateCompiler: templateCompilerParams }]); babel.plugins!.push([ require.resolve('babel-plugin-ember-template-compilation'), { - precompilerPath: join(__dirname, '../src/babel-plugin-inline-hbs-deps-node.js'), + precompilerPath: hbsDepsPluginPath, } as InlinePrecompileOptions, ]); From c9e58b60f4aa6e36105bbd53a42df5e2ce1adaf6 Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Wed, 6 Apr 2022 22:15:29 -0400 Subject: [PATCH 8/8] updating audit tool to match newer template compilation strategy --- packages/compat/src/audit.ts | 40 +++++++++++------- packages/compat/src/resolver-transform.ts | 4 +- packages/compat/src/resolver.ts | 50 +++++++++++++---------- packages/compat/tests/audit.test.ts | 15 ++++--- 4 files changed, 66 insertions(+), 43 deletions(-) diff --git a/packages/compat/src/audit.ts b/packages/compat/src/audit.ts index 6740bf031..c08b56a8a 100644 --- a/packages/compat/src/audit.ts +++ b/packages/compat/src/audit.ts @@ -17,6 +17,7 @@ import { } from './audit/babel-visitor'; import { AuditBuildOptions, AuditOptions } from './audit/options'; import { buildApp, BuildError, isBuildError } from './audit/build'; +import { AuditMessage } from './resolver'; const { JSDOM } = jsdom; @@ -244,10 +245,6 @@ export class Audit { config = Object.assign({}, config); config.plugins = config.plugins.filter((p: any) => !isMacrosPlugin(p)); - // TODO: find the template compiler params inside the babel config and - // maniuplate them to turn on the resolver's enableAuditMode. Or create some - // other out-of-band way to do that. - config.ast = true; return config; } @@ -312,16 +309,31 @@ export class Audit { } async run(): Promise { - this.debug(`meta`, this.meta); - for (let asset of this.meta.assets) { - if (asset.endsWith('.html')) { - this.scheduleVisit(resolvePath(this.appDir, asset), { isRoot: true }); + (globalThis as any).embroider_audit = this.handleResolverError.bind(this); + + try { + this.debug(`meta`, this.meta); + for (let asset of this.meta.assets) { + if (asset.endsWith('.html')) { + this.scheduleVisit(resolvePath(this.appDir, asset), { isRoot: true }); + } } + await this.drainQueue(); + this.linkModules(); + this.inspectModules(); + return AuditResults.create(this.appDir, this.findings, this.modules); + } finally { + delete (globalThis as any).embroider_audit; } - await this.drainQueue(); - this.linkModules(); - this.inspectModules(); - return AuditResults.create(this.appDir, this.findings, this.modules); + } + + private handleResolverError(msg: AuditMessage) { + this.pushFinding({ + message: msg.message, + filename: msg.filename, + detail: msg.detail, + codeFrame: this.frames.render(this.frames.forSource(msg.source)(msg)), + }); } private linkModules() { @@ -465,7 +477,7 @@ export class Audit { dependencies: result.imports.map(i => i.source), }; } catch (err) { - if (err.code === 'BABEL_PARSE_ERROR') { + if (['BABEL_PARSE_ERROR', 'BABEL_TRANSFORM_ERROR'].includes(err.code)) { return [ { filename, @@ -509,7 +521,7 @@ export class Audit { } private async resolve(specifier: string, fromPath: string): Promise { - if (specifier === '@embroider/macros') { + if (['@embroider/macros', '@ember/template-factory'].includes(specifier)) { return; } try { diff --git a/packages/compat/src/resolver-transform.ts b/packages/compat/src/resolver-transform.ts index 34f084cb4..9044e5a86 100644 --- a/packages/compat/src/resolver-transform.ts +++ b/packages/compat/src/resolver-transform.ts @@ -4,8 +4,8 @@ import type { ASTv1 } from '@glimmer/syntax'; // This is the AST transform that resolves components, helpers and modifiers at build time // and puts them into `dependencies`. export function makeResolverTransform(resolver: Resolver) { - function resolverTransform({ filename }: { filename: string }) { - resolver.enter(filename); + function resolverTransform({ filename, contents }: { filename: string; contents: string }) { + resolver.enter(filename, contents); let scopeStack = new ScopeStack(); diff --git a/packages/compat/src/resolver.ts b/packages/compat/src/resolver.ts index 0cd0cd168..c8cd54040 100644 --- a/packages/compat/src/resolver.ts +++ b/packages/compat/src/resolver.ts @@ -151,10 +151,19 @@ export function rehydrate(params: RehydrationParams) { return new CompatResolver(params); } +export interface AuditMessage { + message: string; + detail: string; + loc: Loc; + source: string; + filename: string; +} + export default class CompatResolver implements Resolver { private dependencies: Map = new Map(); private templateCompiler: TemplateCompiler | undefined; - private auditMode = false; + private auditHandler: undefined | ((msg: AuditMessage) => void); + private currentContents: string | undefined; _parallelBabel: { requireFile: string; @@ -169,9 +178,12 @@ export default class CompatResolver implements Resolver { buildUsing: 'rehydrate', params, }; + if ((globalThis as any).embroider_audit) { + this.auditHandler = (globalThis as any).embroider_audit; + } } - enter(moduleName: string) { + enter(moduleName: string, contents: string) { let rules = this.findComponentRules(moduleName); let deps: Resolution[]; if (rules?.dependsOnComponents) { @@ -180,6 +192,7 @@ export default class CompatResolver implements Resolver { deps = []; } this.dependencies.set(moduleName, deps); + this.currentContents = contents; } private add(resolution: Resolution, from: string) { @@ -351,29 +364,13 @@ export default class CompatResolver implements Resolver { } } - // called by our audit tool. Forces staticComponents, staticHelpers and staticModifiers - // to activate so we can audit their behavior, while making their errors silent - // until we can gather them up at the end of the build for the audit results. - enableAuditMode() { - this.auditMode = true; - } - - errorsIn(moduleName: string): ResolutionFail[] { - let deps = this.dependencies.get(moduleName); - if (deps) { - return deps.filter(d => d.type === 'error') as ResolutionFail[]; - } else { - return []; - } - } - dependenciesOf(moduleName: string): ResolvedDep[] { let flatDeps: Map = new Map(); let deps = this.dependencies.get(moduleName); if (deps) { for (let dep of deps) { if (dep.type === 'error') { - if (!this.auditMode && !this.params.options.allowUnsafeDynamicComponents) { + if (!this.auditHandler && !this.params.options.allowUnsafeDynamicComponents) { let e: ResolverDependencyError = new Error( `${dep.message}: ${dep.detail} in ${humanReadableFile(this.params.root, moduleName)}` ); @@ -382,6 +379,15 @@ export default class CompatResolver implements Resolver { e.moduleName = moduleName; throw e; } + if (this.auditHandler) { + this.auditHandler({ + message: dep.message, + filename: moduleName, + detail: dep.detail, + loc: dep.loc, + source: this.currentContents!, + }); + } } else { for (let entry of dep.modules) { let { runtimeName } = entry; @@ -441,15 +447,15 @@ export default class CompatResolver implements Resolver { } private get staticComponentsEnabled(): boolean { - return this.params.options.staticComponents || this.auditMode; + return this.params.options.staticComponents || Boolean(this.auditHandler); } private get staticHelpersEnabled(): boolean { - return this.params.options.staticHelpers || this.auditMode; + return this.params.options.staticHelpers || Boolean(this.auditHandler); } private get staticModifiersEnabled(): boolean { - return this.params.options.staticModifiers || this.auditMode; + return this.params.options.staticModifiers || Boolean(this.auditHandler); } private tryHelper(path: string, from: string): Resolution | null { diff --git a/packages/compat/tests/audit.test.ts b/packages/compat/tests/audit.test.ts index e0725af13..429f7b518 100644 --- a/packages/compat/tests/audit.test.ts +++ b/packages/compat/tests/audit.test.ts @@ -1,5 +1,5 @@ import { emberTemplateCompilerPath, Project } from '@embroider/test-support'; -import { AppMeta, throwOnWarnings } from '@embroider/core'; +import { AppMeta, NodeTemplateCompilerParams, throwOnWarnings } from '@embroider/core'; import merge from 'lodash/merge'; import fromPairs from 'lodash/fromPairs'; import { Audit, Finding } from '../src/audit'; @@ -7,6 +7,7 @@ import CompatResolver from '../src/resolver'; import { dirname, join } from 'path'; import type { TransformOptions } from '@babel/core'; import type { Options as InlinePrecompileOptions } from 'babel-plugin-ember-template-compilation'; +import { makePortable } from '@embroider/core/src/portable-babel-config'; describe('audit', function () { throwOnWarnings(); @@ -24,7 +25,7 @@ describe('audit', function () { const resolvableExtensions = ['.js', '.hbs']; - let templateCompilerParams = { + let templateCompilerParams: NodeTemplateCompilerParams = { compilerPath: emberTemplateCompilerPath(), compilerChecksum: `mock-compiler-checksum${Math.random()}`, EmberENV: {}, @@ -74,7 +75,11 @@ describe('audit', function () { 'index.html': ``, 'app.js': `import Hello from './hello.hbs';`, 'hello.hbs': ``, - 'babel_config.js': `module.exports = ${JSON.stringify(babel)}`, + 'babel_config.js': `module.exports = ${JSON.stringify( + makePortable(babel, { basedir: '.' }, []).config, + null, + 2 + )}`, }); let appMeta: AppMeta = { type: 'app', @@ -337,7 +342,7 @@ describe('audit', function () { }, ]); expect(result.findings[0].codeFrame).toBeDefined(); - expect(Object.keys(result.modules).length).toBe(3); + expect(Object.keys(result.modules).length).toBe(2); }); test('traverse through template even when it has some errors', async function () { @@ -379,7 +384,7 @@ describe('audit', function () { }); let result = await audit(); expect(result.findings.map(f => ({ filename: f.filename, message: f.message }))).toEqual([ - { filename: './hello.hbs', message: 'failed to compile template' }, + { filename: './hello.hbs', message: 'failed to parse' }, ]); expect(Object.keys(result.modules).length).toBe(3); });