Skip to content

Commit fe71b80

Browse files
Merge pull request #21336 from NullVoxPopuli-ai-agent/nvp/array-as-keyword
RFC#1000 - {{array}} as keyword
2 parents 64946ff + a5c9e79 commit fe71b80

5 files changed

Lines changed: 205 additions & 2 deletions

File tree

packages/@ember/template-compiler/lib/compile-options.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fn, hash } from '@ember/helper';
1+
import { array, fn, hash } from '@ember/helper';
22
import { on } from '@ember/modifier';
33
import { assert } from '@ember/debug';
44
import {
@@ -25,6 +25,7 @@ function malformedComponentLookup(string: string) {
2525
export const RUNTIME_KEYWORDS_NAME = '__ember_keywords__';
2626

2727
export const keywords: Record<string, unknown> = {
28+
array,
2829
fn,
2930
hash,
3031
on,

packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export default function autoImportBuiltins(env: EmberASTPluginEnvironment): ASTP
2727
}
2828
},
2929
SubExpression(node: AST.SubExpression) {
30+
if (isArray(node, hasLocal)) {
31+
rewriteKeyword(env, node, 'array', '@ember/helper');
32+
}
3033
if (isFn(node, hasLocal)) {
3134
rewriteKeyword(env, node, 'fn', '@ember/helper');
3235
}
@@ -35,6 +38,9 @@ export default function autoImportBuiltins(env: EmberASTPluginEnvironment): ASTP
3538
}
3639
},
3740
MustacheStatement(node: AST.MustacheStatement) {
41+
if (isArray(node, hasLocal)) {
42+
rewriteKeyword(env, node, 'array', '@ember/helper');
43+
}
3844
if (isFn(node, hasLocal)) {
3945
rewriteKeyword(env, node, 'fn', '@ember/helper');
4046
}
@@ -68,6 +74,13 @@ function isOn(
6874
return isPath(node.path) && node.path.original === 'on' && !hasLocal('on');
6975
}
7076

77+
function isArray(
78+
node: AST.MustacheStatement | AST.SubExpression,
79+
hasLocal: (k: string) => boolean
80+
): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } {
81+
return isPath(node.path) && node.path.original === 'array' && !hasLocal('array');
82+
}
83+
7184
function isFn(
7285
node: AST.MustacheStatement | AST.SubExpression,
7386
hasLocal: (k: string) => boolean
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { castToBrowser } from '@glimmer/debug-util';
2+
import {
3+
GlimmerishComponent,
4+
jitSuite,
5+
RenderTest,
6+
test,
7+
} from '@glimmer-workspace/integration-tests';
8+
9+
import { template } from '@ember/template-compiler/runtime';
10+
11+
class KeywordArrayRuntime extends RenderTest {
12+
static suiteName = 'keyword helper: array (runtime)';
13+
14+
@test
15+
'it works'() {
16+
const compiled = template('{{JSON.stringify (array "hello" "goodbye")}}', {
17+
strictMode: true,
18+
scope: () => ({ JSON }),
19+
});
20+
21+
this.renderComponent(compiled);
22+
this.assertHTML('["hello","goodbye"]');
23+
}
24+
25+
@test
26+
'it works (shadowed)'() {
27+
const array = (x: string) => x.toUpperCase();
28+
const compiled = template('{{array "hello"}}', {
29+
strictMode: true,
30+
scope: () => ({ JSON, array }),
31+
});
32+
33+
this.renderComponent(compiled);
34+
this.assertHTML('HELLO');
35+
}
36+
37+
@test
38+
'implicit scope'() {
39+
const compiled = template('{{JSON.stringify (array "hello" "goodbye")}}', {
40+
strictMode: true,
41+
eval() {
42+
return eval(arguments[0]);
43+
},
44+
});
45+
46+
this.renderComponent(compiled);
47+
this.assertHTML('["hello","goodbye"]');
48+
}
49+
50+
@test
51+
'implicit scope (shadowed)'() {
52+
const array = (...data: string[]) => data.reverse();
53+
54+
hide(array);
55+
56+
const compiled = template('{{JSON.stringify (array "hello" "goodbye")}}', {
57+
strictMode: true,
58+
eval() {
59+
return eval(arguments[0]);
60+
},
61+
});
62+
63+
this.renderComponent(compiled);
64+
this.assertHTML('["goodbye","hello"]');
65+
}
66+
67+
@test
68+
'no eval and no scope'(assert: Assert) {
69+
let receivedData: unknown[] | undefined;
70+
71+
class Foo extends GlimmerishComponent {
72+
static {
73+
template(
74+
'<button {{on "click" (fn this.capture (array "hello" "goodbye"))}}>Click</button>',
75+
{
76+
strictMode: true,
77+
component: this,
78+
}
79+
);
80+
}
81+
82+
capture = (data: unknown[]) => {
83+
receivedData = data;
84+
assert.step('captured');
85+
};
86+
}
87+
88+
this.renderComponent(Foo);
89+
90+
castToBrowser(this.element, 'div').querySelector('button')!.click();
91+
assert.verifySteps(['captured']);
92+
assert.deepEqual(receivedData, ['hello', 'goodbye']);
93+
}
94+
}
95+
96+
jitSuite(KeywordArrayRuntime);
97+
98+
/**
99+
* This function is used to hide a variable from the transpiler, so that it
100+
* doesn't get removed as "unused". It does not actually do anything with the
101+
* variable, it just makes it be part of an expression that the transpiler
102+
* won't remove.
103+
*
104+
* It's a bit of a hack, but it's necessary for testing.
105+
*
106+
* @param variable The variable to hide.
107+
*/
108+
const hide = (variable: unknown) => {
109+
new Function(`return (${JSON.stringify(variable)});`);
110+
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests';
2+
3+
import { template } from '@ember/template-compiler';
4+
5+
class KeywordArray extends RenderTest {
6+
static suiteName = 'keyword helper: array';
7+
8+
@test
9+
'it works'() {
10+
const compiled = template('{{JSON.stringify (array "hello" "goodbye")}}', {
11+
strictMode: true,
12+
scope: () => ({ JSON }),
13+
});
14+
15+
this.renderComponent(compiled);
16+
this.assertHTML('["hello","goodbye"]');
17+
}
18+
19+
@test
20+
'it works (shadowed)'() {
21+
const array = (x: string) => x.toUpperCase();
22+
const compiled = template('{{array "hello"}}', {
23+
strictMode: true,
24+
scope: () => ({ JSON, array }),
25+
});
26+
27+
this.renderComponent(compiled);
28+
this.assertHTML('HELLO');
29+
}
30+
}
31+
32+
jitSuite(KeywordArray);
33+
34+
/**
35+
* This function is used to hide a variable from the transpiler, so that it
36+
* doesn't get removed as "unused". It does not actually do anything with the
37+
* variable, it just makes it be part of an expression that the transpiler
38+
* won't remove.
39+
*
40+
* It's a bit of a hack, but it's necessary for testing.
41+
*
42+
* @param variable The variable to hide.
43+
*/
44+
const hide = (variable: unknown) => {
45+
new Function(`return (${JSON.stringify(variable)});`);
46+
};

smoke-tests/scenarios/basic-test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ function basicTest(scenarios: Scenarios, appName: string) {
516516
import { setupRenderingTest } from 'ember-qunit';
517517
import { render } from '@ember/test-helpers';
518518
519-
module('{{hash}} as keyword', function(hooks) {
519+
module('{{hash}} as keyword (shadowed)', function(hooks) {
520520
setupRenderingTest(hooks);
521521
522522
test('it works', async function(assert) {
@@ -526,6 +526,39 @@ function basicTest(scenarios: Scenarios, appName: string) {
526526
});
527527
});
528528
`,
529+
'array-as-keyword-test.gjs': `
530+
import { module, test } from 'qunit';
531+
import { setupRenderingTest } from 'ember-qunit';
532+
import { render } from '@ember/test-helpers';
533+
534+
module('{{array}} as keyword', function(hooks) {
535+
setupRenderingTest(hooks);
536+
537+
test('it works', async function(assert) {
538+
await render(
539+
<template>
540+
{{JSON.stringify (array "hello" "goodbye")}}
541+
</template>
542+
);
543+
assert.dom().hasText('["hello","goodbye"]');
544+
});
545+
});
546+
`,
547+
'array-as-keyword-shadowed-test.gjs': `
548+
import { module, test } from 'qunit';
549+
import { setupRenderingTest } from 'ember-qunit';
550+
import { render } from '@ember/test-helpers';
551+
552+
module('{{array}} as keyword (shadowed)', function(hooks) {
553+
setupRenderingTest(hooks);
554+
555+
test('it works', async function(assert) {
556+
const array = (data) => data;
557+
await render(<template>{{array "hello"}}</template>);
558+
assert.dom().hasText('hello');
559+
});
560+
});
561+
`,
529562
},
530563
},
531564
});

0 commit comments

Comments
 (0)