Skip to content

Commit

Permalink
Merge pull request #2231 from embroider-build/codemod-render-tests
Browse files Browse the repository at this point in the history
template-tag-codemod: add support for rendering tests
  • Loading branch information
ef4 authored Jan 21, 2025
2 parents b3b53c1 + f1bc043 commit 1c0f616
Show file tree
Hide file tree
Showing 7 changed files with 335 additions and 25 deletions.
5 changes: 5 additions & 0 deletions packages/template-tag-codemod/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,17 @@
],
"scripts": {},
"dependencies": {
"@babel/code-frame": "^7.26.2",
"@babel/core": "^7.26.0",
"@babel/generator": "^7.26.5",
"@babel/plugin-syntax-decorators": "^7.25.9",
"@babel/plugin-syntax-typescript": "^7.25.9",
"@embroider/compat": "workspace:^*",
"@embroider/core": "workspace:^*",
"@embroider/reverse-exports": "workspace:^*",
"@types/babel__core": "^7.20.5",
"@types/yargs": "^17.0.3",
"babel-import-util": "^3.0.0",
"babel-plugin-ember-template-compilation": "^2.3.0",
"broccoli": "^3.5.2",
"console-ui": "^3.1.2",
Expand All @@ -41,6 +44,8 @@
},
"devDependencies": {
"@glimmer/syntax": "^0.84.3",
"@types/babel__code-frame": "^7.0.6",
"@types/babel__generator": "^7.6.8",
"@types/glob": "^8.1.0",
"@types/node": "^22.9.3",
"typescript": "^5.4.5"
Expand Down
11 changes: 11 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 All @@ -37,6 +42,12 @@ yargs(process.argv.slice(2))
default: optionsWithDefaults().components,
describe: `Controls which component files we will convert to template tag. Provide a list of globs.`,
})
.option('renderTests', {
array: true,
type: 'string',
default: optionsWithDefaults().renderTests,
describe: `Controls the files in which we will search for rendering tests to convert to template tags. Provide a list of globs.`,
})
.option('defaultFormat', {
type: 'string',
default: optionsWithDefaults().defaultFormat,
Expand Down
17 changes: 15 additions & 2 deletions packages/template-tag-codemod/src/extract-meta.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type * as Babel from '@babel/core';
import { transformAsync } from '@babel/core';

export interface MetaResult {
templateSource: string;
Expand All @@ -12,7 +13,7 @@ export interface MetaResult {
>;
}

export interface ExtractMetaOpts {
interface ExtractMetaOpts {
result: MetaResult | undefined;
}

Expand All @@ -21,7 +22,7 @@ export interface ExtractMetaOpts {
necessary that it covers every possible way of expressing a template in
javascript
*/
export default function extractMetaPlugin(_babel: typeof Babel): Babel.PluginObj<{ opts: ExtractMetaOpts }> {
function extractMetaPlugin(_babel: typeof Babel): Babel.PluginObj<{ opts: ExtractMetaOpts }> {
return {
visitor: {
CallExpression(path, state) {
Expand Down Expand Up @@ -102,3 +103,15 @@ export default function extractMetaPlugin(_babel: typeof Babel): Babel.PluginObj
},
};
}

export async function extractMeta(source: string, filename: string) {
const meta: ExtractMetaOpts = { result: undefined };
await transformAsync(source, {
filename,
plugins: [[extractMetaPlugin, meta]],
});
if (!meta.result) {
throw new Error(`failed to extract metadata while processing ${filename}`);
}
return meta.result;
}
90 changes: 90 additions & 0 deletions packages/template-tag-codemod/src/identify-render-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { type NodePath, parseAsync, traverse, type types } from '@babel/core';
import { createRequire } from 'module';
import codeFrame from '@babel/code-frame';

const require = createRequire(import.meta.url);
const { codeFrameColumns } = codeFrame;

export interface RenderTest {
node: types.Node;
startIndex: number;
endIndex: number;
templateContent: string;
statementStart: number;
availableBinding: string;
}

export async function identifyRenderTests(
source: string,
filename: string
): Promise<{ renderTests: RenderTest[]; parsed: types.File }> {
let renderTests: RenderTest[] = [];
let parsed = await parseAsync(source, {
filename,
plugins: [
[require.resolve('@babel/plugin-syntax-decorators'), { legacy: true }],
require.resolve('@babel/plugin-syntax-typescript'),
],
});

if (!parsed) {
throw new Error(`bug, unexpected output from babel parseAsync`);
}

function fail(node: types.Node, message: string) {
let m = `[${filename}] ${message}`;
if (node.loc) {
m = m + '\n' + codeFrameColumns(source, node.loc);
}
return new Error(m);
}

traverse(parsed, {
CallExpression(path) {
if (path.get('callee').referencesImport('@ember/test-helpers', 'render')) {
let [arg0] = path.get('arguments');
if (arg0.isTaggedTemplateExpression()) {
let tag = arg0.get('tag');
if (isLooseHBS(tag)) {
let loc = arg0.node.loc;
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 {
throw fail(arg0.node, `unsupported syntax in rendering test (${arg0.type})`);
}
}
},
});
return { renderTests, parsed };
}

function isLooseHBS(path: NodePath<types.Expression>) {
if (path.isReferencedIdentifier()) {
if (path.referencesImport('ember-cli-htmlbars', 'hbs')) {
return true;
}
}
return false;
}
Loading

0 comments on commit 1c0f616

Please sign in to comment.