Skip to content

Commit 3d9f9d4

Browse files
committed
Preserve imports used within templates
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.
1 parent 46c0a74 commit 3d9f9d4

File tree

3 files changed

+83
-25
lines changed

3 files changed

+83
-25
lines changed

__tests__/tests.ts

+33
Original file line numberDiff line numberDiff line change
@@ -1811,6 +1811,39 @@ describe('htmlbars-inline-precompile', function () {
18111811
`);
18121812
});
18131813

1814+
it('respects local priority when inter-operating with @babel/plugin-transform-typescript', function () {
1815+
plugins = [
1816+
[
1817+
HTMLBarsInlinePrecompile,
1818+
{
1819+
compiler,
1820+
targetFormat: 'hbs',
1821+
},
1822+
],
1823+
TransformTypescript,
1824+
];
1825+
1826+
let transformed = transform(
1827+
`import { template } from '@ember/template-compiler';
1828+
import HelloWorld from 'somewhere';
1829+
export default function() {
1830+
let { HelloWorld } = globalThis;
1831+
return template('<HelloWorld />', { eval: function() { return eval(arguments[0]) } })
1832+
}
1833+
`
1834+
);
1835+
1836+
expect(transformed).toEqualCode(`
1837+
import { precompileTemplate } from "@ember/template-compilation";
1838+
import { setComponentTemplate } from "@ember/component";
1839+
import templateOnly from "@ember/component/template-only";
1840+
export default function() {
1841+
let { HelloWorld } = globalThis;
1842+
return setComponentTemplate(precompileTemplate('<HelloWorld />', { scope: () => ({ HelloWorld }), strictMode: true }), templateOnly());
1843+
}
1844+
`);
1845+
});
1846+
18141847
it('interoperates correctly with @babel/plugin-transform-typescript when handling locals with wire target', function () {
18151848
plugins = [
18161849
[

src/plugin.ts

+47-24
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { NodePath } from '@babel/traverse';
1+
import type { NodePath } from '@babel/traverse';
22
import type * as Babel from '@babel/core';
33
import type { types as t } from '@babel/core';
44
import { ImportUtil } from 'babel-import-util';
@@ -147,6 +147,7 @@ interface State<EnvSpecificOptions> {
147147
lastInsertedPath: NodePath<t.Statement> | undefined;
148148
filename: string;
149149
recursionGuard: Set<unknown>;
150+
originalImportedNames: Map<string, [string, string]>;
150151
}
151152

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

158159
return {
159-
pre(state) {
160-
const imports = state.ast.program.body.filter(
161-
(b) => b.type === 'ImportDeclaration'
162-
) as t.ImportDeclaration[];
163-
const templateCompilerImport = imports.find(
164-
(i) => i.source.value === '@ember/template-compiler'
165-
);
166-
167-
if (templateCompilerImport) {
168-
const program = NodePath.get({
169-
hub: state.hub,
170-
key: 'program',
171-
parent: state.ast,
172-
parentPath: null,
173-
container: state.ast,
174-
});
175-
for (const i of imports) {
176-
const specifiers = i.specifiers;
177-
for (const specifier of specifiers) {
178-
const local = specifier.local;
179-
if (!state.scope.getBinding(local.name)?.referencePaths.length) {
180-
state.scope.getBinding(local.name)?.referencePaths.push(program);
181-
}
160+
pre(this: State<EnvSpecificOptions>, file) {
161+
// Remember the available set of imported names very early here in <pre>
162+
// so that when other plugins (particularly
163+
// @babel/plugin-transform-typescript) drop "unused" imports in their
164+
// own Program.enter we still know about them. If we want to use them
165+
// from inside a template, they weren't really unused and we can ensure
166+
// they continue to exist.
167+
this.originalImportedNames = new Map();
168+
for (let statement of file.ast.program.body) {
169+
if (statement.type === 'ImportDeclaration') {
170+
for (let specifier of statement.specifiers) {
171+
this.originalImportedNames.set(specifier.local.name, [
172+
statement.source.value,
173+
importedName(specifier),
174+
]);
182175
}
183176
}
184177
}
@@ -520,6 +513,7 @@ function insertCompiledTemplate<EnvSpecificOptions>(
520513
configFile: false,
521514
}) as t.File;
522515

516+
ensureImportedNames(target, scopeLocals, state.util, state.originalImportedNames);
523517
remapIdentifiers(precompileResultAST, babel, scopeLocals);
524518

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

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

755+
function ensureImportedNames(
756+
target: NodePath<t.Node>,
757+
scopeLocals: ScopeLocals,
758+
util: ImportUtil,
759+
originalImportedNames: Map<string, [string, string]>
760+
) {
761+
for (let [nameInTemplate, identifier] of scopeLocals.entries()) {
762+
if (!target.scope.getBinding(identifier)) {
763+
let available = originalImportedNames.get(identifier);
764+
if (available) {
765+
let newIdent = util.import(target, available[0], available[1], identifier);
766+
scopeLocals.add(nameInTemplate, newIdent.name);
767+
}
768+
}
769+
}
770+
}
771+
772+
function importedName(node: t.ImportDeclaration['specifiers'][number]): string {
773+
if (node.type === 'ImportDefaultSpecifier') {
774+
return 'default';
775+
} else if (node.type === 'ImportNamespaceSpecifier') {
776+
return '*';
777+
} else {
778+
return name(node.imported);
779+
}
780+
}
781+
759782
export default makePlugin<Options>((options) => options);

src/scope-locals.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export class ScopeLocals {
3636

3737
add(key: string, value?: string) {
3838
this.#mapping[key] = value ?? key;
39-
this.#locals.push(key);
39+
if (!this.#locals.includes(key)) {
40+
this.#locals.push(key);
41+
}
4042
}
4143
}

0 commit comments

Comments
 (0)