From 176cf7c756ec8955cae057742e126a8d7e3a3bc2 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:29:35 -0400 Subject: [PATCH 1/3] Assert the compiled scope independently of the wire format's shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Older compilers emit the wire format's scope as an array (`scope: () => [Setup]`); newer ones emit an object (`scope: () => ({ Foo: Setup })`). Both carry the same information — the names the template uses, mapped to the JS they resolve to — but the array form leaves the names implicit, in the `locals` we handed the compiler. Add a `wireScope` helper that reads either shape back into the same mapping, by pairing an array-form scope with those locals. That lets a test assert on the compiled scope without caring which ember-source is installed, which matters because the try scenarios float that dependency from 3.28 through beta. Co-Authored-By: Claude Opus 5 (1M context) --- __tests__/all.test.ts | 72 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/__tests__/all.test.ts b/__tests__/all.test.ts index d180146..a5b909a 100644 --- a/__tests__/all.test.ts +++ b/__tests__/all.test.ts @@ -417,6 +417,24 @@ describe('htmlbars-inline-precompile', function () { `); }); + it('puts a scope local into the compiled scope', async function () { + let transformed = await transform(` + import { Setup } from './foo.js'; + import { precompileTemplate } from '@ember/template-compilation'; + import { setComponentTemplate } from '@ember/component'; + import templateOnly from '@ember/component/template-only'; + + export default setComponentTemplate(precompileTemplate("", { + strictMode: true, + scope: () => ({ + Setup + }) + }), templateOnly()); + `); + + expect(wireScope(transformed)).toEqual({ Setup: 'Setup' }); + }); + it('does not fully remove imports that have other imports', async function () { let transformed = await transform(` import { precompileTemplate, compileTemplate } from '@ember/template-compilation'; @@ -2539,6 +2557,60 @@ describe('htmlbars-inline-precompile', function () { // This takes out parts of ember's wire format that aren't our job and shouldn't // break our tests if they change. +// Reads the compiled scope back out of the wire format as a mapping from the +// name the template uses to the JS it resolves to. +// +// Older compilers emit the scope as an array (`scope: () => [Setup]`) and newer +// ones as an object (`scope: () => ({ Foo: Setup })`). Both carry the same +// information, but the array form leaves the names implicit: they're the +// `locals` we handed the compiler, in order. Reading those back off the +// precompile call puts both shapes into the same form, so a test can assert on +// the scope without caring which ember-source is installed. +function wireScope(src: string): Record { + let t = babel.types; + let found: Record | undefined; + + babel.traverse(babel.parse(src, { configFile: false, babelrc: false })!, { + ObjectProperty(path) { + let { key, value } = path.node; + let isScopeKey = + (t.isIdentifier(key) && key.name === 'scope') || + (t.isStringLiteral(key) && key.value === 'scope'); + if (!isScopeKey || !t.isArrowFunctionExpression(value)) { + return; + } + + let source = (node: babel.types.Node) => src.slice(node.start!, node.end!); + + if (t.isObjectExpression(value.body)) { + found = Object.fromEntries( + value.body.properties.map((prop) => { + if (!t.isObjectProperty(prop)) { + throw new Error(`unexpected scope entry: ${prop.type}`); + } + let name = t.isIdentifier(prop.key) ? prop.key.name : source(prop.key); + return [name, source(prop.value)]; + }) + ); + } else if (t.isArrayExpression(value.body)) { + let locals = (precompileSpy.mock.lastCall?.at(-1) as { locals?: string[] })?.locals; + if (!locals) { + throw new Error('cannot name an array-form scope without the compiler options'); + } + found = Object.fromEntries( + value.body.elements.map((element, index) => [locals[index], source(element!)]) + ); + } + path.stop(); + }, + }); + + if (!found) { + throw new Error(`no wire-format scope found in:\n${src}`); + } + return found; +} + function normalizeWireFormat(src: string): string { return canonicalizeWireScope(src) .replace(/"moduleName":\s"[^"]+"/, '"moduleName": ""') From 18a7dfa2517e489d7f9d135c73b56372c2b70f13 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 17 Aug 2026 12:36:39 -0400 Subject: [PATCH 2/3] Fix comment position --- __tests__/all.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/__tests__/all.test.ts b/__tests__/all.test.ts index a5b909a..a16a313 100644 --- a/__tests__/all.test.ts +++ b/__tests__/all.test.ts @@ -2555,11 +2555,6 @@ describe('htmlbars-inline-precompile', function () { }); }); -// This takes out parts of ember's wire format that aren't our job and shouldn't -// break our tests if they change. -// Reads the compiled scope back out of the wire format as a mapping from the -// name the template uses to the JS it resolves to. -// // Older compilers emit the scope as an array (`scope: () => [Setup]`) and newer // ones as an object (`scope: () => ({ Foo: Setup })`). Both carry the same // information, but the array form leaves the names implicit: they're the @@ -2611,6 +2606,10 @@ function wireScope(src: string): Record { return found; } +// This takes out parts of ember's wire format that aren't our job and shouldn't +// break our tests if they change. +// Reads the compiled scope back out of the wire format as a mapping from the +// name the template uses to the JS it resolves to. function normalizeWireFormat(src: string): string { return canonicalizeWireScope(src) .replace(/"moduleName":\s"[^"]+"/, '"moduleName": ""') From ca1d7f141fef3c7c0e8e46856e4b63b78d821d73 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 17 Aug 2026 12:37:10 -0400 Subject: [PATCH 3/3] Fix comment position --- __tests__/all.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/__tests__/all.test.ts b/__tests__/all.test.ts index a16a313..49ea93f 100644 --- a/__tests__/all.test.ts +++ b/__tests__/all.test.ts @@ -2555,6 +2555,9 @@ describe('htmlbars-inline-precompile', function () { }); }); +// Reads the compiled scope back out of the wire format as a mapping from the +// name the template uses to the JS it resolves to. +// // Older compilers emit the scope as an array (`scope: () => [Setup]`) and newer // ones as an object (`scope: () => ({ Foo: Setup })`). Both carry the same // information, but the array form leaves the names implicit: they're the @@ -2608,8 +2611,6 @@ function wireScope(src: string): Record { // This takes out parts of ember's wire format that aren't our job and shouldn't // break our tests if they change. -// Reads the compiled scope back out of the wire format as a mapping from the -// name the template uses to the JS it resolves to. function normalizeWireFormat(src: string): string { return canonicalizeWireScope(src) .replace(/"moduleName":\s"[^"]+"/, '"moduleName": ""')