Skip to content

Commit

Permalink
Preserve imports used within templates
Browse files Browse the repository at this point in the history
Builds off #31 to fix #30.

Instead of keeping *everything* as in #31 (which is not safe in general), we use the `pre` to take a snapshot of available imports and then only when we discover that a template wants to use a name that is not in scope do we check if it was in the original set of available imports and reintroduce an import for it.
  • Loading branch information
ef4 committed Nov 1, 2023
1 parent 46c0a74 commit 3d9f9d4
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 25 deletions.
33 changes: 33 additions & 0 deletions __tests__/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1811,6 +1811,39 @@ describe('htmlbars-inline-precompile', function () {
`);
});

it('respects local priority when inter-operating with @babel/plugin-transform-typescript', function () {
plugins = [
[
HTMLBarsInlinePrecompile,
{
compiler,
targetFormat: 'hbs',
},
],
TransformTypescript,
];

let transformed = transform(
`import { template } from '@ember/template-compiler';
import HelloWorld from 'somewhere';
export default function() {
let { HelloWorld } = globalThis;
return template('<HelloWorld />', { eval: function() { return eval(arguments[0]) } })
}
`
);

expect(transformed).toEqualCode(`
import { precompileTemplate } from "@ember/template-compilation";
import { setComponentTemplate } from "@ember/component";
import templateOnly from "@ember/component/template-only";
export default function() {
let { HelloWorld } = globalThis;
return setComponentTemplate(precompileTemplate('<HelloWorld />', { scope: () => ({ HelloWorld }), strictMode: true }), templateOnly());
}
`);
});

it('interoperates correctly with @babel/plugin-transform-typescript when handling locals with wire target', function () {
plugins = [
[
Expand Down
71 changes: 47 additions & 24 deletions src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NodePath } from '@babel/traverse';
import type { NodePath } from '@babel/traverse';
import type * as Babel from '@babel/core';
import type { types as t } from '@babel/core';
import { ImportUtil } from 'babel-import-util';
Expand Down Expand Up @@ -147,6 +147,7 @@ interface State<EnvSpecificOptions> {
lastInsertedPath: NodePath<t.Statement> | undefined;
filename: string;
recursionGuard: Set<unknown>;
originalImportedNames: Map<string, [string, string]>;
}

export function makePlugin<EnvSpecificOptions>(loadOptions: (opts: EnvSpecificOptions) => Options) {
Expand All @@ -156,29 +157,21 @@ export function makePlugin<EnvSpecificOptions>(loadOptions: (opts: EnvSpecificOp
let t = babel.types;

return {
pre(state) {
const imports = state.ast.program.body.filter(
(b) => b.type === 'ImportDeclaration'
) as t.ImportDeclaration[];
const templateCompilerImport = imports.find(
(i) => i.source.value === '@ember/template-compiler'
);

if (templateCompilerImport) {
const program = NodePath.get({
hub: state.hub,
key: 'program',
parent: state.ast,
parentPath: null,
container: state.ast,
});
for (const i of imports) {
const specifiers = i.specifiers;
for (const specifier of specifiers) {
const local = specifier.local;
if (!state.scope.getBinding(local.name)?.referencePaths.length) {
state.scope.getBinding(local.name)?.referencePaths.push(program);
}
pre(this: State<EnvSpecificOptions>, file) {
// Remember the available set of imported names very early here in <pre>
// so that when other plugins (particularly
// @babel/plugin-transform-typescript) drop "unused" imports in their
// own Program.enter we still know about them. If we want to use them
// from inside a template, they weren't really unused and we can ensure
// they continue to exist.
this.originalImportedNames = new Map();
for (let statement of file.ast.program.body) {
if (statement.type === 'ImportDeclaration') {
for (let specifier of statement.specifiers) {
this.originalImportedNames.set(specifier.local.name, [
statement.source.value,
importedName(specifier),
]);
}
}
}
Expand Down Expand Up @@ -520,6 +513,7 @@ function insertCompiledTemplate<EnvSpecificOptions>(
configFile: false,
}) as t.File;

ensureImportedNames(target, scopeLocals, state.util, state.originalImportedNames);
remapIdentifiers(precompileResultAST, babel, scopeLocals);

let templateExpression = (precompileResultAST.program.body[0] as t.VariableDeclaration)
Expand Down Expand Up @@ -585,6 +579,7 @@ function insertTransformedTemplate<EnvSpecificOptions>(
maybePruneImport(state.util, target.get('callee'));
target.set('callee', precompileTemplate(state.util, target));
}
ensureImportedNames(target, scopeLocals, state.util, state.originalImportedNames);
updateScope(babel, target, scopeLocals);
}

Expand Down Expand Up @@ -619,6 +614,7 @@ function insertTransformedTemplate<EnvSpecificOptions>(
let newCall = target.replaceWith(
t.callExpression(precompileTemplate(state.util, target), [t.stringLiteral(transformed)])
)[0];
ensureImportedNames(newCall, scopeLocals, state.util, state.originalImportedNames);
updateScope(babel, newCall, scopeLocals);
} else {
(target.get('quasi').get('quasis.0') as NodePath<t.TemplateElement>).replaceWith(
Expand Down Expand Up @@ -756,4 +752,31 @@ function name(node: t.StringLiteral | t.Identifier) {
}
}

function ensureImportedNames(
target: NodePath<t.Node>,
scopeLocals: ScopeLocals,
util: ImportUtil,
originalImportedNames: Map<string, [string, string]>
) {
for (let [nameInTemplate, identifier] of scopeLocals.entries()) {
if (!target.scope.getBinding(identifier)) {
let available = originalImportedNames.get(identifier);
if (available) {
let newIdent = util.import(target, available[0], available[1], identifier);
scopeLocals.add(nameInTemplate, newIdent.name);
}
}
}
}

function importedName(node: t.ImportDeclaration['specifiers'][number]): string {
if (node.type === 'ImportDefaultSpecifier') {
return 'default';
} else if (node.type === 'ImportNamespaceSpecifier') {
return '*';
} else {
return name(node.imported);
}
}

export default makePlugin<Options>((options) => options);
4 changes: 3 additions & 1 deletion src/scope-locals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export class ScopeLocals {

add(key: string, value?: string) {
this.#mapping[key] = value ?? key;
this.#locals.push(key);
if (!this.#locals.includes(key)) {
this.#locals.push(key);
}
}
}

0 comments on commit 3d9f9d4

Please sign in to comment.