Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export interface ParseOptions {
templateOnly?: boolean;
/**
* Custom JS/TS parser. Called with the placeholder JS string
* (templates replaced with backtick expressions of equal length).
* (templates replaced with `void `...`` expressions, or `static{`...`}`
* blocks for class members, of equal length).
* Must return at least `{ ast }`.
*/
parser?: (placeholderJS: string) => { ast: ASTNode; [key: string]: unknown };
Expand Down
17 changes: 12 additions & 5 deletions src/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const preprocessor = new Preprocessor();
const PLACEHOLDER_TYPES = new Set([
"ExpressionStatement",
"StaticBlock",
"TemplateLiteral",
"UnaryExpression",
"ExportDefaultDeclaration",
]);

Expand Down Expand Up @@ -250,7 +250,7 @@ export function toTree(source, options = {}) {
setParent(ast, parent);
// Recurse into the Glimmer subtree so visitors fire on its nodes too.
// The Glimmer root's parentPath reflects its true JS parent — the
// placeholder (TemplateLiteral / StaticBlock) is an internal artifact.
// placeholder (UnaryExpression / StaticBlock) is an internal artifact.
if (hasVisitors) walkWithKeys(ast, parentPath);
return;
}
Expand Down Expand Up @@ -348,9 +348,16 @@ export const parse = toTree;
* Replaces <template>...</template> regions with placeholder expressions
* of the same character length that are valid JS/TS.
*
* Expression templates become: `content ` (backtick, space-padded)
* Expression templates become: void `content ` (void + backtick, space-padded)
* Class member templates become: static{`content `} (static block, space-padded)
*
* The `void` prefix matters at a statement start: a bare backtick literal
* after an unterminated expression statement continues it as a tagged
* template (`const y = x\n`...`` reads as `x`...``), whereas `void` cannot
* continue an expression, so automatic semicolon insertion applies exactly
* as it does for the real `<template>`. `void` is a keyword, so the
* placeholder also adds no identifier reference for scope analysis to see.
*
* This format is compatible with all JS/TS parsers including
* oxc-parser, @typescript-eslint/parser, and @babel/eslint-parser.
*/
Expand Down Expand Up @@ -381,8 +388,8 @@ function toPlaceholderJS(source, parseResults) {
const spaces = tplLength - content.length - 10; // "static{`" + "`}" = 10
parts.push(`static{\`${content}${" ".repeat(Math.max(0, spaces))}\`}`);
} else {
const spaces = tplLength - content.length - 2; // "`" + "`" = 2
parts.push(`\`${content}${" ".repeat(Math.max(0, spaces))}\``);
const spaces = tplLength - content.length - 7; // "void `" + "`" = 7
parts.push(`void \`${content}${" ".repeat(Math.max(0, spaces))}\``);
}

cursor = end;
Expand Down
14 changes: 7 additions & 7 deletions tests/custom-parser-ts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe("toTree — custom parser (typescript-eslint) — node identity", () =>
it("preserves outer-AST node identity after a standalone template splice", () => {
const source = `const Bar = () => <template></template>;`;
// Snapshot ancestor references before toTree gets a chance to splice,
// and verify the placeholder is still a TemplateLiteral at that point.
// and verify the placeholder is still a `void` expression at that point.
let origProgram, origDecl, origDtor, origArrow, origPlaceholder;
const result = toTree(source, {
parser: (js) => {
Expand All @@ -35,8 +35,8 @@ describe("toTree — custom parser (typescript-eslint) — node identity", () =>
origDecl = origProgram.body[0]; // VariableDeclaration
origDtor = origDecl.declarations[0]; // VariableDeclarator
origArrow = origDtor.init; // ArrowFunctionExpression
origPlaceholder = origArrow.body; // TemplateLiteral (placeholder)
expect(origPlaceholder.type).toBe("TemplateLiteral");
origPlaceholder = origArrow.body; // UnaryExpression (placeholder)
expect(origPlaceholder.type).toBe("UnaryExpression");
return parsed;
},
});
Expand Down Expand Up @@ -121,9 +121,9 @@ describe("toTree — custom parser (typescript-eslint) — templateInfos.placeho
const result = toTree(source, {
parser: (js) => {
const parsed = tsParseForESLint(js);
// Capture the ArrowFunctionExpression body's placeholder (TemplateLiteral)
// Capture the ArrowFunctionExpression body's placeholder (UnaryExpression)
capturedPlaceholder = parsed.ast.body[0].declarations[0].init.body;
expect(capturedPlaceholder.type).toBe("TemplateLiteral");
expect(capturedPlaceholder.type).toBe("UnaryExpression");
return parsed;
},
});
Expand All @@ -148,8 +148,8 @@ describe("toTree — custom parser (typescript-eslint) — templateInfos.placeho
// After: the GlimmerTemplate resolves to the placeholder's TS node.
const glimmerTS = esMap.get(result.templateInfos[0].ast);
expect(glimmerTS).toBeDefined();
// The placeholder was a TemplateLiteral (backtick) whose TS node is a
// string-typed expression — good enough for typed rules.
// The placeholder was a `void `...`` expression; its TS node stands in
// for the template for typed rules.
expect(glimmerTS).toBe(esMap.get(result.templateInfos[0].placeholder));
});
});
Expand Down
23 changes: 23 additions & 0 deletions tests/parse.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,29 @@ describe("parse", () => {
]);
});

it("keeps a statement-start template separate from an unterminated expression before it", () => {
// Without a semicolon after `x`, a bare backtick placeholder would read
// as the tagged template `x`...``, swallowing the template.
const source = ["const x = 1", "const y = x", "<template>{{x}}</template>", ""].join("\n");
const ast = parse(source);

expect(ast.program.body.map((node) => node.type)).toEqual([
"VariableDeclaration",
"VariableDeclaration",
"GlimmerTemplate",
]);
});

it("keeps a statement-start template separate from a call before it", () => {
const source = ["setup()", "<template>hi</template>", ""].join("\n");
const ast = parse(source);

expect(ast.program.body.map((node) => node.type)).toEqual([
"ExpressionStatement",
"GlimmerTemplate",
]);
});

it("handles multiple classes with templates", () => {
const source = `class A extends Component {
<template><div>A</div></template>
Expand Down
Loading