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
5 changes: 5 additions & 0 deletions packages/ember-tsc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
"types": "./lib/transform/index.d.ts",
"default": "./lib/transform/index.js"
},
"./transform/standalone": {
"types": "./lib/transform/template/rewrite-module-standalone.d.ts",
"default": "./lib/transform/template/rewrite-module-standalone.js"
},
"./bin/*": "./bin/*.js",
"./globals": "./types/globals/index.d.ts",
"./-private/dsl": {
Expand Down Expand Up @@ -87,6 +91,7 @@
"@volar/test-utils": "~2.4.28",
"@volar/typescript": "~2.4.28",
"content-tag": "^4.2.0",
"ember-estree": "^0.8.0",
"silent-error": "^1.1.1",
"volar-service-html": "~0.0.71",
"volar-service-typescript": "~0.0.71",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export type PreprocessData = {
};

export type TemplateLocation = {
/** content-tag's classification: a class's own template, or a template expression. */
type: 'expression' | 'class-member';
startTagOffset: number;
startTagLength: number;
endTagOffset: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { GLOBAL_TAG, PreprocessData, TemplateLocation } from './common.js';

// A `<template>` becomes a tagged-template expression `___T`...``. In a class
// body a bare expression is not a valid member, so there it is wrapped in a
// computed-property name `[___T`...`]` (rewritten to a static block during
// computed-property name `;[___T`...`]` (rewritten to a static block during
// transform). In every other position it is emitted bare: that keeps a
// user-authored array such as `[<template/>]` a single array literal, so it
// can never be confused with — and collapsed into — the wrapper.
Expand All @@ -16,6 +16,12 @@ const p = new Preprocessor();
export const preprocess: GlintExtensionPreprocess<PreprocessData> = (source, path) => {
let templates = p.parse(source, { filename: path });

// content-tag reports a class's own template before any template that
// appears earlier in its heritage clause (`class A extends mixin(<template>
// ...</template>) { <template>...</template> }`); the splicing below needs
// them in source order.
templates.sort((a, b) => a.range.startUtf16Codepoint - b.range.startUtf16Codepoint);

let templateLocations: Array<TemplateLocation> = [];
let contents = '';
let sourceOffset = 0;
Expand All @@ -30,7 +36,11 @@ export const preprocess: GlintExtensionPreprocess<PreprocessData> = (source, pat

let isClassMember = template.type === 'class-member';

if (isClassMember) contents += '[';
// The leading `;` is an empty class element. It stops the `[` from being
// read as an element access on a preceding field whose initializer has
// no trailing semicolon (`isChecked = (v) => v === 1\n<template>...`
// would otherwise become `1[___T`...`]`, an expression template).
if (isClassMember) contents += ';[';

// `transformedStart`/`transformedEnd` bracket the tag literal itself
// (`___T`...``), excluding any class-member `[ ]`, so they line up exactly
Expand All @@ -48,6 +58,7 @@ export const preprocess: GlintExtensionPreprocess<PreprocessData> = (source, pat

sourceOffset = endTagEnd;
templateLocations.push({
type: template.type,
startTagOffset,
endTagOffset,
startTagLength: startTagEnd - startTagOffset,
Expand Down
73 changes: 58 additions & 15 deletions packages/ember-tsc/src/transform/template/inlining/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GlintEmitMetadata } from '@glint/ember-tsc/config-types';
import type ts from 'typescript';
import { CorrelatedSpan, Directive, TransformError } from '../transformed-module.js';
import { TSLib } from '../../util.js';
Expand All @@ -10,6 +11,27 @@ export type CorrelatedSpansResult = {
partialSpans: Array<PartialCorrelatedSpan>;
};

export type ImportedBinding = { specifier: string; source: string; synthetic: boolean };
export type ImportedBindings = Record<string, ImportedBinding>;

/**
* Everything the template transform needs to know about one embedded
* `<template>`, independent of how the surrounding script was analyzed.
* `rewriteModule` derives this from the TypeScript AST; the standalone entry
* point derives it from content-tag's parse output and a lightweight scan of
* the script (see `script-scanner.ts`).
*/
export type EmbeddedTemplate = {
/** The template's text, as authored between the tags. */
template: string;
/** Offsets into the original script, in the same shape as `GlintEmitMetadata['templateLocation']`. */
templateLocation: NonNullable<GlintEmitMetadata['templateLocation']>;
thisBinding: TemplateThisBinding;
/** Text emitted before / after the template's transformed output (e.g. `export default `). */
prepend?: string | undefined;
append?: string | undefined;
};

/**
* How the `{{this}}` of an embedded template should be bound, based on where
* the template sits in the surrounding module:
Expand Down Expand Up @@ -39,22 +61,43 @@ export type CorrelatedSpansResult = {
export type TemplateThisBinding = 'backing-class' | 'lexical' | 'context';

/**
* Given an AST node for an embedded template, determines how the template's
* `{{this}}` should be bound. See {@link TemplateThisBinding}.
* The ancestor kinds that decide a template's `this` binding, in the order
* met walking outward from the template. See {@link TemplateThisBinding}.
*/
export function templateThisBinding(ts: TSLib, node: ts.Node): TemplateThisBinding {
let current: ts.Node | null = node;
do {
if (ts.isClassStaticBlockDeclaration(current)) {
return 'backing-class';
}
if (ts.isHeritageClause(current)) {
return 'context';
}
if (ts.isClassLike(current)) {
return 'lexical';
}
} while ((current = current.parent));
export type ThisBindingAncestor = 'static-block' | 'heritage-clause' | 'class' | 'other';

/**
* Resolves a template's `this` binding from its ancestors, innermost first:
* the first static block, heritage clause or class met decides.
*/
export function thisBindingFromAncestors(
ancestors: Iterable<ThisBindingAncestor>,
): TemplateThisBinding {
for (let ancestor of ancestors) {
switch (ancestor) {
case 'static-block':
return 'backing-class';
case 'heritage-clause':
return 'context';
case 'class':
return 'lexical';
}
}
return 'context';
}

/**
* Given a TS AST node for an embedded template, determines how the template's
* `{{this}}` should be bound. See {@link TemplateThisBinding}.
*/
export function templateThisBinding(ts: TSLib, node: ts.Node): TemplateThisBinding {
return thisBindingFromAncestors(
(function* () {
for (let current = node.parent; current; current = current.parent) {
if (ts.isClassStaticBlockDeclaration(current)) yield 'static-block';
else if (ts.isHeritageClause(current)) yield 'heritage-clause';
else if (ts.isClassLike(current)) yield 'class';
}
})(),
);
}
Loading
Loading