Skip to content

Commit

Permalink
adding nativeLexicalThis=false mode
Browse files Browse the repository at this point in the history
  • Loading branch information
ef4 committed Jan 17, 2025
1 parent 82b9484 commit 4e846db
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 10 deletions.
5 changes: 5 additions & 0 deletions packages/template-tag-codemod/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ yargs(process.argv.slice(2))
type: 'boolean',
describe: `When true, assume we can use template-tag directly in route files (requires ember-source >= 6.3.0-beta.3). When false, assume we can use the ember-route-template addon instead.`,
})
.option('nativeLexicalThis', {
default: optionsWithDefaults().nativeLexicalThis,
type: 'boolean',
describe: `When true, assume that Ember supports accessing the lexically-scoped "this" from template-tags that are used as expressions (requires ember-source >= TODO). When false, introduce a new local variable to make "this" accessible.`,
})
.option('routeTemplates', {
array: true,
type: 'string',
Expand Down
16 changes: 16 additions & 0 deletions packages/template-tag-codemod/src/identify-render-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export interface RenderTest {
startIndex: number;
endIndex: number;
templateContent: string;
statementStart: number;
availableBinding: string;
}

export async function identifyRenderTests(
Expand Down Expand Up @@ -48,11 +50,25 @@ export async function identifyRenderTests(
if (!loc) {
throw new Error(`bug: no locations provided by babel`);
}

let counter = 0;
let availableBinding = 'self';
while (path.scope.getBinding(availableBinding)) {
availableBinding = `self${counter++}`;
}

let statementCandidate: NodePath<unknown> = path;
while (!statementCandidate.isStatement()) {
statementCandidate = statementCandidate.parentPath;
}

renderTests.push({
node: arg0.node,
startIndex: loc.start.index,
endIndex: loc.end.index,
templateContent: arg0.node.quasi.quasis[0].value.raw,
statementStart: statementCandidate.node.loc!.start.index,
availableBinding,
});
}
} else {
Expand Down
41 changes: 33 additions & 8 deletions packages/template-tag-codemod/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { extractMeta, type MetaResult } from './extract-meta.js';
import reverseExports from '@embroider/reverse-exports';
import { dirname } from 'path';
import type { ResolverTransformOptions } from '@embroider/compat';
import { routeTemplateTransform } from './route-template-transform.js';
import { replaceThisTransform } from './replace-this-transform.js';
import { identifyRenderTests, type RenderTest } from './identify-render-tests.js';
import { ImportUtil } from 'babel-import-util';

Expand All @@ -31,6 +31,13 @@ export interface Options {
// Otherwise, assume we're using the ember-route-template addon.
nativeRouteTemplates?: boolean;

// when true, assume we can use
// https://github.com/emberjs/babel-plugin-ember-template-compilation/pull/67.
// This is mostly useful when codemodding rendering tests, which often access
// `{{this.stuff}}` in a template. When false, polyfill that behavior by
// introducing a new local variable.
nativeLexicalThis?: boolean;

// list of globs of the route templates we should convert.
routeTemplates?: string[];

Expand Down Expand Up @@ -63,6 +70,7 @@ export function optionsWithDefaults(options?: Options): OptionsWithDefaults {
relativeLocalPaths: true,
extensions: ['.gts', '.gjs', '.ts', '.js', '.hbs'],
nativeRouteTemplates: true,
nativeLexicalThis: true,
routeTemplates: ['app/templates/**/*.hbs'],
components: ['app/components/**/*.{js,ts,hbs}'],
renderTests: ['tests/**/*.{js,ts}'],
Expand Down Expand Up @@ -122,14 +130,16 @@ const resolutions = new Map<string, { type: 'real' } | { type: 'virtual'; conten
export interface InspectedTemplate {
templateSource: string;
scope: MetaResult['scope'];
replacedThisWith: string | false;
}

export async function inspectContents(
filename: string,
src: string,
isRouteTemplate: boolean,
replaceThisWith: string | false,
opts: OptionsWithDefaults
): Promise<InspectedTemplate> {
let replaced = { didReplace: false };
let strictSource = (await transformAsync(hbsToJS(src), {
filename,
plugins: [
Expand All @@ -148,7 +158,7 @@ export async function inspectContents(
},
} satisfies ResolverTransformOptions,
],
...(isRouteTemplate ? [routeTemplateTransform()] : []),
...(replaceThisWith ? [replaceThisTransform(replaceThisWith, replaced)] : []),
],
} satisfies EtcOptions,
],
Expand All @@ -157,12 +167,12 @@ export async function inspectContents(

const meta = await extractMeta(strictSource, filename);
let { templateSource, scope } = await resolveImports(filename, meta, opts);
return { templateSource, scope };
return { templateSource, scope, replacedThisWith: replaced.didReplace ? replaceThisWith : false };
}

export async function processRouteTemplate(filename: string, opts: OptionsWithDefaults): Promise<void> {
let { templateSource, scope } = await inspectContents(filename, readFileSync(filename, 'utf8'), true, opts);

let { templateSource, scope } = await inspectContents(filename, readFileSync(filename, 'utf8'), '@controller', opts);
'
let outSource: string[] = [];

if (opts.nativeRouteTemplates) {
Expand Down Expand Up @@ -497,8 +507,13 @@ async function inspectTests(
): Promise<(InspectedTemplate & RenderTest)[]> {
return await Promise.all(
renderTests.map(async target => {
let { templateSource, scope } = await inspectContents(filename, target.templateContent, false, opts);
return { ...target, templateSource, scope };
let { templateSource, scope, replacedThisWith } = await inspectContents(
filename,
target.templateContent,
opts.nativeLexicalThis ? false : target.availableBinding,
opts
);
return { ...target, templateSource, scope, replacedThisWith };
})
);
}
Expand All @@ -514,6 +529,15 @@ export async function processRenderTest(filename: string, opts: OptionsWithDefau
let inspectedTests = await inspectTests(filename, renderTests, opts);
edits.unshift({ start: 0, end: 0, replacement: mergeImports(parsed, inspectedTests) });
for (let test of inspectedTests) {
if (!opts.nativeLexicalThis) {
if (test.replacedThisWith) {
edits.push({
start: test.statementStart,
end: test.statementStart,
replacement: `const ${test.replacedThisWith} = this;\n`,
});
}
}
edits.push({
start: test.startIndex,
end: test.endIndex,
Expand All @@ -532,6 +556,7 @@ function mergeImports(parsed: types.File, inspectedTests: (InspectedTemplate & {
traverse(parsed, {
Program(path) {
importUtil = new ImportUtil(babel, path);
importUtil.removeImport('ember-cli-htmlbars', 'hbs');
},
TaggedTemplateExpression(path) {
let matched = inspectedTests.find(t => t.node === path.node);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { ASTPluginBuilder } from '@glimmer/syntax';

export function routeTemplateTransform(): ASTPluginBuilder {
export function replaceThisTransform(replacement: string, result: { didReplace: boolean }): ASTPluginBuilder {
const transform: ASTPluginBuilder = ({ syntax: { builders } }) => {
return {
name: 'template-tag-codemod-route-template',
visitor: {
PathExpression(node) {
if (node.head.type === 'ThisHead') {
return builders.path(`@controller.${node.tail.join('.')}`);
result.didReplace = true;
return builders.path(`${replacement}.${node.tail.join('.')}`);
}
},
},
Expand Down

0 comments on commit 4e846db

Please sign in to comment.