Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export const transform: GlintExtensionTransform<PreprocessData> = (
} else if (isETIDefaultTemplate(ts, node)) {
// Annotate that this template is a default export
setEmitMetadata(node.expression, { prepend: 'export default ' });

return node;
} else if (isETIDefaultSatisfiesTemplate(ts, node)) {
// Annotate that this template is a default export
setEmitMetadata(node.expression.expression, { prepend: 'export default ' });
return node;
} else if (isETITemplateExpression(ts, node)) {
// Convert '[__T`foo`]' as an expression to just '__T`foo`'
Expand Down Expand Up @@ -111,18 +114,43 @@ type ETITemplateProperty = ts.PropertyDeclaration & {
name: ts.ComputedPropertyName & { expression: ETITemplateLiteral };
};

type ETIDefaultTemplate =
| (ts.ExpressionStatement & {
expression: ETITemplateLiteral;
})
| (ts.SatisfiesExpression & {
expression: ETITemplateLiteral;
});
type ETIDefaultTemplate = ts.ExpressionStatement & {
expression: ETITemplateLiteral;
};

type ETIDefaultSatisfiesTemplate = ts.ExpressionStatement & {
expression: ts.SatisfiesExpression & { expression: ETITemplateLiteral };
};

/**
* Implicit default export:
*
* ( <template></template> )
* ^ ExpressionStatement
*
* ( <template></template> satisfies ... )
* ^ SatisfiesExpression
*
* But!
*
* ( const X = <template></template> satisfies ... )
* ^ VariableStatement
*
* So when we check for a wrapping SatisfiesExpression, we need to also make sure
* the parent node is not a variable Statement.
*/
function isETIDefaultTemplate(ts: TSLib, node: ts.Node): node is ETIDefaultTemplate {
return ts.isExpressionStatement(node) && isETITemplateLiteral(ts, node.expression);
}

function isETIDefaultSatisfiesTemplate(
ts: TSLib,
node: ts.Node,
): node is ETIDefaultSatisfiesTemplate {
return (
(ts.isExpressionStatement(node) || ts.isSatisfiesExpression(node)) &&
isETITemplateLiteral(ts, node.expression)
ts.isExpressionStatement(node) &&
ts.isSatisfiesExpression(node.expression) &&
isETITemplateLiteral(ts, node.expression.expression)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,43 @@ describe('Environment: ETI', () => {
);
});

test('single template, no export, with satisfies', () => {
let source = stripIndent`
import type { TOC } from '@ember/component/template-only';
const hello = <template>HelloWorld!</template> satisfies TOC<{
Blocks: { default: [] }
}>;
`;

let { meta, sourceFile } = applyTransform(source);

let declaration = sourceFile.statements[2] as ts.VariableStatement;
let satisfiesExpression = declaration.declarationList.declarations[0]
.initializer! as ts.SatisfiesExpression;
let templateNode = satisfiesExpression.expression;

let start = source.indexOf('<template>');
let contentStart = start + '<template>'.length;
let contentEnd = source.indexOf('</template>');
let end = contentEnd + '</template>'.length;

expect(meta).toEqual(
new Map([
[
templateNode,
{
templateLocation: {
start,
contentStart,
contentEnd,
end,
},
},
],
]),
);
});

test('multiple templates', () => {
let source = stripIndent`
<template>
Expand Down
93 changes: 71 additions & 22 deletions test-packages/package-test-core/__tests__/transform/rewrite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ describe('Transform: rewriteModule', () => {
}"
`);
});

test('embedded gts templates', () => {
let customEnv = GlintEnvironment.load(['ember-loose', 'ember-template-imports']);
let script = {
Expand Down Expand Up @@ -890,31 +891,79 @@ describe('Transform: rewriteModule', () => {
`);
});

test('with implicit export default and satisfies', () => {
let customEnv = GlintEnvironment.load(['ember-loose', 'ember-template-imports']);
let script = {
filename: 'test.gts',
contents: stripIndent`
describe('satisfies', () => {
test('with implicit export default', () => {
let customEnv = GlintEnvironment.load(['ember-loose', 'ember-template-imports']);
let script = {
filename: 'test.gts',
contents: stripIndent`
import type { TOC } from '@ember/component/template-only';
<template>HelloWorld!</template> satisfies TOC<{
Blocks: { default: [] }
}>;
`,
};

let transformedModule = rewriteModule(ts, { script }, customEnv);

expect(transformedModule?.errors).toEqual([]);
expect(transformedModule?.transformedContents).toMatchInlineSnapshot(`
"import __GLINT_GTS_EXTENSION_HACK__ from './__glint-non-existent.gts';
import __GLINT_GJS_EXTENSION_HACK__ from './__glint-non-existent.gjs';
import type { TOC } from '@ember/component/template-only';
<template>HelloWorld!</template> satisfies TOC<{
export default ({} as typeof import("@glint/environment-ember-template-imports/-private/dsl")).templateExpression(function(__glintRef__, __glintDSL__: typeof import("@glint/environment-ember-template-imports/-private/dsl")) {
__glintRef__; __glintDSL__;
}) satisfies TOC<{
Blocks: { default: [] }
}>;
`,
};

let transformedModule = rewriteModule(ts, { script }, customEnv);
}>;"
`);
});

test('with two template-only components', () => {
const emberTemplateImportsEnvironment = GlintEnvironment.load(['ember-template-imports']);

let script = {
filename: 'test.gts',
contents: [
`import type { TOC } from '@ember/component/template-only';`,
``,
`const SmolComp = `,
` <template>`,
` Hello there, {{@name}}`,
` </template> satisfies TOC<{ Args: { name: string }}>;`,
``,
`<template>`,
` <SmolComp @name="Ember" />`,
`</template> satisfies TOC<{ Args: {}, Blocks: {}, Element: null }>`,
``,
].join('\n'),
};

let transformedModule = rewriteModule(ts, { script }, emberTemplateImportsEnvironment);

expect(transformedModule?.errors?.length).toBe(0);
expect(transformedModule?.transformedContents).toMatchInlineSnapshot(`
"import __GLINT_GTS_EXTENSION_HACK__ from './__glint-non-existent.gts';
import __GLINT_GJS_EXTENSION_HACK__ from './__glint-non-existent.gjs';
import type { TOC } from '@ember/component/template-only';

expect(transformedModule?.errors).toEqual([]);
expect(transformedModule?.transformedContents).toMatchInlineSnapshot(`
"import __GLINT_GTS_EXTENSION_HACK__ from './__glint-non-existent.gts';
import __GLINT_GJS_EXTENSION_HACK__ from './__glint-non-existent.gjs';
import type { TOC } from '@ember/component/template-only';
export default ({} as typeof import("@glint/environment-ember-template-imports/-private/dsl")).templateExpression(function(__glintRef__, __glintDSL__: typeof import("@glint/environment-ember-template-imports/-private/dsl")) {
__glintRef__; __glintDSL__;
}) satisfies TOC<{
Blocks: { default: [] }
}>;"
`);
const SmolComp =
({} as typeof import("@glint/environment-ember-template-imports/-private/dsl")).templateExpression(function(__glintRef__, __glintDSL__: typeof import("@glint/environment-ember-template-imports/-private/dsl")) {
__glintDSL__.emitContent(__glintDSL__.resolveOrReturn(__glintRef__.args.name)());
__glintRef__; __glintDSL__;
}) satisfies TOC<{ Args: { name: string }}>;

export default ({} as typeof import("@glint/environment-ember-template-imports/-private/dsl")).templateExpression(function(__glintRef__, __glintDSL__: typeof import("@glint/environment-ember-template-imports/-private/dsl")) {
{
const __glintY__ = __glintDSL__.emitComponent(__glintDSL__.resolve(SmolComp)({
name: "Ember", ...__glintDSL__.NamedArgsMarker }));
__glintY__;
}
__glintRef__; __glintDSL__;
}) satisfies TOC<{ Args: {}, Blocks: {}, Element: null }>
"
`);
});
});
});
});
Loading