diff --git a/README.md b/README.md
index 6b9de7d..d70cefd 100644
--- a/README.md
+++ b/README.md
@@ -77,6 +77,7 @@ interface NodeOptions extends Options {
// Options handling rules:
//
// - we add `content`, which is the original string form of the template
+ // - we add `meta.jsutils`, which gives AST transform plugins access to methods for manipulating the surrounding javascript scope. See "JSUtils" below.
// - we have special parsing for `scope` which becomes `locals` when passed
// to your precompile
// - anything else the user passes to `precompileTemplate` will be passed
@@ -98,6 +99,45 @@ import * as babel from '@babel/core';
babel.transform(someCode, { plugins: [makePlugin(loadTemplateCompiler)] });
```
+# JSUtils: Manipulating Javascript from within AST transforms
+
+AST transforms are plugins for modifying HBS templates at build time. Because those templates are embedded in Javascript and can access the Javascript scope, an AST plugin may want to introduce some new things into Javascript scope. That is what the JSUtils API is for.
+
+Your AST transform can access the JSUtils API via `env.meta.jsutils`. Here's an example transform.
+
+```js
+function myAstTransform(env) {
+ return {
+ name: 'my-ast-transform',
+ visitor: {
+ PathExpression(node, path) {
+ if (node.original === 'onePlusOne') {
+ let name = env.meta.jsutils.bindExpression('1+1', path, { nameHint: 'two' });
+ return env.syntax.builders.path(name);
+ }
+ },
+ },
+ };
+}
+```
+
+The example transform above would rewrite:
+
+```js
+import { precompileTemplate } from '@ember/template-compilation';
+precompileTemplate('>');
+```
+
+To:
+
+```js
+import { precompileTemplate } from '@ember/template-compilation';
+let two = 1 + 1;
+precompileTemplate('', { scope: () => ({ two }) });
+```
+
+See the jsdoc comments in js-utils.js for details on the methods available.
+
# Acknowledgement / History
This repo derives from https://github.com/ember-cli/babel-plugin-htmlbars-inline-precompile
diff --git a/__tests__/mock-precompile.ts b/__tests__/mock-precompile.ts
index 22e60fb..aa59740 100644
--- a/__tests__/mock-precompile.ts
+++ b/__tests__/mock-precompile.ts
@@ -1,3 +1,7 @@
export function precompile(value: string) {
return `precompiledFromPath(${value})`;
}
+
+export function _preprocess(...args: unknown[]) {
+ return args;
+}
diff --git a/__tests__/tests.ts b/__tests__/tests.ts
index 43f80c3..482fdfb 100644
--- a/__tests__/tests.ts
+++ b/__tests__/tests.ts
@@ -5,12 +5,14 @@ import TransformTemplateLiterals from '@babel/plugin-transform-template-literals
import TransformModules from '@babel/plugin-transform-modules-amd';
import TransformUnicodeEscapes from '@babel/plugin-transform-unicode-escapes';
import { stripIndent } from 'common-tags';
+import { EmberTemplateCompiler } from '../src/ember-template-compiler';
+import sinon from 'sinon';
+import { ExtendedPluginBuilder } from '../src/js-utils';
describe('htmlbars-inline-precompile', function () {
- let precompile: NonNullable;
- let plugins: any[];
- let optionsReceived: any;
- let buildOptions: (o?: Partial) => Options;
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ let compiler: EmberTemplateCompiler = { ...require('ember-source/dist/ember-template-compiler') };
+ let plugins: ([typeof HTMLBarsInlinePrecompile, Options] | [unknown])[];
function transform(code: string) {
let x = babel
@@ -23,29 +25,17 @@ describe('htmlbars-inline-precompile', function () {
}
beforeEach(function () {
- optionsReceived = undefined;
- precompile = (template, options) => {
- optionsReceived = options;
- return `"precompiled(${template})"`;
- };
-
- buildOptions = function (o?: Partial): Options {
- let defaultOptions: Options = {
- precompile(...args: Parameters) {
- return precompile(...args);
- },
- };
-
- return Object.assign({}, defaultOptions, o);
- };
+ plugins = [[HTMLBarsInlinePrecompile, { compiler }]];
+ });
- plugins = [[HTMLBarsInlinePrecompile, buildOptions()]];
+ afterEach(function () {
+ sinon.restore();
});
it('supports compilation that returns a non-JSON.parseable object', function () {
- precompile = (template) => {
+ sinon.replace(compiler, 'precompile', (template) => {
return `function() { return "${template}"; }`;
- };
+ });
let transpiled = transform(
"import { precompileTemplate } from '@ember/template-compilation';\nvar compiled = precompileTemplate('hello');"
@@ -64,12 +54,7 @@ describe('htmlbars-inline-precompile', function () {
});
it('supports compilation with templateCompilerPath', function () {
- plugins = [
- [
- HTMLBarsInlinePrecompile,
- buildOptions({ precompilerPath: require.resolve('./mock-precompile') }),
- ],
- ];
+ plugins = [[HTMLBarsInlinePrecompile, { compilerPath: require.resolve('./mock-precompile') }]];
let transpiled = transform(
"import { precompileTemplate } from '@ember/template-compilation';\nvar compiled = precompileTemplate('hello');"
@@ -87,37 +72,35 @@ describe('htmlbars-inline-precompile', function () {
it('passes options when used as a call expression', function () {
let source = 'hello';
+ let spy = sinon.spy(compiler, 'precompile');
+
transform(
`import { precompileTemplate } from '@ember/template-compilation';\nvar compiled = precompileTemplate('${source}');`
);
- expect(optionsReceived).toEqual({
- contents: source,
- });
+ expect(spy.firstCall.lastArg).toHaveProperty('contents', source);
});
it('uses the user provided isProduction option if present', function () {
let source = 'hello';
+ let spy = sinon.spy(compiler, 'precompile');
transform(
`import { precompileTemplate } from '@ember/template-compilation';\nvar compiled = precompileTemplate('${source}', { isProduction: true });`
);
- expect(optionsReceived).toEqual({
- contents: source,
- isProduction: true,
- });
+ expect(spy.firstCall.lastArg).toHaveProperty('isProduction', true);
});
it('allows a template string literal when used as a call expression', function () {
let source = 'hello';
+ let spy = sinon.spy(compiler, 'precompile');
+
transform(
`import { precompileTemplate } from '@ember/template-compilation';\nvar compiled = precompileTemplate(\`${source}\`);`
);
- expect(optionsReceived).toEqual({
- contents: source,
- });
+ expect(spy.firstCall.lastArg).toHaveProperty('contents', source);
});
it('errors when the template string contains placeholders', function () {
@@ -132,9 +115,10 @@ describe('htmlbars-inline-precompile', function () {
plugins = [
[
HTMLBarsInlinePrecompile,
- buildOptions({
+ {
+ compiler,
enableLegacyModules: ['htmlbars-inline-precompile'],
- }),
+ },
],
];
expect(() =>
@@ -144,25 +128,26 @@ describe('htmlbars-inline-precompile', function () {
it('allows static userland options when used as a call expression', function () {
let source = 'hello';
+ let spy = sinon.spy(compiler, 'precompile');
+
transform(
`import { precompileTemplate } from '@ember/template-compilation';\nvar compiled = precompileTemplate('${source}', { parseOptions: { srcName: 'bar.hbs' }, moduleName: 'foo/bar.hbs', xyz: 123, qux: true, stringifiedThing: ${JSON.stringify(
{ foo: 'baz' }
)}});`
);
- expect(optionsReceived).toEqual({
- contents: source,
- parseOptions: { srcName: 'bar.hbs' },
- moduleName: 'foo/bar.hbs',
- xyz: 123,
- qux: true,
- stringifiedThing: {
- foo: 'baz',
- },
- });
+ expect(spy.firstCall.lastArg).toHaveProperty('parseOptions', { srcName: 'bar.hbs' });
+ expect(spy.firstCall.lastArg).toHaveProperty('moduleName', 'foo/bar.hbs');
+ expect(spy.firstCall.lastArg).toHaveProperty('xyz', 123);
+ expect(spy.firstCall.lastArg).toHaveProperty('qux', true);
+ expect(spy.firstCall.lastArg).toHaveProperty('stringifiedThing', { foo: 'baz' });
});
it('adds a comment with the original template string', function () {
+ sinon.replace(compiler, 'precompile', (template) => {
+ return `precompiled("${template}")`;
+ });
+
let transformed = transform(stripIndent`
import { precompileTemplate } from '@ember/template-compilation';
if ('foo') {
@@ -178,15 +163,13 @@ describe('htmlbars-inline-precompile', function () {
/*
hello
*/
- "precompiled(hello)");
+ precompiled("hello"));
}
`);
});
it('avoids a build time error when passed `insertRuntimeErrors`', function () {
- precompile = () => {
- throw new Error('NOOOOOOOOOOOOOOOOOOOOOO');
- };
+ sinon.stub(compiler, 'precompile').throws(new Error('NOOOOOOOOOOOOOOOOOOOOOO'));
let transformed = transform(
`import { precompileTemplate } from '@ember/template-compilation';\nvar compiled = precompileTemplate('hello', { insertRuntimeErrors: true });`
@@ -201,14 +184,13 @@ describe('htmlbars-inline-precompile', function () {
it('escapes any */ included in the template string', function () {
plugins = [
- [
- HTMLBarsInlinePrecompile,
- buildOptions({
- enableLegacyModules: ['htmlbars-inline-precompile'],
- }),
- ],
+ [HTMLBarsInlinePrecompile, { compiler, enableLegacyModules: ['htmlbars-inline-precompile'] }],
];
+ sinon.replace(compiler, 'precompile', (template) => {
+ return `precompiled("${template}")`;
+ });
+
let transformed = transform(stripIndent`
import hbs from 'htmlbars-inline-precompile';
if ('foo') {
@@ -216,35 +198,30 @@ describe('htmlbars-inline-precompile', function () {
}
`);
- expect(transformed).toEqual(stripIndent`
- import { createTemplateFactory } from "@ember/template-factory";
+ expect(transformed).toMatchInlineSnapshot(`
+ "import { createTemplateFactory } from \\"@ember/template-factory\\";
if ('foo') {
const template = createTemplateFactory(
/*
- hello *\\/
+ hello *\\\\/
*/
- "precompiled(hello */)");
- }
+ precompiled(\\"hello */\\"));
+ }"
`);
});
it('passes options when used as a tagged template string', function () {
plugins = [
- [
- HTMLBarsInlinePrecompile,
- buildOptions({
- enableLegacyModules: ['htmlbars-inline-precompile'],
- }),
- ],
+ [HTMLBarsInlinePrecompile, { compiler, enableLegacyModules: ['htmlbars-inline-precompile'] }],
];
let source = 'hello';
+ let spy = sinon.spy(compiler, 'precompile');
+
transform(`import hbs from 'htmlbars-inline-precompile';\nvar compiled = hbs\`${source}\`;`);
- expect(optionsReceived).toEqual({
- contents: source,
- });
+ expect(spy.firstCall.lastArg).toHaveProperty('contents', source);
});
it("strips import statement for '@ember/template-precompilation' module", function () {
@@ -257,12 +234,17 @@ describe('htmlbars-inline-precompile', function () {
});
it('replaces tagged template expressions with precompiled version', function () {
+ sinon.replace(compiler, 'precompile', (template) => {
+ return `precompiled("${template}")`;
+ });
+
plugins = [
[
HTMLBarsInlinePrecompile,
- buildOptions({
+ {
+ compiler,
enableLegacyModules: ['htmlbars-inline-precompile'],
- }),
+ },
],
];
let transformed = transform(
@@ -275,17 +257,22 @@ describe('htmlbars-inline-precompile', function () {
/*
hello
*/
- \\"precompiled(hello)\\");"
+ precompiled(\\"hello\\"));"
`);
});
it('replaces tagged template expressions with precompiled version when ember-cli-htmlbars is enabled', function () {
+ sinon.replace(compiler, 'precompile', (template) => {
+ return `precompiled("${template}")`;
+ });
+
plugins = [
[
HTMLBarsInlinePrecompile,
- buildOptions({
+ {
+ compiler,
enableLegacyModules: ['ember-cli-htmlbars'],
- }),
+ },
],
];
@@ -299,7 +286,7 @@ describe('htmlbars-inline-precompile', function () {
/*
hello
*/
- \\"precompiled(hello)\\");"
+ precompiled(\\"hello\\"));"
`);
});
@@ -320,6 +307,10 @@ describe('htmlbars-inline-precompile', function () {
});
it('works with multiple imports', function () {
+ sinon.replace(compiler, 'precompile', (template) => {
+ return `precompiled("${template}")`;
+ });
+
let transformed = transform(`
import { precompileTemplate } from '@ember/template-compilation';
import { precompileTemplate as other } from '@ember/template-compilation';
@@ -333,12 +324,12 @@ describe('htmlbars-inline-precompile', function () {
/*
hello
*/
- \\"precompiled(hello)\\");
+ precompiled(\\"hello\\"));
let b = createTemplateFactory(
/*
hello
*/
- \\"precompiled(hello)\\");"
+ precompiled(\\"hello\\"));"
`);
});
@@ -364,6 +355,10 @@ describe('htmlbars-inline-precompile', function () {
});
it('works properly when used along with modules transform', function () {
+ sinon.replace(compiler, 'precompile', (template) => {
+ return `precompiled("${template}")`;
+ });
+
plugins.push([TransformModules]);
let transformed = transform(
"import { precompileTemplate } from '@ember/template-compilation';\n" +
@@ -379,17 +374,21 @@ describe('htmlbars-inline-precompile', function () {
/*
hello
*/
- \\"precompiled(hello)\\");
+ precompiled(\\"hello\\"));
var compiled2 = (0, _templateFactory.createTemplateFactory)(
/*
goodbye
*/
- \\"precompiled(goodbye)\\");
+ precompiled(\\"goodbye\\"));
});"
`);
});
it('does not error when reusing a preexisting import', function () {
+ sinon.replace(compiler, 'precompile', (template) => {
+ return `precompiled("${template}")`;
+ });
+
let transformed = transform(`
import { createTemplateFactory } from '@ember/template-factory';
import { precompileTemplate } from '@ember/template-compilation';
@@ -403,12 +402,16 @@ describe('htmlbars-inline-precompile', function () {
/*
hello
*/
- \\"precompiled(hello)\\");
+ precompiled(\\"hello\\"));
createTemplateFactory('whatever here');"
`);
});
it('works properly when used after modules transform', function () {
+ sinon.replace(compiler, 'precompile', (template) => {
+ return `precompiled("${template}")`;
+ });
+
plugins.unshift([TransformModules]);
let transformed = transform(
"import { precompileTemplate } from '@ember/template-compilation';\nvar compiled = precompileTemplate('hello');"
@@ -422,12 +425,16 @@ describe('htmlbars-inline-precompile', function () {
/*
hello
*/
- \\"precompiled(hello)\\");
+ precompiled(\\"hello\\"));
});"
`);
});
it('works properly when used along with @babel/plugin-transform-unicode-escapes', function () {
+ sinon.replace(compiler, 'precompile', (template) => {
+ return `precompiled("${template}")`;
+ });
+
plugins.push([TransformUnicodeEscapes]);
let transformed = transform(
"import { precompileTemplate } from '@ember/template-compilation';\nvar compiled = precompileTemplate('some emoji goes 💥');"
@@ -439,19 +446,24 @@ describe('htmlbars-inline-precompile', function () {
/*
some emoji goes 💥
*/
- \\"precompiled(some emoji goes 💥)\\");"
+ precompiled(\\"some emoji goes 💥\\"));"
`);
});
it('replaces tagged template expressions when before babel-plugin-transform-es2015-template-literals', function () {
+ sinon.replace(compiler, 'precompile', (template) => {
+ return `precompiled("${template}")`;
+ });
+
plugins = [
[
HTMLBarsInlinePrecompile,
- buildOptions({
+ {
+ compiler,
enableLegacyModules: ['htmlbars-inline-precompile'],
- }),
+ },
],
- TransformTemplateLiterals,
+ [TransformTemplateLiterals],
];
let transformed = transform(
@@ -464,7 +476,7 @@ describe('htmlbars-inline-precompile', function () {
/*
hello
*/
- \\"precompiled(hello)\\");"
+ precompiled(\\"hello\\"));"
`);
});
@@ -472,9 +484,10 @@ describe('htmlbars-inline-precompile', function () {
plugins = [
[
HTMLBarsInlinePrecompile,
- buildOptions({
+ {
+ compiler,
enableLegacyModules: ['htmlbars-inline-precompile'],
- }),
+ },
],
];
let transformed = transform(
@@ -489,9 +502,10 @@ describe('htmlbars-inline-precompile', function () {
plugins = [
[
HTMLBarsInlinePrecompile,
- buildOptions({
+ {
+ compiler,
enableLegacyModules: ['htmlbars-inline-precompile'],
- }),
+ },
],
];
expect(() =>
@@ -502,16 +516,21 @@ describe('htmlbars-inline-precompile', function () {
});
it('works with glimmer modules', function () {
+ sinon.replace(compiler, 'precompile', (template) => {
+ return `precompiled("${template}")`;
+ });
+
plugins = [
[
HTMLBarsInlinePrecompile,
- buildOptions({
+ {
+ compiler,
outputModuleOverrides: {
'@ember/template-factory': {
createTemplateFactory: ['createTemplateFactory', '@glimmer/core'],
},
},
- }),
+ },
],
];
@@ -520,13 +539,13 @@ describe('htmlbars-inline-precompile', function () {
const template = precompileTemplate('hello');
`);
- expect(transformed).toEqual(stripIndent`
- import { createTemplateFactory } from "@glimmer/core";
+ expect(transformed).toMatchInlineSnapshot(`
+ "import { createTemplateFactory } from \\"@glimmer/core\\";
const template = createTemplateFactory(
/*
hello
*/
- "precompiled(hello)");
+ precompiled(\\"hello\\"));"
`);
});
@@ -566,70 +585,778 @@ describe('htmlbars-inline-precompile', function () {
);
});
- describe('with ember-source', function () {
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const compiler = require('ember-source/dist/ember-template-compiler');
+ let expressionTransform: ExtendedPluginBuilder = (env) => {
+ return {
+ name: 'expression-transform',
+ visitor: {
+ PathExpression(node, path) {
+ if (node.original === 'onePlusOne') {
+ let name = env.meta.jsutils.bindExpression('1+1', path, { nameHint: 'two' });
+ return env.syntax.builders.path(name);
+ }
+ return undefined;
+ },
+ },
+ };
+ };
+
+ let importTransform: ExtendedPluginBuilder = (env) => {
+ return {
+ name: 'import-transform',
+ visitor: {
+ PathExpression(node, path) {
+ if (node.original === 'onePlusOne') {
+ let name = env.meta.jsutils.bindImport('my-library', 'default', path, {
+ nameHint: 'two',
+ });
+ return env.syntax.builders.path(name);
+ }
+ return undefined;
+ },
+ },
+ };
+ };
+
+ it('includes the original template content', function () {
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+
+ const template = precompileTemplate('hello {{firstName}}');
+ `);
+
+ expect(transformed).toContain(`hello {{firstName}}`);
+ });
+
+ it('allows AST transform to bind a JS expression', function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ { compiler, targetFormat: 'hbs', transforms: [expressionTransform] },
+ ],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ const template = precompileTemplate('');
+ `);
+
+ expect(transformed).toMatchInlineSnapshot(`
+ "import { precompileTemplate } from '@ember/template-compilation';
+ let two = 1 + 1;
+ const template = precompileTemplate(\\"\\", {
+ scope: () => ({
+ two
+ })
+ });"
+ `);
+ });
+
+ it('adds locals to the compiled output', function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ compiler,
+ transforms: [expressionTransform],
+ },
+ ],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ const template = precompileTemplate('');
+ `);
+ expect(transformed).toContain(`"scope": () => [two]`);
+ });
+
+ it('allows AST transform to bind a JS import', function () {
+ plugins = [
+ [HTMLBarsInlinePrecompile, { compiler, targetFormat: 'hbs', transforms: [importTransform] }],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ const template = precompileTemplate('');
+ `);
+
+ expect(transformed).toMatchInlineSnapshot(`
+ "import two from \\"my-library\\";
+ import { precompileTemplate } from '@ember/template-compilation';
+ const template = precompileTemplate(\\"\\", {
+ scope: () => ({
+ two
+ })
+ });"
+ `);
+ });
+
+ it('does not smash existing js binding for import', function () {
+ plugins = [
+ [HTMLBarsInlinePrecompile, { compiler, targetFormat: 'hbs', transforms: [importTransform] }],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ export function inner() {
+ let two = 'twice';
+ const template = precompileTemplate('');
+ }
+ `);
+
+ expect(transformed).toMatchInlineSnapshot(`
+ "import two0 from \\"my-library\\";
+ import { precompileTemplate } from '@ember/template-compilation';
+ export function inner() {
+ let two = 'twice';
+ const template = precompileTemplate(\\"\\", {
+ scope: () => ({
+ two0
+ })
+ });
+ }"
+ `);
+ });
+
+ it('does not smash existing hbs binding for import', function () {
+ plugins = [
+ [HTMLBarsInlinePrecompile, { compiler, targetFormat: 'hbs', transforms: [importTransform] }],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ export function inner() {
+ const template = precompileTemplate('{{#let "twice" as |two|}}{{/let}}');
+ }
+ `);
+
+ expect(transformed).toMatchInlineSnapshot(`
+ "import two from \\"my-library\\";
+ import { precompileTemplate } from '@ember/template-compilation';
+ let two0 = two;
+ export function inner() {
+ const template = precompileTemplate(\\"{{#let \\\\\\"twice\\\\\\" as |two|}}{{/let}}\\", {
+ scope: () => ({
+ two0
+ })
+ });
+ }"
+ `);
+ });
+
+ it('does not smash existing js binding for expression', function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ { compiler, targetFormat: 'hbs', transforms: [expressionTransform] },
+ ],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ export default function() {
+ let two = 'twice';
+ const template = precompileTemplate('');
+ }
+ `);
+
+ expect(transformed).toMatchInlineSnapshot(`
+ "import { precompileTemplate } from '@ember/template-compilation';
+ let two0 = 1 + 1;
+ export default function () {
+ let two = 'twice';
+ const template = precompileTemplate(\\"\\", {
+ scope: () => ({
+ two0
+ })
+ });
+ }"
+ `);
+ });
+
+ it('reuses existing imports when possible', () => {
+ plugins = [
+ [HTMLBarsInlinePrecompile, { compiler, targetFormat: 'hbs', transforms: [importTransform] }],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ export default function() {
+ const template = precompileTemplate('{{onePlusOne}}{{onePlusOne}}');
+ }
+ `);
+
+ expect(transformed).toContain(`{{two}}{{two}}`);
+ expect(transformed).toContain(`scope: () => ({
+ two
+ })`);
+ expect(transformed).toContain(`import two from "my-library"`);
+ });
+
+ it('rebinds existing imports when necessary', () => {
+ plugins = [
+ [HTMLBarsInlinePrecompile, { compiler, targetFormat: 'hbs', transforms: [importTransform] }],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ export default function() {
+ const template = precompileTemplate('{{onePlusOne}}{{#let "twice" as |two|}}{{onePlusOne}}{{/let}}');
+ }
+ `);
+
+ //expect(transformed).toContain(`{{two}}{{#let "twice" as |two|}}{{two0}}{{/let}}`);
+ expect(transformed).toContain(`{{two}}{{#let \\"twice\\" as |two|}}{{two0}}{{/let}}`);
+ expect(transformed).toContain(`scope: () => ({
+ two,
+ two0
+ })`);
+ expect(transformed).toContain(`import two from "my-library"`);
+ expect(transformed).toContain('let two0 = two');
+ });
+
+ it('does not smash own newly-created js binding for expression', function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ { compiler, targetFormat: 'hbs', transforms: [expressionTransform] },
+ ],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ export default function() {
+ const template1 = precompileTemplate('');
+ const template2 = precompileTemplate('');
+ }
+ `);
+
+ expect(transformed).toMatchInlineSnapshot(`
+ "import { precompileTemplate } from '@ember/template-compilation';
+ let two = 1 + 1;
+ let two0 = 1 + 1;
+ export default function () {
+ const template1 = precompileTemplate(\\"\\", {
+ scope: () => ({
+ two
+ })
+ });
+ const template2 = precompileTemplate(\\"\\", {
+ scope: () => ({
+ two0
+ })
+ });
+ }"
+ `);
+ });
+
+ it('does not smash existing hbs block binding for expression', function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ { compiler, targetFormat: 'hbs', transforms: [expressionTransform] },
+ ],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ export default function() {
+ const template = precompileTemplate('{{#let "twice" as |two|}}{{/let}}');
+ }
+ `);
+
+ expect(transformed).toMatchInlineSnapshot(`
+ "import { precompileTemplate } from '@ember/template-compilation';
+ let two0 = 1 + 1;
+ export default function () {
+ const template = precompileTemplate(\\"{{#let \\\\\\"twice\\\\\\" as |two|}}{{/let}}\\", {
+ scope: () => ({
+ two0
+ })
+ });
+ }"
+ `);
+ });
+
+ it('does not smash existing hbs element binding for expression', function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ { compiler, targetFormat: 'hbs', transforms: [expressionTransform] },
+ ],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ export default function() {
+ const template = precompileTemplate('');
+ }
+ `);
+
+ expect(transformed).toMatchInlineSnapshot(`
+ "import { precompileTemplate } from '@ember/template-compilation';
+ let two0 = 1 + 1;
+ export default function () {
+ const template = precompileTemplate(\\"\\", {
+ scope: () => ({
+ two0
+ })
+ });
+ }"
+ `);
+ });
+
+ it('understands that block params are only defined in the body, not the arguments, of an element', function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ { compiler, targetFormat: 'hbs', transforms: [expressionTransform] },
+ ],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ export default function() {
+ const template = precompileTemplate('{{two}}');
+ }
+ `);
+
+ expect(transformed).toMatchInlineSnapshot(`
+ "import { precompileTemplate } from '@ember/template-compilation';
+ let two = 1 + 1;
+ export default function () {
+ const template = precompileTemplate(\\"{{two}}\\", {
+ scope: () => ({
+ two
+ })
+ });
+ }"
+ `);
+ });
- beforeEach(function () {
- precompile = (template, options) => {
- return compiler.precompile(template, options);
+ it('does not smash other previously-bound expressions with new ones', () => {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ { compiler, targetFormat: 'hbs', transforms: [expressionTransform] },
+ ],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ export default function() {
+ const template = precompileTemplate('{{onePlusOne}}{{onePlusOne}}');
+ }
+ `);
+
+ expect(transformed).toContain(`{{two}}{{two0}}`);
+ expect(transformed).toContain(`scope: () => ({
+ two,
+ two0
+ })`);
+ expect(transformed).toContain(`let two = 1 + 1`);
+ expect(transformed).toContain(`let two0 = 1 + 1`);
+ });
+
+ it('can bind expressions that need imports', function () {
+ let nowTransform: ExtendedPluginBuilder = (env) => {
+ return {
+ name: 'now-transform',
+ visitor: {
+ PathExpression(node, path) {
+ if (node.original === 'now') {
+ let name = env.meta.jsutils.bindExpression(
+ (context) => {
+ let identifier = context.import('luxon', 'DateTime');
+ return `${identifier}.now()`;
+ },
+ path,
+ { nameHint: 'current' }
+ );
+ return env.syntax.builders.path(name);
+ }
+ return undefined;
+ },
+ },
};
+ };
+
+ plugins = [
+ [HTMLBarsInlinePrecompile, { compiler, targetFormat: 'hbs', transforms: [nowTransform] }],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ export default function() {
+ const template = precompileTemplate('');
+ }
+ `);
+
+ expect(transformed).toMatch(/let current = DateTime.now()/);
+ expect(transformed).toMatch(/import { DateTime } from "luxon"/);
+ expect(transformed).toContain('when={{current}}');
+ });
+
+ it('can emit side-effectful expression that need imports', function () {
+ let compatTransform: ExtendedPluginBuilder = (env) => {
+ return {
+ name: 'compat-transform',
+ visitor: {
+ ElementNode(node) {
+ if (node.tag === 'Thing') {
+ env.meta.jsutils.emitExpression((context) => {
+ let identifier = context.import('ember-thing', '*', 'thing');
+ return `window.define('my-app/components/thing', ${identifier})`;
+ });
+ }
+ },
+ },
+ };
+ };
+
+ plugins = [
+ [HTMLBarsInlinePrecompile, { compiler, targetFormat: 'hbs', transforms: [compatTransform] }],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ export default function() {
+ const template = precompileTemplate('');
+ }
+ `);
+
+ expect(transformed).toContain(`import * as thing from "ember-thing"`);
+ expect(transformed).toContain(`window.define('my-app/components/thing', thing)`);
+ });
+
+ it('can emit side-effectful import', function () {
+ let compatTransform: ExtendedPluginBuilder = (env) => {
+ return {
+ name: 'compat-transform',
+ visitor: {
+ ElementNode(node) {
+ if (node.tag === 'Thing') {
+ env.meta.jsutils.importForSideEffect('setup-the-things');
+ }
+ },
+ },
+ };
+ };
+
+ plugins = [
+ [HTMLBarsInlinePrecompile, { compiler, targetFormat: 'hbs', transforms: [compatTransform] }],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ export default function() {
+ const template = precompileTemplate('');
+ }
+ `);
+
+ expect(transformed).toContain(`import "setup-the-things"`);
+ });
+
+ describe('source-to-source', function () {
+ const color: ExtendedPluginBuilder = (env) => {
+ return {
+ name: 'simple-transform',
+ visitor: {
+ PathExpression(node) {
+ if (node.original === 'red') {
+ return env.syntax.builders.string('#ff0000');
+ }
+ return undefined;
+ },
+ },
+ };
+ };
+
+ it('can run an ast transform inside precompileTemplate', function () {
+ plugins = [
+ [HTMLBarsInlinePrecompile, { compiler, targetFormat: 'hbs', transforms: [color] }],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ const template = precompileTemplate('');
+ `);
+
+ expect(transformed).toMatchInlineSnapshot(`
+ "import { precompileTemplate } from '@ember/template-compilation';
+ const template = precompileTemplate(\\"\\");"
+ `);
});
- it('includes the original template content', function () {
+ it('can run an ast transform inside hbs backticks', function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ compiler,
+ targetFormat: 'hbs',
+ transforms: [color],
+ enableLegacyModules: ['ember-cli-htmlbars'],
+ },
+ ],
+ ];
+
+ let transformed = transform(
+ "import { hbs } from 'ember-cli-htmlbars'; const template = hbs``;"
+ );
+
+ expect(transformed).toMatchInlineSnapshot(`
+ "import { hbs } from 'ember-cli-htmlbars';
+ const template = hbs\`\`;"
+ `);
+ });
+
+ it('can create the options object for precompileTemplate', function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ { compiler, targetFormat: 'hbs', transforms: [expressionTransform] },
+ ],
+ ];
+
let transformed = transform(stripIndent`
import { precompileTemplate } from '@ember/template-compilation';
+ const template = precompileTemplate('');
+ `);
- const template = precompileTemplate('hello {{firstName}}');
+ expect(transformed).toMatchInlineSnapshot(`
+ "import { precompileTemplate } from '@ember/template-compilation';
+ let two = 1 + 1;
+ const template = precompileTemplate(\\"\\", {
+ scope: () => ({
+ two
+ })
+ });"
+ `);
+ });
+
+ it('adds scope to existing options object', function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ { compiler, targetFormat: 'hbs', transforms: [expressionTransform] },
+ ],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ import Message from 'message';
+ const template = precompileTemplate('', {
+ moduleName: 'customModuleName'
+ });
+ `);
+
+ expect(transformed).toMatchInlineSnapshot(`
+ "import { precompileTemplate } from '@ember/template-compilation';
+ import Message from 'message';
+ let two = 1 + 1;
+ const template = precompileTemplate(\\"\\", {
+ moduleName: 'customModuleName',
+ scope: () => ({
+ two
+ })
+ });"
+ `);
+ });
+
+ it('adds new locals to preexisting scope', function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ { compiler, targetFormat: 'hbs', transforms: [expressionTransform] },
+ ],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { precompileTemplate } from '@ember/template-compilation';
+ import Message from 'message';
+ const template = precompileTemplate('', {
+ scope: () => ({
+ Message
+ })
+ });
+ `);
+
+ expect(transformed).toMatchInlineSnapshot(`
+ "import { precompileTemplate } from '@ember/template-compilation';
+ import Message from 'message';
+ let two = 1 + 1;
+ const template = precompileTemplate(\\"\\", {
+ scope: () => ({
+ Message,
+ two
+ })
+ });"
+ `);
+ });
+
+ it('switches from legacy callExpressions to precompileTemplate when needed to support scope', function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ compiler,
+ targetFormat: 'hbs',
+ transforms: [expressionTransform],
+ enableLegacyModules: ['ember-cli-htmlbars'],
+ },
+ ],
+ ];
+
+ let transformed = transform(stripIndent`
+ import { hbs } from 'ember-cli-htmlbars';
+ const template = hbs('');
+ `);
+
+ expect(transformed).toMatchInlineSnapshot(`
+ "import { precompileTemplate } from \\"@ember/template-compilation\\";
+ let two = 1 + 1;
+ const template = precompileTemplate(\\"\\", {
+ scope: () => ({
+ two
+ })
+ });"
+ `);
+ });
+
+ it('switches from hbs backticks to precompileTemplate when needed to support scope', function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ compiler,
+ targetFormat: 'hbs',
+ transforms: [expressionTransform],
+ enableLegacyModules: ['ember-cli-htmlbars'],
+ },
+ ],
+ ];
+
+ let transformed = transform(
+ "import { hbs } from 'ember-cli-htmlbars'; const template = hbs``;"
+ );
+
+ expect(transformed).toMatchInlineSnapshot(`
+ "import { precompileTemplate } from \\"@ember/template-compilation\\";
+ let two = 1 + 1;
+ const template = precompileTemplate(\\"\\", {
+ scope: () => ({
+ two
+ })
+ });"
`);
+ });
+
+ it('does not remove original import if there are still callsites using it', function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ compiler,
+ targetFormat: 'hbs',
+ transforms: [expressionTransform],
+ enableLegacyModules: ['ember-cli-htmlbars'],
+ },
+ ],
+ ];
+
+ let transformed = transform(
+ "import { hbs } from 'ember-cli-htmlbars'; const template = hbs``; const other = hbs`hello`;"
+ );
- expect(transformed).toContain(`hello {{firstName}}`);
+ expect(transformed).toMatchInlineSnapshot(`
+ "import { precompileTemplate } from \\"@ember/template-compilation\\";
+ import { hbs } from 'ember-cli-htmlbars';
+ let two = 1 + 1;
+ const template = precompileTemplate(\\"\\", {
+ scope: () => ({
+ two
+ })
+ });
+ const other = hbs\`hello\`;"
+ `);
});
});
+ it('removes original import when there are multiple callsites that all needed replacement', function () {
+ plugins = [
+ [
+ HTMLBarsInlinePrecompile,
+ {
+ compiler,
+ targetFormat: 'hbs',
+ transforms: [expressionTransform],
+ enableLegacyModules: ['ember-cli-htmlbars'],
+ },
+ ],
+ ];
+
+ let transformed = transform(
+ "import { hbs } from 'ember-cli-htmlbars'; const template = hbs``; const other = hbs`{{onePlusOne}}`;"
+ );
+
+ expect(transformed).toMatchInlineSnapshot(`
+ "import { precompileTemplate } from \\"@ember/template-compilation\\";
+ let two = 1 + 1;
+ let two0 = 1 + 1;
+ const template = precompileTemplate(\\"\\", {
+ scope: () => ({
+ two
+ })
+ });
+ const other = precompileTemplate(\\"{{two0}}\\", {
+ scope: () => ({
+ two0
+ })
+ });"
+ `);
+ });
+
describe('scope', function () {
it('correctly handles scope function (non-block arrow function)', function () {
let source = 'hello';
+ let spy = sinon.spy(compiler, 'precompile');
+
transform(
`import { precompileTemplate } from '@ember/template-compilation';\nvar compiled = precompileTemplate('${source}', { scope: () => ({ foo, bar }) });`
);
- expect(optionsReceived).toEqual({
- contents: source,
- locals: ['foo', 'bar'],
- });
+ expect(spy.firstCall.lastArg).toHaveProperty('locals', ['foo', 'bar']);
});
it('correctly handles scope function (block arrow function)', function () {
let source = 'hello';
+ let spy = sinon.spy(compiler, 'precompile');
+
transform(
`import { precompileTemplate } from '@ember/template-compilation';\nvar compiled = precompileTemplate('${source}', { scope: () => { return { foo, bar }; }});`
);
- expect(optionsReceived).toEqual({
- contents: source,
- locals: ['foo', 'bar'],
- });
+
+ expect(spy.firstCall.lastArg).toHaveProperty('locals', ['foo', 'bar']);
});
it('correctly handles scope function (normal function)', function () {
let source = 'hello';
+ let spy = sinon.spy(compiler, 'precompile');
+
transform(
`import { precompileTemplate } from '@ember/template-compilation';\nvar compiled = precompileTemplate('${source}', { scope: function() { return { foo, bar }; }});`
);
- expect(optionsReceived).toEqual({
- contents: source,
- locals: ['foo', 'bar'],
- });
+
+ expect(spy.firstCall.lastArg).toHaveProperty('locals', ['foo', 'bar']);
});
it('correctly handles scope function (object method)', function () {
let source = 'hello';
+ let spy = sinon.spy(compiler, 'precompile');
+
transform(
`import { precompileTemplate } from '@ember/template-compilation';\nvar compiled = precompileTemplate('${source}', { scope() { return { foo, bar }; }});`
);
- expect(optionsReceived).toEqual({
- contents: source,
- locals: ['foo', 'bar'],
- });
+ expect(spy.firstCall.lastArg).toHaveProperty('locals', ['foo', 'bar']);
});
it('errors if scope contains mismatched keys/values', function () {
diff --git a/package.json b/package.json
index 91b0e92..2077157 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "babel-plugin-ember-template-compilation",
- "version": "1.0.2",
+ "version": "2.0.0-alpha.4",
"description": "Babel implementation of Ember's low-level template-compilation API",
"repository": "https://github.com/emberjs/babel-plugin-ember-template-compilation",
"license": "MIT",
@@ -34,7 +34,7 @@
"src/**/*.js.map"
],
"dependencies": {
- "babel-import-util": "^1.2.0"
+ "babel-import-util": "^1.3.0"
},
"devDependencies": {
"@babel/core": "^7.14.8",
@@ -43,12 +43,14 @@
"@babel/plugin-transform-template-literals": "^7.14.5",
"@babel/plugin-transform-unicode-escapes": "^7.14.5",
"@babel/traverse": "^7.14.5",
+ "@glimmer/syntax": "^0.84.2",
"@types/babel__traverse": "^7.11.1",
"@types/jest": "^26.0.23",
+ "@types/sinon": "^10.0.13",
"@typescript-eslint/eslint-plugin": "^4.28.4",
"@typescript-eslint/parser": "^4.28.4",
"common-tags": "^1.8.0",
- "ember-source": "^3.27.5",
+ "ember-source": "^3.28.9",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-node": "^11.1.0",
@@ -57,6 +59,7 @@
"prettier": "^2.2.1",
"release-it": "^14.10.0",
"release-it-lerna-changelog": "^3.1.0",
+ "sinon": "^14.0.0",
"typescript": "^4.3.5"
},
"engines": {
diff --git a/src/ember-template-compiler.ts b/src/ember-template-compiler.ts
new file mode 100644
index 0000000..c49badd
--- /dev/null
+++ b/src/ember-template-compiler.ts
@@ -0,0 +1,32 @@
+import { ASTv1 } from '@glimmer/syntax';
+import { ExtendedPluginBuilder } from './js-utils';
+
+// The interface we use from ember-template-compiler.js
+export interface EmberTemplateCompiler {
+ precompile(templateString: string, options: PreprocessOptions): string;
+ _buildCompileOptions(options: PreprocessOptions): PreprocessOptions;
+ _print(ast: ASTv1.Template, options?: { entityEncoding?: 'transformed' | 'raw' }): string;
+ _preprocess(src: string, options?: PreprocessOptions): ASTv1.Template;
+}
+
+export interface PreprocessOptions {
+ contents: string;
+ moduleName: string;
+ plugins?: { ast?: ExtendedPluginBuilder[] };
+ filename?: string;
+ parseOptions?: {
+ srcName?: string;
+ ignoreStandalone?: boolean;
+ };
+ mode?: 'codemod' | 'precompile';
+ strictMode?: boolean;
+ locals?: string[];
+}
+
+export function assertTemplateCompiler(
+ emberTemplateCompiler: any
+): asserts emberTemplateCompiler is EmberTemplateCompiler {
+ if (typeof emberTemplateCompiler._preprocess !== 'function') {
+ throw new Error(`Unexpected API on ember template compiler. This plugin supports Ember 3.27+.`);
+ }
+}
diff --git a/src/js-utils.ts b/src/js-utils.ts
new file mode 100644
index 0000000..f74b519
--- /dev/null
+++ b/src/js-utils.ts
@@ -0,0 +1,309 @@
+import type { types as t } from '@babel/core';
+import type * as Babel from '@babel/core';
+import type { NodePath } from '@babel/traverse';
+import type { ASTPluginBuilder, ASTPluginEnvironment, ASTv1, WalkerPath } from '@glimmer/syntax';
+import type { ImportUtil } from 'babel-import-util';
+
+interface State {
+ program: NodePath;
+ lastInsertedPath: NodePath | undefined;
+}
+
+// This exists to give AST plugins a controlled interface for influencing the
+// surrounding Javascript scope
+export class JSUtils {
+ #babel: typeof Babel;
+ #state: State;
+ #template: NodePath;
+ #locals: string[];
+ #importer: ImportUtil;
+
+ constructor(
+ babel: typeof Babel,
+ state: State,
+ template: NodePath,
+ locals: string[],
+ importer: ImportUtil
+ ) {
+ this.#babel = babel;
+ this.#state = state;
+ this.#template = template;
+ this.#locals = locals;
+ this.#importer = importer;
+
+ if (!this.#state.lastInsertedPath) {
+ let target: NodePath | undefined;
+ for (let statement of this.#state.program.get('body')) {
+ if (!statement.isImportDeclaration()) {
+ break;
+ }
+ target = statement;
+ }
+ if (target) {
+ this.#state.lastInsertedPath = target;
+ }
+ }
+ }
+
+ /**
+ * Create a new binding that you can use in your template, initialized with
+ * the given Javascript expression.
+ *
+ * @param { Expression } expression A javascript expression whose value will
+ * initialize your new binding. See docs on the Expression type for details.
+ * @param target The location within your template where the binding will be
+ * used. This matters so we can avoid naming collisions.
+ * @param opts.nameHint Optionally, provide a descriptive name for your new
+ * binding. We will mangle this name as needed to avoid collisions, but
+ * picking a good name here can aid in debugging.
+ *
+ * @return The name you can use in your template to access the binding.
+ */
+ bindExpression(
+ expression: Expression,
+ target: WalkerPath,
+ opts?: { nameHint?: string }
+ ): string {
+ let name = unusedNameLike(
+ opts?.nameHint ?? 'a',
+ (candidate) =>
+ this.#template.scope.hasBinding(candidate) ||
+ this.#locals.includes(candidate) ||
+ astNodeHasBinding(target, candidate)
+ );
+ let t = this.#babel.types;
+ let declaration: NodePath = this.#emitStatement(
+ t.variableDeclaration('let', [
+ t.variableDeclarator(
+ t.identifier(name),
+ this.#parseExpression(this.#state.program, expression)
+ ),
+ ])
+ );
+ declaration.scope.registerBinding('module', declaration.get('declarations.0') as NodePath);
+ this.#locals.push(name);
+ return name;
+ }
+
+ #emitStatement(statement: T): NodePath {
+ if (this.#state.lastInsertedPath) {
+ this.#state.lastInsertedPath = this.#state.lastInsertedPath.insertAfter(statement)[0];
+ } else {
+ this.#state.lastInsertedPath = this.#state.program.unshiftContainer('body', statement)[0];
+ }
+ return this.#state.lastInsertedPath as NodePath;
+ }
+
+ /**
+ * Gain access to an imported value within your template.
+ *
+ * @param moduleSpecifier The path to import from.
+ * @param exportedName The named export you wish to access, or "default" for
+ * the default export, or "*" for the namespace export.
+ * @param target The location within your template where the binding will be
+ * used. This matters so we can avoid naming collisions.
+ * @param opts.nameHint Optionally, provide a descriptive name for your new
+ * binding. We will mangle this name as needed to avoid collisions, but
+ * picking a good name here can aid in debugging.
+ *
+ * @return The name you can use in your template to access the imported value.
+ */
+ bindImport(
+ moduleSpecifier: string,
+ exportedName: string,
+ target: WalkerPath,
+ opts?: { nameHint?: string }
+ ): string {
+ // This will discover or create the local name for accessing the given import.
+ let importedIdentifier = this.#importer.import(
+ this.#template,
+ moduleSpecifier,
+ exportedName,
+ opts?.nameHint
+ );
+
+ // If we're already referencing the imported name from the outer scope and
+ // it's not shadowed at our target location in the template, we can reuse
+ // the existing import.
+ if (
+ this.#locals.includes(importedIdentifier.name) &&
+ !astNodeHasBinding(target, importedIdentifier.name)
+ ) {
+ return importedIdentifier.name;
+ }
+
+ let identifier = unusedNameLike(
+ importedIdentifier.name,
+ (candidate) => this.#locals.includes(candidate) || astNodeHasBinding(target, candidate)
+ );
+ if (identifier !== importedIdentifier.name) {
+ // The importedIdentifier that we have in Javascript is not usable within
+ // our HBS because it's shadowed by a block param. So we will introduce a
+ // second name via a variable declaration.
+ //
+ // The reason we don't force the import itself to have this name is that
+ // we might be re-using an existing import, and we don't want to go
+ // rewriting all of its callsites that are unrelated to us.
+ let t = this.#babel.types;
+ this.#emitStatement(
+ t.variableDeclaration('let', [
+ t.variableDeclarator(t.identifier(identifier), importedIdentifier),
+ ])
+ );
+ }
+ this.#locals.push(identifier);
+ return identifier;
+ }
+
+ /**
+ * Add an import statement purely for side effect.
+ *
+ * @param moduleSpecifier the module to import
+ */
+ importForSideEffect(moduleSpecifier: string): void {
+ this.#importer.importForSideEffect(moduleSpecifier);
+ }
+
+ /**
+ * Emit a javascript expresison for side-effect. This only accepts
+ * expressions, not statements, because you should not introduce new bindings.
+ * To introduce a binding see bindExpression or bindImport instead.
+ *
+ * @param { Expression } expression A javascript expression whose value will
+ * initialize your new binding. See docs on the Expression type below for
+ * details.
+ */
+ emitExpression(expression: Expression): void {
+ let t = this.#babel.types;
+ this.#emitStatement(
+ t.expressionStatement(this.#parseExpression(this.#state.program, expression))
+ );
+ }
+
+ #parseExpression(target: NodePath, expression: Expression): t.Expression {
+ let expressionString: string;
+ if (typeof expression === 'string') {
+ expressionString = expression;
+ } else {
+ expressionString = expression(new ExpressionContext(this.#importer, target));
+ }
+
+ let parsed = this.#babel.parse(expressionString);
+ if (!parsed) {
+ throw new Error(
+ `JSUtils.bindExpression could not understand the expression: ${expressionString}`
+ );
+ }
+ let statements = body(parsed);
+ if (statements.length !== 1) {
+ throw new Error(
+ `JSUtils.bindExpression expected to find exactly one expression but found ${statements.length} in: ${expressionString}`
+ );
+ }
+ let statement = statements[0];
+ if (statement.type !== 'ExpressionStatement') {
+ throw new Error(
+ `JSUtils.bindExpression expected to find an expression but found ${statement.type} in: ${expressionString}`
+ );
+ }
+ return statement.expression;
+ }
+}
+
+function unusedNameLike(desiredName: string, isUsed: (name: string) => boolean): string {
+ let candidate = desiredName;
+ let counter = 0;
+ while (isUsed(candidate)) {
+ candidate = `${desiredName}${counter++}`;
+ }
+ return candidate;
+}
+
+function astNodeHasBinding(target: WalkerPath, name: string): boolean {
+ let cursor: WalkerPath | null = target;
+ while (cursor) {
+ let parentNode = cursor.parent?.node;
+ if (
+ parentNode?.type === 'ElementNode' &&
+ parentNode.blockParams.includes(name) &&
+ // an ElementNode's block params are valid only within its children
+ parentNode.children.includes(cursor.node as ASTv1.Statement)
+ ) {
+ return true;
+ }
+
+ if (
+ parentNode?.type === 'Block' &&
+ parentNode.blockParams.includes(name) &&
+ // a Block's blockParams are valid only within its body
+ parentNode.body.includes(cursor.node as ASTv1.Statement)
+ ) {
+ return true;
+ }
+
+ cursor = cursor.parent;
+ }
+ return false;
+}
+
+/**
+ * This extends Glimmer's ASTPluginEnvironment type to put our jsutils into meta
+ */
+export type WithJSUtils = {
+ meta: T['meta'] & { jsutils: JSUtils };
+} & T;
+
+export type ExtendedPluginBuilder = ASTPluginBuilder>;
+
+function body(node: t.Program | t.File) {
+ if (node.type === 'File') {
+ return node.program.body;
+ } else {
+ return node.body;
+ }
+}
+
+/**
+ * Allows you to construct an expression that relies on imported values.
+ */
+class ExpressionContext {
+ #importer: ImportUtil;
+ #target: NodePath;
+
+ constructor(importer: ImportUtil, target: NodePath) {
+ this.#importer = importer;
+ this.#target = target;
+ }
+
+ /**
+ * Find or create a local binding for the given import.
+ *
+ * @param moduleSpecifier The path to import from.
+ * @param exportedName The named export you wish to access, or "default" for
+ * the default export, or "*" for the namespace export.
+ * @param nameHint Optionally, provide a descriptive name for your new
+ * binding. We will mangle this name as needed to avoid collisions, but
+ * picking a good name here can aid in debugging.
+
+ * @return the local identifier for the imported value
+ */
+ import(moduleSpecifier: string, exportedName: string, nameHint?: string): string {
+ return this.#importer.import(this.#target, moduleSpecifier, exportedName, nameHint).name;
+ }
+}
+
+/**
+ * You can pass a Javascript expression as a string like:
+ *
+ * "new Date()"
+ *
+ * Or as a function that returns a string:
+ *
+ * () => "new Date()"
+ *
+ * When you use a function, it can use imported values:
+ *
+ * (context) => `new ${context.import("luxon", "DateTime")}()`
+ *
+ */
+export type Expression = string | ((context: ExpressionContext) => string);
diff --git a/src/node-main.ts b/src/node-main.ts
index e92a0d6..1825749 100644
--- a/src/node-main.ts
+++ b/src/node-main.ts
@@ -1,46 +1,81 @@
import { resolve } from 'path';
-import makePlugin from './plugin';
-import type * as Babel from '@babel/core';
-
-import { Options as PluginOptions, EmberPrecompile } from './plugin';
-
-export interface Options extends PluginOptions {
- // The on-disk path to a module that provides a `precompile` function as
- // defined below. You need to either set `precompilePath` or set `precompile`.
- precompilerPath?: string;
-
- // A precompile function that invokes Ember's template compiler.
- //
- // Options handling rules:
- //
- // - we add `content`, which is the original string form of the template
- // - we have special parsing for `scope` which becomes `locals` when passed
- // to your precompile
- // - anything else the user passes to `precompileTemplate` will be passed
- // through to your `precompile`.
- precompile?: EmberPrecompile;
+import { makePlugin } from './plugin';
+
+import { Options as SharedOptions } from './plugin';
+import { assertTemplateCompiler, EmberTemplateCompiler } from './ember-template-compiler';
+import { ExtendedPluginBuilder } from './js-utils';
+
+export * from './public-types';
+
+export type Transform = ExtendedPluginBuilder | string | [string, unknown];
+
+export type Options = Omit & {
+ // The on-disk path to the ember-template-comipler.js module for our current
+ // ember version. You need to either set `compilerPath` or set `compiler`.
+ compilerPath?: string;
+
+ // The ember-template-compiler.js module that ships within your ember-source
+ // version. You need to set either `compilerPath` or `compiler`.
+ compiler?: EmberTemplateCompiler;
+
+ // List of custom transformations to apply to the handlebars AST before
+ // compilation. These can be
+ // - the actual functions
+ // - resolvable module names
+ // - pairs of [resolvableModuleName, options], in which case we will invoke
+ // the default export of the module with the options as argument, and the
+ // actual ast transform function should be returned.
+ transforms?: Transform[];
+};
+
+function cwdRequire(moduleName: string) {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ return require(require.resolve(moduleName, { paths: [process.cwd()] }));
}
-const htmlbarsInlinePrecompile = makePlugin(function (opts: Options) {
- if (opts.precompilerPath) {
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- let mod: any = require(opts.precompilerPath);
- return mod.precompile;
- } else if (opts.precompile) {
- return opts.precompile;
+function handleNodeSpecificOptions(opts: Options): SharedOptions {
+ let compiler: EmberTemplateCompiler;
+ if (opts.compilerPath) {
+ let mod: any = cwdRequire(opts.compilerPath);
+ assertTemplateCompiler(mod);
+ compiler = mod;
+ } else if (opts.compiler) {
+ assertTemplateCompiler(opts.compiler);
+ compiler = opts.compiler;
+ } else {
+ throw new Error(`must provide compilerPath or compiler`);
}
-}) as {
- (babel: typeof Babel): Babel.PluginObj;
- _parallelBabel: { requireFile: string };
- baseDir(): string;
-};
-htmlbarsInlinePrecompile._parallelBabel = {
+ let transforms = [];
+ if (opts.transforms) {
+ transforms = opts.transforms.map((t) => {
+ if (typeof t === 'string') {
+ return esCompat(cwdRequire(t)).default;
+ } else if (Array.isArray(t) && typeof t[0] === 'string') {
+ return esCompat(cwdRequire(t[0])).default.call(undefined, t[1]);
+ } else {
+ return t;
+ }
+ });
+ }
+ return { ...opts, transforms, compiler };
+}
+
+const htmlbarsInlinePrecompile = makePlugin(handleNodeSpecificOptions);
+
+(htmlbarsInlinePrecompile as any)._parallelBabel = {
requireFile: __filename,
};
-htmlbarsInlinePrecompile.baseDir = function () {
+(htmlbarsInlinePrecompile as any).baseDir = function () {
return resolve(__dirname, '..');
};
-export default htmlbarsInlinePrecompile;
+export default htmlbarsInlinePrecompile as typeof htmlbarsInlinePrecompile & {
+ baseDir(): string;
+ _parallelBabel: { requireFile: string };
+};
+
+function esCompat(m: Record) {
+ return m?.__esModule ? m : { default: m };
+}
diff --git a/src/plugin.ts b/src/plugin.ts
index 3f2c3bc..87e45f9 100644
--- a/src/plugin.ts
+++ b/src/plugin.ts
@@ -3,13 +3,11 @@ import type * as Babel from '@babel/core';
import type { types as t } from '@babel/core';
import { ImportUtil } from 'babel-import-util';
import { ExpressionParser } from './expression-parser';
+import { JSUtils, ExtendedPluginBuilder } from './js-utils';
+import type { EmberTemplateCompiler, PreprocessOptions } from './ember-template-compiler';
+import { LegacyModuleName } from './public-types';
-export type EmberPrecompile = (templateString: string, options: Record) => string;
-
-export type LegacyModuleName =
- | 'ember-cli-htmlbars'
- | 'ember-cli-htmlbars-inline-precompile'
- | 'htmlbars-inline-precompile';
+export * from './public-types';
type ModuleName = LegacyModuleName | '@ember/template-compilation';
@@ -48,6 +46,9 @@ const INLINE_PRECOMPILE_MODULES: ModuleConfig[] = [
];
export interface Options {
+ // The ember-template-compiler.js module that ships within your ember-source version.
+ compiler: EmberTemplateCompiler;
+
// Allows you to remap what imports will be emitted in our compiled output. By
// example:
//
@@ -70,87 +71,68 @@ export interface Options {
// use, and we can enable those too by including their module names in this
// list.
enableLegacyModules?: LegacyModuleName[];
+
+ // Controls the output format.
+ //
+ // "wire": The default. In the output, your templates are ready to execute in
+ // the most performant way.
+ //
+ // "hbs": In the output, your templates will still be in HBS format.
+ // Generally this means they will still need further processing before
+ // they're ready to execute. The purpose of this mode is to support things
+ // like codemods and pre-publication transformations in libraries.
+ targetFormat?: 'wire' | 'hbs';
+
+ // Optional list of custom transforms to apply to the handlebars AST before
+ // compilation.
+ transforms?: ExtendedPluginBuilder[];
}
-interface State {
- opts: Options;
+interface State {
+ opts: EnvSpecificOptions;
+ normalizedOpts: Required;
util: ImportUtil;
- precompile: EmberPrecompile;
templateFactory: { moduleName: string; exportName: string };
+ program: NodePath;
+ lastInsertedPath: NodePath | undefined;
+ filename: string;
}
-export default function makePlugin(
- // receives the Babel plugin options, returns Ember's precompiler
- loadPrecompiler: (opts: O) => EmberPrecompile
-) {
- return function htmlbarsInlinePrecompile(babel: typeof Babel): Babel.PluginObj {
+export function makePlugin(loadOptions: (opts: EnvSpecificOptions) => Options) {
+ return function htmlbarsInlinePrecompile(
+ babel: typeof Babel
+ ): Babel.PluginObj> {
let t = babel.types;
- function insertCompiledTemplate(
- target: NodePath,
- state: State,
- template: string,
- userTypedOptions: Record
- ): void {
- let options = Object.assign({ contents: template }, userTypedOptions);
- let precompile = state.precompile;
- let precompileResultString: string;
-
- if (options.insertRuntimeErrors) {
- try {
- precompileResultString = precompile(template, options);
- } catch (error) {
- target.replaceWith(runtimeErrorIIFE(babel, { ERROR_MESSAGE: (error as any).message }));
- return;
- }
- } else {
- precompileResultString = precompile(template, options);
- }
-
- let precompileResultAST = babel.parse(`var precompileResult = ${precompileResultString};`, {
- babelrc: false,
- configFile: false,
- }) as t.File;
-
- let templateExpression = (precompileResultAST.program.body[0] as t.VariableDeclaration)
- .declarations[0].init as t.Expression;
-
- t.addComment(
- templateExpression,
- 'leading',
- `\n ${template.replace(/\*\//g, '*\\/')}\n`,
- /* line comment? */ false
- );
-
- let templateFactoryIdentifier = state.util.import(
- target,
- state.templateFactory.moduleName,
- state.templateFactory.exportName
- );
- target.replaceWith(t.callExpression(templateFactoryIdentifier, [templateExpression]));
- }
-
return {
visitor: {
Program: {
- enter(path: NodePath, state: State) {
- let moduleName = '@ember/template-factory';
- let exportName = 'createTemplateFactory';
- let overrides = state.opts.outputModuleOverrides?.[moduleName]?.[exportName];
- state.templateFactory = overrides
- ? { exportName: overrides[0], moduleName: overrides[1] }
- : { exportName, moduleName };
+ enter(path: NodePath, state: State) {
+ state.normalizedOpts = {
+ targetFormat: 'wire',
+ outputModuleOverrides: {},
+ enableLegacyModules: [],
+ transforms: [],
+ ...loadOptions(state.opts),
+ };
+
+ state.templateFactory = templateFactoryConfig(state.normalizedOpts);
state.util = new ImportUtil(t, path);
- state.precompile = loadPrecompiler(state.opts as O);
+ state.program = path;
},
- exit(_path: NodePath, state: State) {
- for (let { moduleName, export: exportName } of configuredModules(state)) {
- state.util.removeImport(moduleName, exportName);
+ exit(_path: NodePath, state: State) {
+ if (state.normalizedOpts.targetFormat === 'wire') {
+ for (let { moduleName, export: exportName } of configuredModules(state)) {
+ state.util.removeImport(moduleName, exportName);
+ }
}
},
},
- TaggedTemplateExpression(path: NodePath, state: State) {
+ TaggedTemplateExpression(
+ path: NodePath,
+ state: State
+ ) {
let tagPath = path.get('tag');
if (!tagPath.isIdentifier()) {
@@ -174,10 +156,14 @@ export default function makePlugin(
}
let template = path.node.quasi.quasis.map((quasi) => quasi.value.cooked).join('');
- insertCompiledTemplate(path, state, template, {});
+ if (state.normalizedOpts.targetFormat === 'wire') {
+ insertCompiledTemplate(babel, state, template, path, {});
+ } else {
+ insertTransformedTemplate(babel, state, template, path, {}, options);
+ }
},
- CallExpression(path: NodePath, state: State) {
+ CallExpression(path: NodePath, state: State) {
let calleePath = path.get('callee');
if (!calleePath.isIdentifier()) {
@@ -229,7 +215,7 @@ export default function makePlugin(
userTypedOptions = new ExpressionParser(babel).parseObjectExpression(
calleePath.node.name,
secondArg,
- true
+ options.enableScope
);
}
if (restArgs.length > 0) {
@@ -237,18 +223,22 @@ export default function makePlugin(
`${calleePath.node.name} can only be invoked with 2 arguments: the template string, and any static options`
);
}
- insertCompiledTemplate(path, state, template, userTypedOptions);
+ if (state.normalizedOpts.targetFormat === 'wire') {
+ insertCompiledTemplate(babel, state, template, path, userTypedOptions);
+ } else {
+ insertTransformedTemplate(babel, state, template, path, userTypedOptions, options);
+ }
},
},
};
- };
+ } as (babel: typeof Babel) => Babel.PluginObj;
}
-function* configuredModules(state: State) {
+function* configuredModules(state: State) {
for (let moduleConfig of INLINE_PRECOMPILE_MODULES) {
if (
moduleConfig.moduleName !== '@ember/template-compilation' &&
- !state.opts.enableLegacyModules?.includes(moduleConfig.moduleName)
+ !state.normalizedOpts.enableLegacyModules.includes(moduleConfig.moduleName)
) {
continue;
}
@@ -256,9 +246,9 @@ function* configuredModules(state: State) {
}
}
-function referencesInlineCompiler(
+function referencesInlineCompiler(
path: NodePath,
- state: State
+ state: State
): ModuleConfig | undefined {
for (let moduleConfig of configuredModules(state)) {
if (path.referencesImport(moduleConfig.moduleName, moduleConfig.export)) {
@@ -274,3 +264,200 @@ function runtimeErrorIIFE(babel: typeof Babel, replacements: { ERROR_MESSAGE: st
) as t.ExpressionStatement;
return statement.expression;
}
+
+function buildPrecompileOptions(
+ babel: typeof Babel,
+ target: NodePath,
+ state: State,
+ template: string,
+ userTypedOptions: Record
+): PreprocessOptions & Record {
+ if (!userTypedOptions.locals) {
+ userTypedOptions.locals = [];
+ }
+ let jsutils = new JSUtils(babel, state, target, userTypedOptions.locals as string[], state.util);
+ let meta = Object.assign({ jsutils }, userTypedOptions?.meta);
+ return Object.assign(
+ {
+ contents: template,
+ meta,
+
+ // TODO: embroider's template-compiler allows this to be overriden to get
+ // backward-compatible module names that don't match the real name of the
+ // on-disk file. What's our plan for migrating people away from that?
+ moduleName: state.filename,
+
+ // This is here so it's *always* the real filename. Historically, there is
+ // also `moduleName` but that did not match the real on-disk filename, it
+ // was the notional runtime module name from classic ember builds.
+ filename: state.filename,
+
+ plugins: {
+ ast: state.normalizedOpts.transforms,
+ },
+ },
+ userTypedOptions
+ );
+}
+
+function insertCompiledTemplate(
+ babel: typeof Babel,
+ state: State,
+ template: string,
+ target: NodePath,
+ userTypedOptions: Record
+) {
+ let t = babel.types;
+ let options = buildPrecompileOptions(babel, target, state, template, userTypedOptions);
+
+ let precompileResultString: string;
+
+ if (options.insertRuntimeErrors) {
+ try {
+ precompileResultString = state.normalizedOpts.compiler.precompile(template, options);
+ } catch (error) {
+ target.replaceWith(runtimeErrorIIFE(babel, { ERROR_MESSAGE: (error as any).message }));
+ return;
+ }
+ } else {
+ precompileResultString = state.normalizedOpts.compiler.precompile(template, options);
+ }
+
+ let precompileResultAST = babel.parse(`var precompileResult = ${precompileResultString};`, {
+ babelrc: false,
+ configFile: false,
+ }) as t.File;
+
+ let templateExpression = (precompileResultAST.program.body[0] as t.VariableDeclaration)
+ .declarations[0].init as t.Expression;
+
+ t.addComment(
+ templateExpression,
+ 'leading',
+ `\n ${template.replace(/\*\//g, '*\\/')}\n`,
+ /* line comment? */ false
+ );
+
+ let templateFactoryIdentifier = state.util.import(
+ target,
+ state.templateFactory.moduleName,
+ state.templateFactory.exportName
+ );
+ target.replaceWith(t.callExpression(templateFactoryIdentifier, [templateExpression]));
+}
+
+function insertTransformedTemplate(
+ babel: typeof Babel,
+ state: State,
+ template: string,
+ target: NodePath | NodePath,
+ userTypedOptions: Record,
+ formatOptions: ModuleConfig
+) {
+ let t = babel.types;
+ let options = buildPrecompileOptions(babel, target, state, template, userTypedOptions);
+ let ast = state.normalizedOpts.compiler._preprocess(template, { ...options, mode: 'codemod' });
+ let transformed = state.normalizedOpts.compiler._print(ast);
+ if (target.isCallExpression()) {
+ (target.get('arguments.0') as NodePath).replaceWith(t.stringLiteral(transformed));
+ if (options.locals && options.locals.length > 0) {
+ if (!formatOptions.enableScope) {
+ maybePruneImport(state.util, target.get('callee'));
+ target.set('callee', precompileTemplate(state.util, target));
+ }
+ updateScope(babel, target, options.locals);
+ }
+ } else {
+ if (options.locals && options.locals.length > 0) {
+ // need to add scope, so need to replace the backticks form with a call
+ // expression to precompileTemplate
+ maybePruneImport(state.util, target.get('tag'));
+ let newCall = target.replaceWith(
+ t.callExpression(precompileTemplate(state.util, target), [t.stringLiteral(transformed)])
+ )[0];
+ updateScope(babel, newCall, options.locals);
+ } else {
+ (target.get('quasi').get('quasis.0') as NodePath).replaceWith(
+ t.templateElement({ raw: transformed })
+ );
+ }
+ }
+}
+
+function templateFactoryConfig(opts: Required) {
+ let moduleName = '@ember/template-factory';
+ let exportName = 'createTemplateFactory';
+ let overrides = opts.outputModuleOverrides[moduleName]?.[exportName];
+ return overrides
+ ? { exportName: overrides[0], moduleName: overrides[1] }
+ : { exportName, moduleName };
+}
+
+function buildScope(babel: typeof Babel, locals: string[]) {
+ let t = babel.types;
+ return t.arrowFunctionExpression(
+ [],
+ t.objectExpression(
+ locals.map((name) => t.objectProperty(t.identifier(name), t.identifier(name), false, true))
+ )
+ );
+}
+function updateScope(babel: typeof Babel, target: NodePath, locals: string[]) {
+ let t = babel.types;
+ let secondArg = target.get('arguments.1') as NodePath | undefined;
+ if (secondArg) {
+ let scope = secondArg.get('properties').find((p) => {
+ let key = p.get('key') as NodePath;
+ return key.isIdentifier() && key.node.name === 'scope';
+ });
+ if (scope) {
+ scope.set('value', buildScope(babel, locals));
+ } else {
+ secondArg.pushContainer(
+ 'properties',
+ t.objectProperty(t.identifier('scope'), buildScope(babel, locals))
+ );
+ }
+ } else {
+ target.pushContainer(
+ 'arguments',
+ t.objectExpression([t.objectProperty(t.identifier('scope'), buildScope(babel, locals))])
+ );
+ }
+}
+
+function maybePruneImport(
+ util: ImportUtil,
+ identifier: NodePath
+) {
+ if (!identifier.isIdentifier()) {
+ return;
+ }
+ let binding = identifier.scope.getBinding(identifier.node.name);
+ // this checks if the identifier (that we're about to remove) is used in
+ // exactly one place.
+ if (
+ binding?.referencePaths.reduce((count, path) => (path.removed ? count : count + 1), 0) === 1
+ ) {
+ let specifier = binding.path;
+ if (specifier.isImportSpecifier()) {
+ let declaration = specifier.parentPath as NodePath;
+ util.removeImport(declaration.node.source.value, name(specifier.node.imported));
+ }
+ }
+ identifier.removed = true;
+}
+
+function precompileTemplate(util: ImportUtil, target: NodePath) {
+ return util.import(target, '@ember/template-compilation', 'precompileTemplate');
+}
+
+function name(node: t.StringLiteral | t.Identifier) {
+ if (node.type === 'StringLiteral') {
+ return node.value;
+ } else {
+ return node.name;
+ }
+}
+
+export default makePlugin((options) => options);
diff --git a/src/public-types.ts b/src/public-types.ts
new file mode 100644
index 0000000..8ae2c9c
--- /dev/null
+++ b/src/public-types.ts
@@ -0,0 +1,6 @@
+export type LegacyModuleName =
+ | 'ember-cli-htmlbars'
+ | 'ember-cli-htmlbars-inline-precompile'
+ | 'htmlbars-inline-precompile';
+
+export type { JSUtils, WithJSUtils } from './js-utils';
diff --git a/yarn.lock b/yarn.lock
index 702e6c6..3042164 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1026,13 +1026,49 @@
resolved "https://registry.yarnpkg.com/@ember/edition-utils/-/edition-utils-1.2.0.tgz#a039f542dc14c8e8299c81cd5abba95e2459cfa6"
integrity sha512-VmVq/8saCaPdesQmftPqbFtxJWrzxNGSQ+e8x8LLe3Hjm36pJ04Q8LeORGZkAeOhldoUX9seLGmSaHeXkIqoog==
-"@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==
+"@glimmer/env@0.1.7":
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/@glimmer/env/-/env-0.1.7.tgz#fd2d2b55a9029c6b37a6c935e8c8871ae70dfa07"
+ integrity sha512-JKF/a9I9jw6fGoz8kA7LEQslrwJ5jms5CXhu/aqkBWk+PmZ6pTl8mlb/eJ/5ujBGTiQzBhy5AIWF712iA+4/mw==
+
+"@glimmer/interfaces@0.84.2":
+ version "0.84.2"
+ resolved "https://registry.yarnpkg.com/@glimmer/interfaces/-/interfaces-0.84.2.tgz#764cf92c954adcd1a851e5dc68ec1f6b654dc3bd"
+ integrity sha512-tMZxQpOddUVmHEOuripkNqVR7ba0K4doiYnFd4WyswqoHPlxqpBujbIamQ+bWCWEF0U4yxsXKa31ekS/JHkiBQ==
+ dependencies:
+ "@simple-dom/interface" "^1.4.0"
+
+"@glimmer/syntax@^0.84.2":
+ version "0.84.2"
+ resolved "https://registry.yarnpkg.com/@glimmer/syntax/-/syntax-0.84.2.tgz#a3f65e51eec20f6adb79c6159d1ad1166fa5bccd"
+ integrity sha512-SPBd1tpIR9XeaXsXsMRCnKz63eLnIZ0d5G9QC4zIBFBC3pQdtG0F5kWeuRVCdfTIFuR+5WBMfk5jvg+3gbQhjg==
+ dependencies:
+ "@glimmer/interfaces" "0.84.2"
+ "@glimmer/util" "0.84.2"
+ "@handlebars/parser" "~2.0.0"
+ simple-html-tokenizer "^0.5.11"
+
+"@glimmer/util@0.84.2":
+ version "0.84.2"
+ resolved "https://registry.yarnpkg.com/@glimmer/util/-/util-0.84.2.tgz#2711ba40f25f44b2ea309cad49f5c2622c6211bc"
+ integrity sha512-VbhzE2s4rmU+qJF3gGBTL1IDjq+/G2Th51XErS8MQVMCmE4CU2pdwSzec8PyOowqCGUOrVIWuMzEI6VoPM4L4w==
+ dependencies:
+ "@glimmer/env" "0.1.7"
+ "@glimmer/interfaces" "0.84.2"
+ "@simple-dom/interface" "^1.4.0"
+
+"@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"
+"@handlebars/parser@~2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@handlebars/parser/-/parser-2.0.0.tgz#5e8b7298f31ff8f7b260e6b7363c7e9ceed7d9c5"
+ integrity sha512-EP9uEDZv/L5Qh9IWuMUGJRfwhXJ4h1dqKTT4/3+tY0eu7sPis7xh23j61SYUnNF4vqCQvvUXpDo9Bh/+q1zASA==
+
"@iarna/toml@2.2.5":
version "2.2.5"
resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c"
@@ -1359,6 +1395,11 @@
dependencies:
"@octokit/openapi-types" "^7.3.2"
+"@simple-dom/interface@^1.4.0":
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/@simple-dom/interface/-/interface-1.4.0.tgz#e8feea579232017f89b0138e2726facda6fbb71f"
+ integrity sha512-l5qumKFWU0S+4ZzMaLXFU8tQZsicHEMEyAxI5kDFGhJsRqDwe0a7/iPA/GdxlGyDKseQQAgIz5kzU7eXTrlSpA==
+
"@sindresorhus/is@^0.14.0":
version "0.14.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
@@ -1369,6 +1410,13 @@
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.1.tgz#d26729db850fa327b7cacc5522252194404226f5"
integrity sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g==
+"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"
+ integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==
+ dependencies:
+ type-detect "4.0.8"
+
"@sinonjs/commons@^1.7.0":
version "1.7.2"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2"
@@ -1376,6 +1424,13 @@
dependencies:
type-detect "4.0.8"
+"@sinonjs/fake-timers@>=5", "@sinonjs/fake-timers@^9.1.2":
+ version "9.1.2"
+ resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c"
+ integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==
+ dependencies:
+ "@sinonjs/commons" "^1.7.0"
+
"@sinonjs/fake-timers@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40"
@@ -1383,6 +1438,20 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
+"@sinonjs/samsam@^6.1.1":
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-6.1.1.tgz#627f7f4cbdb56e6419fa2c1a3e4751ce4f6a00b1"
+ integrity sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==
+ dependencies:
+ "@sinonjs/commons" "^1.6.0"
+ lodash.get "^4.4.2"
+ type-detect "^4.0.8"
+
+"@sinonjs/text-encoding@^0.7.1":
+ version "0.7.2"
+ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918"
+ integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==
+
"@szmarczak/http-timer@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"
@@ -1554,6 +1623,18 @@
"@types/glob" "*"
"@types/node" "*"
+"@types/sinon@^10.0.13":
+ version "10.0.13"
+ resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.13.tgz#60a7a87a70d9372d0b7b38cc03e825f46981fb83"
+ integrity sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==
+ dependencies:
+ "@types/sinonjs__fake-timers" "*"
+
+"@types/sinonjs__fake-timers@*":
+ version "8.1.2"
+ resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz#bf2e02a3dbd4aecaf95942ecd99b7402e03fad5e"
+ integrity sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==
+
"@types/stack-utils@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff"
@@ -1896,10 +1977,10 @@ aws4@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
-babel-import-util@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/babel-import-util/-/babel-import-util-1.2.0.tgz#96dc3ad9a186b4383229bcce35e1c4fb4de9a0d8"
- integrity sha512-hsjWpyurnbXLNPcIKSkV6l23YfMSCLdA39C5pMh9VSFtGbGU+UFv5Uyz3u//pOOc/FUknGod1KgUd8KM8VQ07g==
+babel-import-util@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/babel-import-util/-/babel-import-util-1.3.0.tgz#dc9251ea39a7747bd586c1c13b8d785a42797f8e"
+ integrity sha512-PPzUT17eAI18zn6ek1R3sB4Krc/MbnmT1MkZQFmyhjoaEGBVwNABhfVU9+EKcDSKrrOm9OIpGhjxukx1GCiy1g==
babel-jest@^26.6.3:
version "26.6.3"
@@ -1915,7 +1996,7 @@ babel-jest@^26.6.3:
graceful-fs "^4.2.4"
slash "^3.0.0"
-babel-plugin-debug-macros@^0.3.3, babel-plugin-debug-macros@^0.3.4:
+babel-plugin-debug-macros@^0.3.4:
version "0.3.4"
resolved "https://registry.yarnpkg.com/babel-plugin-debug-macros/-/babel-plugin-debug-macros-0.3.4.tgz#22961d0cb851a80654cece807a8b4b73d85c6075"
integrity sha512-wfel/vb3pXfwIDZUrkoDrn5FHmlWI96PCJ3UCDv2a86poJ3EQrnArNW5KfHSVJ9IOgxHbo748cQt7sDU+0KCEw==
@@ -2430,9 +2511,9 @@ can-symlink@^1.0.0:
tmp "0.0.28"
caniuse-lite@^1.0.30001219:
- version "1.0.30001237"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001237.tgz#4b7783661515b8e7151fc6376cfd97f0e427b9e5"
- integrity sha512-pDHgRndit6p1NR2GhzMbQ6CkRrp4VKuSsqbcLeOQppYPKOYkKT/6ZvZDvKJUqcmtyWIAHuZq3SVS2vc1egCZzw==
+ version "1.0.30001373"
+ resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001373.tgz"
+ integrity sha512-pJYArGHrPp3TUqQzFYRmP/lwJlj8RCbVe3Gd3eJQkAV8SAC6b19XS9BjMvRdvaS8RMkaTN8ZhoHP6S1y8zzwEQ==
capture-exit@^2.0.0:
version "2.0.0"
@@ -2920,6 +3001,11 @@ diff-sequences@^26.6.2:
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1"
integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==
+diff@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
+ integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
+
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@@ -3066,17 +3152,17 @@ ember-router-generator@^2.0.0:
"@babel/traverse" "^7.4.5"
recast "^0.18.1"
-ember-source@^3.27.5:
- version "3.27.5"
- resolved "https://registry.yarnpkg.com/ember-source/-/ember-source-3.27.5.tgz#8e9ce24c17e7a16dc3c2b128d3d3e24ea79e6726"
- integrity sha512-oSGM9mD6BuOcGilYqU+F2MtCferQhKWO3REX1P9qgN1Wzfa5kXjbjBBdPNWfBtg7bZLGM27H8JgiV6+t3uGegA==
+ember-source@^3.28.9:
+ version "3.28.9"
+ resolved "https://registry.yarnpkg.com/ember-source/-/ember-source-3.28.9.tgz#804c56b2d71d3cc3decff15a3273bb35d668300a"
+ integrity sha512-Fy7V3yvj+3oyo2+ke52aaihKMcFnnF7Oj9ixj547yzh2faqRfqouB5ZSiwXFH8rxw22rKaM8DiuQO4JN2Ay6xQ==
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"
@@ -5125,6 +5211,11 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
+just-extend@^4.0.2:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744"
+ integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==
+
keyv@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
@@ -5244,6 +5335,11 @@ lodash.foreach@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
integrity sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=
+lodash.get@^4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+ integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
+
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
@@ -5633,6 +5729,17 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+nise@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.1.tgz#ac4237e0d785ecfcb83e20f389185975da5c31f3"
+ integrity sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==
+ dependencies:
+ "@sinonjs/commons" "^1.8.3"
+ "@sinonjs/fake-timers" ">=5"
+ "@sinonjs/text-encoding" "^0.7.1"
+ just-extend "^4.0.2"
+ path-to-regexp "^1.7.0"
+
node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
@@ -6032,6 +6139,13 @@ path-root@^0.1.1:
dependencies:
path-root-regex "^0.1.0"
+path-to-regexp@^1.7.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
+ integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
+ dependencies:
+ isarray "0.0.1"
+
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -6799,6 +6913,23 @@ silent-error@^1.0.0, silent-error@^1.1.1:
dependencies:
debug "^2.2.0"
+simple-html-tokenizer@^0.5.11:
+ version "0.5.11"
+ resolved "https://registry.yarnpkg.com/simple-html-tokenizer/-/simple-html-tokenizer-0.5.11.tgz#4c5186083c164ba22a7b477b7687ac056ad6b1d9"
+ integrity sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og==
+
+sinon@^14.0.0:
+ version "14.0.0"
+ resolved "https://registry.yarnpkg.com/sinon/-/sinon-14.0.0.tgz#203731c116d3a2d58dc4e3cbe1f443ba9382a031"
+ integrity sha512-ugA6BFmE+WrJdh0owRZHToLd32Uw3Lxq6E6LtNRU+xTVBefx632h03Q7apXWRsRdZAJ41LB8aUfn2+O4jsDNMw==
+ dependencies:
+ "@sinonjs/commons" "^1.8.3"
+ "@sinonjs/fake-timers" "^9.1.2"
+ "@sinonjs/samsam" "^6.1.1"
+ diff "^5.0.0"
+ nise "^5.1.1"
+ supports-color "^7.2.0"
+
sisteransi@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.0.tgz#77d9622ff909080f1c19e5f4a1df0c1b0a27b88c"
@@ -7108,7 +7239,7 @@ supports-color@^5.3.0:
dependencies:
has-flag "^3.0.0"
-supports-color@^7.0.0, supports-color@^7.1.0:
+supports-color@^7.0.0, supports-color@^7.1.0, supports-color@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
@@ -7343,7 +7474,7 @@ type-check@~0.3.2:
dependencies:
prelude-ls "~1.1.2"
-type-detect@4.0.8:
+type-detect@4.0.8, type-detect@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==