Skip to content

Commit bbf5ac2

Browse files
authored
Merge pull request #33 from patricklx/patch-3
pass correct scope to glimmer
2 parents cd3ce26 + 3e8ee07 commit bbf5ac2

File tree

7 files changed

+265
-122
lines changed

7 files changed

+265
-122
lines changed

__tests__/tests.ts

+56-14
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,31 @@ describe('htmlbars-inline-precompile', function () {
996996
expect(transformed).toContain(`window.define('my-app/components/thing', thing)`);
997997
});
998998

999+
it('prevents inconsistent external manipulation of the locals array', function () {
1000+
let compatTransform: ExtendedPluginBuilder = (env) => {
1001+
return {
1002+
name: 'compat-transform',
1003+
visitor: {
1004+
Template() {
1005+
(env as any).locals.push('NewThing');
1006+
},
1007+
},
1008+
};
1009+
};
1010+
1011+
plugins = [[HTMLBarsInlinePrecompile, { targetFormat: 'hbs', transforms: [compatTransform] }]];
1012+
1013+
expect(() => {
1014+
transform(stripIndent`
1015+
import { precompileTemplate } from '@ember/template-compilation';
1016+
let NewThing = Thing;
1017+
export default function() {
1018+
const template = precompileTemplate('<Thing />');
1019+
}
1020+
`);
1021+
}).toThrow(/The only supported way to manipulate locals is via the jsutils API/);
1022+
});
1023+
9991024
it('can emit side-effectful import', function () {
10001025
let compatTransform: ExtendedPluginBuilder = (env) => {
10011026
return {
@@ -1529,7 +1554,7 @@ describe('htmlbars-inline-precompile', function () {
15291554

15301555
describe('scope', function () {
15311556
it('correctly handles scope function (non-block arrow function)', function () {
1532-
let source = 'hello';
1557+
let source = '<foo /><bar/>';
15331558
let spy = sinon.spy(compiler, 'precompile');
15341559

15351560
transform(
@@ -1539,7 +1564,7 @@ describe('htmlbars-inline-precompile', function () {
15391564
});
15401565

15411566
it('correctly handles scope function (block arrow function)', function () {
1542-
let source = 'hello';
1567+
let source = '<foo /><bar/>';
15431568
let spy = sinon.spy(compiler, 'precompile');
15441569

15451570
transform(
@@ -1550,7 +1575,7 @@ describe('htmlbars-inline-precompile', function () {
15501575
});
15511576

15521577
it('correctly handles scope function (normal function)', function () {
1553-
let source = 'hello';
1578+
let source = '<foo /><bar/>';
15541579
let spy = sinon.spy(compiler, 'precompile');
15551580

15561581
transform(
@@ -1561,7 +1586,7 @@ describe('htmlbars-inline-precompile', function () {
15611586
});
15621587

15631588
it('correctly handles scope function (object method)', function () {
1564-
let source = 'hello';
1589+
let source = '<foo /><bar/>';
15651590
let spy = sinon.spy(compiler, 'precompile');
15661591

15671592
transform(
@@ -1571,7 +1596,7 @@ describe('htmlbars-inline-precompile', function () {
15711596
});
15721597

15731598
it('correctly handles scope function with coverage', function () {
1574-
let source = 'hello';
1599+
let source = '<foo /><bar/>';
15751600
let spy = sinon.spy(compiler, 'precompile');
15761601

15771602
transform(
@@ -1626,6 +1651,26 @@ describe('htmlbars-inline-precompile', function () {
16261651
/Scope objects for `precompileTemplate` may only contain direct references to in-scope values, e.g. { bar } or { bar: bar }/
16271652
);
16281653
});
1654+
1655+
it('correctly removes not used scope', function () {
1656+
let spy = sinon.spy(compiler, 'precompile');
1657+
transform(`
1658+
import { precompileTemplate } from '@ember/template-compilation';
1659+
let foo, bar;
1660+
var compiled = precompileTemplate('<foo /><bar/>', { scope: () => ({ foo, bar, baz }) });
1661+
`);
1662+
expect(spy.firstCall.lastArg).toHaveProperty('locals', ['foo', 'bar']);
1663+
});
1664+
1665+
it('does not automagically add to scope when not using implicit-scope-form', function () {
1666+
let spy = sinon.spy(compiler, 'precompile');
1667+
transform(`
1668+
import { precompileTemplate } from '@ember/template-compilation';
1669+
let foo, bar;
1670+
var compiled = precompileTemplate('<foo /><bar/>', { scope: () => ({ bar }) });
1671+
`);
1672+
expect(spy.firstCall.lastArg).toHaveProperty('locals', ['bar']);
1673+
});
16291674
});
16301675

16311676
describe('implicit-scope-form', function () {
@@ -1660,9 +1705,7 @@ describe('htmlbars-inline-precompile', function () {
16601705
// that's what the lint rules are for. When it comes to correctness, we need
16611706
// our scope to behave like real Javascript, and Javascript doesn't care
16621707
// whether you've (for example) capitalized your variable identifier.
1663-
//
1664-
// needs https://github.com/glimmerjs/glimmer-vm/pull/1421
1665-
it.skip('shadows html elements with locals', function () {
1708+
it('shadows html elements with locals', function () {
16661709
plugins = [
16671710
[
16681711
HTMLBarsInlinePrecompile,
@@ -1681,9 +1724,9 @@ describe('htmlbars-inline-precompile', function () {
16811724
);
16821725

16831726
expect(transformed).toEqualCode(`
1684-
import templateOnly from "@ember/component/template-only";
1685-
import { setComponentTemplate } from "@ember/component";
16861727
import { precompileTemplate } from "@ember/template-compilation";
1728+
import { setComponentTemplate } from "@ember/component";
1729+
import templateOnly from "@ember/component/template-only";
16871730
let div = 1;
16881731
export default setComponentTemplate(precompileTemplate('<div></div>', { scope: () => ({ div }), strictMode: true }), templateOnly());
16891732
`);
@@ -1716,8 +1759,7 @@ describe('htmlbars-inline-precompile', function () {
17161759
`);
17171760
});
17181761

1719-
// needs https://github.com/glimmerjs/glimmer-vm/pull/1421
1720-
it.skip('leaves ember keywords alone when no local is defined', function () {
1762+
it('leaves ember keywords alone when no local is defined', function () {
17211763
plugins = [
17221764
[
17231765
HTMLBarsInlinePrecompile,
@@ -1735,9 +1777,9 @@ describe('htmlbars-inline-precompile', function () {
17351777
);
17361778

17371779
expect(transformed).toEqualCode(`
1738-
import templateOnly from "@ember/component/template-only";
1739-
import { setComponentTemplate } from "@ember/component";
17401780
import { precompileTemplate } from "@ember/template-compilation";
1781+
import { setComponentTemplate } from "@ember/component";
1782+
import templateOnly from "@ember/component/template-only";
17411783
export default setComponentTemplate(precompileTemplate('{{hasBlock "thing"}}', { strictMode: true }), templateOnly());
17421784
`);
17431785
});

src/expression-parser.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export class ExpressionParser {
107107

108108
res.add(propName, value.name);
109109
return res;
110-
}, new ScopeLocals());
110+
}, new ScopeLocals({ mode: 'explicit' }));
111111
}
112112

113113
parseEval(

src/hbs-utils.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { ASTv1, WalkerPath } from '@glimmer/syntax';
2+
3+
export function astNodeHasBinding(target: WalkerPath<ASTv1.Node>, name: string): boolean {
4+
let cursor: WalkerPath<ASTv1.Node> | null = target;
5+
while (cursor) {
6+
let parentNode = cursor.parent?.node;
7+
if (
8+
parentNode?.type === 'ElementNode' &&
9+
parentNode.blockParams.includes(name) &&
10+
// an ElementNode's block params are valid only within its children
11+
parentNode.children.includes(cursor.node as ASTv1.Statement)
12+
) {
13+
return true;
14+
}
15+
16+
if (
17+
parentNode?.type === 'Block' &&
18+
parentNode.blockParams.includes(name) &&
19+
// a Block's blockParams are valid only within its body
20+
parentNode.body.includes(cursor.node as ASTv1.Statement)
21+
) {
22+
return true;
23+
}
24+
25+
cursor = cursor.parent;
26+
}
27+
return false;
28+
}

src/js-utils.ts

+29-63
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type * as Babel from '@babel/core';
33
import type { NodePath } from '@babel/traverse';
44
import type { ASTPluginBuilder, ASTPluginEnvironment, ASTv1, WalkerPath } from '@glimmer/syntax';
55
import type { ImportUtil } from 'babel-import-util';
6-
import { ScopeLocals } from './scope-locals';
6+
import { astNodeHasBinding } from './hbs-utils';
77

88
interface State {
99
program: NodePath<Babel.types.Program>;
@@ -16,23 +16,20 @@ export class JSUtils {
1616
#babel: typeof Babel;
1717
#state: State;
1818
#template: NodePath<t.Expression>;
19-
#scopeLocals: ScopeLocals;
19+
#addedBinding: (name: string) => void;
2020
#importer: ImportUtil;
2121

2222
constructor(
2323
babel: typeof Babel,
2424
state: State,
2525
template: NodePath<t.Expression>,
26-
// mapping of handlebars identifiers to javascript identifiers, as appears
27-
// in the `scope` argument to precompileTemplate. This is both read and
28-
// write -- we might put more stuff into it.
29-
scopeLocals: ScopeLocals,
26+
addedBinding: (name: string) => void,
3027
importer: ImportUtil
3128
) {
3229
this.#babel = babel;
3330
this.#state = state;
3431
this.#template = template;
35-
this.#scopeLocals = scopeLocals;
32+
this.#addedBinding = addedBinding;
3633
this.#importer = importer;
3734

3835
if (!this.#state.lastInsertedPath) {
@@ -71,9 +68,7 @@ export class JSUtils {
7168
let name = unusedNameLike(
7269
opts?.nameHint ?? 'a',
7370
(candidate) =>
74-
this.#template.scope.hasBinding(candidate) ||
75-
this.#scopeLocals.has(candidate) ||
76-
astNodeHasBinding(target, candidate)
71+
this.#template.scope.hasBinding(candidate) || astNodeHasBinding(target, candidate)
7772
);
7873
let t = this.#babel.types;
7974
let declaration: NodePath<t.VariableDeclaration> = this.#emitStatement(
@@ -84,8 +79,8 @@ export class JSUtils {
8479
),
8580
])
8681
);
87-
declaration.scope.registerBinding('module', declaration.get('declarations.0') as NodePath);
88-
this.#scopeLocals.add(name);
82+
declaration.scope.registerBinding('let', declaration.get('declarations.0') as NodePath);
83+
this.#addedBinding(name);
8984
return name;
9085
}
9186

@@ -126,36 +121,34 @@ export class JSUtils {
126121
opts?.nameHint
127122
);
128123

129-
// If we're already referencing the imported name from the outer scope and
130-
// it's not shadowed at our target location in the template, we can reuse
131-
// the existing import.
132-
if (
133-
this.#scopeLocals.has(importedIdentifier.name) &&
134-
!astNodeHasBinding(target, importedIdentifier.name)
135-
) {
124+
// Simple base case: the JS name that's available is also unused at our spot
125+
// in HBS, so just use it.
126+
if (!astNodeHasBinding(target, importedIdentifier.name)) {
127+
this.#addedBinding(importedIdentifier.name);
136128
return importedIdentifier.name;
137129
}
138130

131+
// The importedIdentifier that we have in Javascript is not usable within
132+
// our HBS because it's shadowed by a block param. So we will introduce a
133+
// second name via a variable declaration.
134+
//
135+
// The reason we don't force the import itself to have this name is that
136+
// we might be re-using an existing import, and we don't want to go
137+
// rewriting all of its callsites that are unrelated to us.
139138
let identifier = unusedNameLike(
140139
importedIdentifier.name,
141-
(candidate) => this.#scopeLocals.has(candidate) || astNodeHasBinding(target, candidate)
140+
(candidate) =>
141+
this.#template.scope.hasBinding(candidate) || astNodeHasBinding(target, candidate)
142142
);
143-
if (identifier !== importedIdentifier.name) {
144-
// The importedIdentifier that we have in Javascript is not usable within
145-
// our HBS because it's shadowed by a block param. So we will introduce a
146-
// second name via a variable declaration.
147-
//
148-
// The reason we don't force the import itself to have this name is that
149-
// we might be re-using an existing import, and we don't want to go
150-
// rewriting all of its callsites that are unrelated to us.
151-
let t = this.#babel.types;
152-
this.#emitStatement(
153-
t.variableDeclaration('let', [
154-
t.variableDeclarator(t.identifier(identifier), importedIdentifier),
155-
])
156-
);
157-
}
158-
this.#scopeLocals.add(identifier);
143+
let t = this.#babel.types;
144+
let declaration = this.#emitStatement(
145+
t.variableDeclaration('let', [
146+
t.variableDeclarator(t.identifier(identifier), importedIdentifier),
147+
])
148+
);
149+
declaration.scope.registerBinding('let', declaration.get('declarations.0') as NodePath);
150+
151+
this.#addedBinding(identifier);
159152
return identifier;
160153
}
161154

@@ -223,33 +216,6 @@ function unusedNameLike(desiredName: string, isUsed: (name: string) => boolean):
223216
return candidate;
224217
}
225218

226-
function astNodeHasBinding(target: WalkerPath<ASTv1.Node>, name: string): boolean {
227-
let cursor: WalkerPath<ASTv1.Node> | null = target;
228-
while (cursor) {
229-
let parentNode = cursor.parent?.node;
230-
if (
231-
parentNode?.type === 'ElementNode' &&
232-
parentNode.blockParams.includes(name) &&
233-
// an ElementNode's block params are valid only within its children
234-
parentNode.children.includes(cursor.node as ASTv1.Statement)
235-
) {
236-
return true;
237-
}
238-
239-
if (
240-
parentNode?.type === 'Block' &&
241-
parentNode.blockParams.includes(name) &&
242-
// a Block's blockParams are valid only within its body
243-
parentNode.body.includes(cursor.node as ASTv1.Statement)
244-
) {
245-
return true;
246-
}
247-
248-
cursor = cursor.parent;
249-
}
250-
return false;
251-
}
252-
253219
/**
254220
* This extends Glimmer's ASTPluginEnvironment type to put our jsutils into meta
255221
*/

0 commit comments

Comments
 (0)