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

[beta] template-tag code mod #1842

Merged
merged 22 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7fe1c21
resolver transform to emit imports for helper and modifiers that need…
void-mAlex Feb 17, 2024
d1fc685
added built in components
void-mAlex Feb 17, 2024
87b8604
adding coverage for direct invocation of built-ins
ef4 Feb 20, 2024
95d3284
refactoring into combined list
ef4 Feb 20, 2024
562e744
fix the ambiguous cases
ef4 Feb 20, 2024
ded054d
if it's a pure builtin without an import do nothing
void-mAlex Mar 9, 2024
213f90f
correct typo for uniqueId import path
void-mAlex Mar 14, 2024
f2cb191
WIP compatBuild replacement for running an app code through embroider…
void-mAlex Mar 14, 2024
fb168b2
Merge remote-tracking branch 'origin/stable' into template-tag-codemod
void-mAlex Apr 23, 2024
4e03cad
Merge remote-tracking branch 'origin/stable' into template-tag-codemod
void-mAlex Jul 7, 2024
5770075
export templateTagCodemod entry point from compat package
void-mAlex Jul 16, 2024
8d8fc32
revert resolver transform changes
void-mAlex Jul 16, 2024
8fe8e8d
handle templateOnly components
void-mAlex Jul 16, 2024
5cb2df7
add basic test for template-tag-codemod
mansona Jul 16, 2024
2a95c0f
remove un-needed template only import
void-mAlex Jul 16, 2024
53eab6c
check template only tests passes
void-mAlex Jul 16, 2024
bbcab0a
revert lock file changes
void-mAlex Jul 16, 2024
6518cb6
pattern match against windows separators as well
void-mAlex Jul 17, 2024
4c211ea
resolve babel plugins relative to embroider compat
void-mAlex Jul 18, 2024
61e75fd
Merge branch 'template-tag-codemod' of github.com:embroider-build/emb…
void-mAlex Jul 18, 2024
962bab8
declare dependency on @babel/plugin-syntax-decorators
void-mAlex Jul 18, 2024
1498c47
duplicate execute function to avoid an export from audit system
void-mAlex Jul 18, 2024
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
2 changes: 1 addition & 1 deletion packages/compat/src/audit/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function isBuildError(err: any): err is BuildError {
return err?.isBuildError;
}

