Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

template-tag-codemod: add support for rendering tests #2231

Merged
merged 4 commits into from
Jan 21, 2025
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/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
Loading