async function execute(
export async function execute(
shellCommand: string,
opts?: { env?: Record<string, string>; pwd?: string }
): Promise<{
Expand Down
1 change: 1 addition & 0 deletions packages/compat/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export { default as Addons } from './compat-addons';
export { default as Options, recommendedOptions } from './options';
export { default as V1Addon } from './v1-addon';
export { default as compatBuild, PipelineOptions } from './default-pipeline';
export { default as templateTagCodemod } from './template-tag-codemod';
export { PackageRules, ModuleRules } from './dependency-rules';
237 changes: 237 additions & 0 deletions packages/compat/src/template-tag-codemod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import { default as compatBuild } from './default-pipeline';
import type { EmberAppInstance } from '@embroider/core';
import type { Node, InputNode } from 'broccoli-node-api';
import { join, relative, resolve } from 'path';
import type { types as t } from '@babel/core';
import type { NodePath } from '@babel/traverse';
import { statSync, readdirSync, readFileSync, writeFileSync } from 'fs';
import Plugin from 'broccoli-plugin';
import { transformSync } from '@babel/core';
import { hbsToJS, ResolverLoader } from '@embroider/core';
import { ImportUtil } from 'babel-import-util';
import ResolverTransform from './resolver-transform';
import { execute } from './audit/build';
import { locateEmbroiderWorkingDir } from '@embroider/core';

export interface TemplateTagCodemodOptions {
shouldTransformPath: (outputPath: string) => boolean;
dryRun: boolean;
}

export default function templateTagCodemod(
emberApp: EmberAppInstance,
{ shouldTransformPath = (() => true) as TemplateTagCodemodOptions['shouldTransformPath'], dryRun = false } = {}
): Node {
return new TemplateTagCodemodPlugin(
[
compatBuild(emberApp, undefined, {
staticAddonTrees: true,
staticAddonTestSupportTrees: true,
staticComponents: true,
staticHelpers: true,
staticModifiers: true,
staticEmberSource: true,
amdCompatibility: {
es: [],
},
}),
],
{ shouldTransformPath, dryRun }
);
}
class TemplateTagCodemodPlugin extends Plugin {
constructor(inputNodes: InputNode[], readonly options: TemplateTagCodemodOptions) {
super(inputNodes, {
name: 'TemplateTagCodemodPlugin',
});
}
async build() {
function* walkSync(dir: string): Generator<string> {
const files = readdirSync(dir);

for (const file of files) {
const pathToFile = join(dir, file);
const isDirectory = statSync(pathToFile).isDirectory();
if (isDirectory) {
yield* walkSync(pathToFile);
} else {
yield pathToFile;
}
}
}
this.inputPaths[0];
const tmp_path = readFileSync(this.inputPaths[0] + '/.stage2-output').toLocaleString();
const compatPattern = /#embroider_compat\/(?<type>[^\/]+)\/(?<rest>.*)/;
const resolver = new ResolverLoader(process.cwd()).resolver;
const hbs_file_test = /\/rewritten-app\/components\/.*\.hbs$/;
// locate ember-source for the host app so we know which version to insert builtIns for
const emberSourceEntrypoint = require.resolve('ember-source', { paths: [process.cwd()] });
const emberVersion = JSON.parse(readFileSync(join(emberSourceEntrypoint, '../../package.json')).toString()).version;

for await (const current_file of walkSync(tmp_path)) {
if (hbs_file_test.test(current_file) && this.options.shouldTransformPath(current_file)) {
const template_file_src = readFileSync(current_file).toLocaleString();
const ember_template_compiler = resolver.nodeResolve(
'ember-source/vendor/ember/ember-template-compiler',
resolve(locateEmbroiderWorkingDir(process.cwd()), 'rewritten-app', 'package.json')
);
if (ember_template_compiler.type === 'not_found') {
throw 'This will not ever be true';
}
let src =
transformSync(hbsToJS(template_file_src), {
plugins: [
[
'babel-plugin-ember-template-compilation',
{
compilerPath: ember_template_compiler.filename,
transforms: [ResolverTransform({ appRoot: process.cwd(), emberVersion: emberVersion })],
targetFormat: 'hbs',
},
],
],
})?.code ?? '';
const import_bucket: NodePath<t.ImportDeclaration>[] = [];
let transformed_template_value = '';
transformSync(src, {
plugins: [
function template_tag_extractor(): unknown {
return {
visitor: {
ImportDeclaration(import_declaration: NodePath<t.ImportDeclaration>) {
const extractor = import_declaration.node.source.value.match(compatPattern);
if (extractor) {
const result = resolver.nodeResolve(extractor[0], current_file);
if (result.type === 'real') {
// find package
const owner_package = resolver.packageCache.ownerOfFile(result.filename);
// change import to real one
import_declaration.node.source.value =
owner_package!.name + '/' + extractor[1] + '/' + extractor[2];
import_bucket.push(import_declaration);
}
} else if (import_declaration.node.source.value.indexOf('@ember/template-compilation') === -1) {
import_bucket.push(import_declaration);
}
},
CallExpression(path: NodePath<t.CallExpression>) {
// reverse of hbs to js
// extract the template string to put into template tag in backing class
if (
'name' in path.node.callee &&
path.node.callee.name === 'precompileTemplate' &&
path.node.arguments &&
'value' in path.node.arguments[0]
) {
transformed_template_value = `<template>\n\t${path.node.arguments[0].value}\n</template>`;
}
},
},
};
},
],
});

//find backing class
const backing_class_resolution = resolver.nodeResolve(
'#embroider_compat/' + relative(tmp_path, current_file).slice(0, -4),
tmp_path
);

const backing_class_filename = 'filename' in backing_class_resolution ? backing_class_resolution.filename : '';
const backing_class_src = readFileSync(backing_class_filename).toString();
// console.log(backing_class_src);
const magic_string = '__MAGIC_STRING_FOR_TEMPLATE_TAG_REPLACE__';
const is_template_only =
backing_class_src.indexOf("import templateOnlyComponent from '@ember/component/template-only';") !== -1;

src = transformSync(backing_class_src, {
plugins: [
['@babel/plugin-syntax-decorators', { decoratorsBeforeExport: true }],
function glimmer_syntax_creator(babel): unknown {
return {
name: 'test',
visitor: {
Program: {
enter(path: NodePath<t.Program>) {
// Always instantiate the ImportUtil instance at the Program scope
const importUtil = new ImportUtil(babel.types, path);
const first_node = path.get('body')[0];
if (
first_node &&
first_node.node &&
first_node.node.leadingComments &&
first_node.node.leadingComments[0]?.value.includes('__COLOCATED_TEMPLATE__')
) {
//remove magic comment
first_node.node.leadingComments.splice(0, 1);
}
for (const template_import of import_bucket) {
for (let i = 0, len = template_import.node.specifiers.length; i < len; ++i) {
const specifier = template_import.node.specifiers[i];
if (specifier.type === 'ImportDefaultSpecifier') {
importUtil.import(path, template_import.node.source.value, 'default', specifier.local.name);
} else if (specifier.type === 'ImportSpecifier') {
importUtil.import(path, template_import.node.source.value, specifier.local.name);
}
}
}
},
},
ImportDeclaration(import_declaration: NodePath<t.ImportDeclaration>) {
if (import_declaration.node.source.value.indexOf('@ember/component/template-only') !== -1) {
import_declaration.remove();
}
},
ExportDefaultDeclaration(path: NodePath<t.ExportDefaultDeclaration>) {
path.traverse({
ClassBody(path) {
const classbody_nodes = path.get('body');
//add magic string to be replaces with the contents of the template tag
classbody_nodes[classbody_nodes.length - 1].addComment('trailing', magic_string, false);
},
});
},
},
};
},
],
})!.code!.replace(`/*${magic_string}*/`, transformed_template_value);
if (is_template_only) {
// because we can't inject a comment as the default export
// we replace the known exported string
src = src.replace('templateOnlyComponent()', transformed_template_value);
}

const dryRun = this.options.dryRun ? '--dry-run' : '';
// work out original file path in app tree
const app_relative_path = join('app', relative(tmp_path, current_file));
const new_file_path = app_relative_path.slice(0, -4) + '.gjs';

// write glimmer file out
if (this.options.dryRun) {
console.log('Write new file', new_file_path, src);
} else {
writeFileSync(join(process.cwd(), new_file_path), src, { flag: 'wx+' });
}

// git rm old files (js/ts if exists + hbs)
let rm_hbs = await execute(`git rm ${app_relative_path} ${dryRun}`, {
pwd: process.cwd(),
});
console.log(rm_hbs.output);

if (!is_template_only) {
// remove backing class only if it's not a template only component
// resolve repative path to rewritten-app
const app_relative_path = join('app', relative(tmp_path, backing_class_filename));
let rm_js = await execute(`git rm ${app_relative_path} ${dryRun}`, {
pwd: process.cwd(),
});

console.log(rm_js.output);
}
}
}
}
}
Loading
Loading