diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0140c3a5..2ffd05cd2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,65 @@
# Embroider Changelog
+## Release (2024-08-30)
+
+@embroider/compat 3.6.1 (patch)
+@embroider/core 3.4.15 (patch)
+@embroider/macros 1.16.6 (patch)
+@embroider/shared-internals 2.6.3 (patch)
+@embroider/webpack 4.0.5 (patch)
+
+#### :bug: Bug Fix
+* `@embroider/shared-internals`
+ * [#2075](https://github.com/embroider-build/embroider/pull/2075) Update ember standard modules to include @ember/renderer and @ember/-internals and ember-testing ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
+* `@embroider/compat`
+ * [#2067](https://github.com/embroider-build/embroider/pull/2067) codemod fixes ([@void-mAlex](https://github.com/void-mAlex))
+
+#### :memo: Documentation
+* [#2055](https://github.com/embroider-build/embroider/pull/2055) document templateTagCodemod usage ([@void-mAlex](https://github.com/void-mAlex))
+
+#### :house: Internal
+* `@embroider/webpack`
+ * [#2076](https://github.com/embroider-build/embroider/pull/2076) [Stable]: Follow upstream type change from webpack ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
+* Other
+ * [#2058](https://github.com/embroider-build/embroider/pull/2058) Set the packageManager field ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
+
+#### Committers: 2
+- Alex ([@void-mAlex](https://github.com/void-mAlex))
+- [@NullVoxPopuli](https://github.com/NullVoxPopuli)
+
+## Release (2024-07-18)
+
+@embroider/compat 3.6.0 (minor)
+
+#### :rocket: Enhancement
+* `@embroider/compat`, `@embroider/test-scenarios`
+ * [#1842](https://github.com/embroider-build/embroider/pull/1842) [beta] template-tag code mod ([@void-mAlex](https://github.com/void-mAlex))
+
+#### Committers: 1
+- Alex ([@void-mAlex](https://github.com/void-mAlex))
+
+## Release (2024-07-16)
+
+@embroider/compat 3.5.7 (patch)
+@embroider/util 1.13.2 (patch)
+
+#### :bug: Bug Fix
+* `@embroider/compat`
+ * [#2033](https://github.com/embroider-build/embroider/pull/2033) Remove deprecations warnings in resolver transform ([@mkszepp](https://github.com/mkszepp))
+ * [#2047](https://github.com/embroider-build/embroider/pull/2047) Add semver to power select with create ([@mkszepp](https://github.com/mkszepp))
+
+#### :house: Internal
+* `@embroider/test-scenarios`
+ * [#1930](https://github.com/embroider-build/embroider/pull/1930) create a smoke test for the widest possible matrix ([@mansona](https://github.com/mansona))
+* Other
+ * [#2015](https://github.com/embroider-build/embroider/pull/2015) update github actions ([@mansona](https://github.com/mansona))
+* `@embroider/util`, `@embroider/sample-transforms`, `@embroider/test-support`, `@embroider/test-scenarios`
+ * [#1931](https://github.com/embroider-build/embroider/pull/1931) update scenario-tester ([@mansona](https://github.com/mansona))
+
+#### Committers: 2
+- Chris Manson ([@mansona](https://github.com/mansona))
+- Markus Sanin ([@mkszepp](https://github.com/mkszepp))
+
## Release (2024-07-03)
@embroider/compat 3.5.6 (patch)
diff --git a/README.md b/README.md
index d9d1b231d..c894ecba0 100644
--- a/README.md
+++ b/README.md
@@ -117,6 +117,28 @@ return require('@embroider/compat').compatBuild(app, Webpack, {
});
```
+## Template Tag Codemod
+
+Edit `ember-cli-build.js`:
+```js
+return require('@embroider/compat').templateTagCodemod(app, {
+ shouldTransformPath: (path) => { return true; },
+ dryRun: true,
+});
+```
+Run a normal ember build to transform your hbs templates into template tag single file components.
+Requires optimized build (static* flags to be turned on)
+
+### Options
+
+* `shouldTransformPath` - allows users to filter the templates that the code mod would run on
+* `dryRun` - option can be used to obtain a summary of the changed the build would perform and which files it would act upon
+
+### Limitations
+
+* App templates only
+* `@embroider/compat` >= 3.6.0
+
## Compatibility
### Ember version
diff --git a/package.json b/package.json
index 4375eac84..18cb68b16 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
"publishConfig": {
"registry": "https://registry.npmjs.org"
},
+ "packageManager": "pnpm@8.15.8+sha256.691fe176eea9a8a80df20e4976f3dfb44a04841ceb885638fe2a26174f81e65e",
"changelog": {
"__comment__": "Our release infrastructure relies on these exact labels. Be careful changing them.",
"labels": {
diff --git a/packages/compat/package.json b/packages/compat/package.json
index 971ea0da8..07f815287 100644
--- a/packages/compat/package.json
+++ b/packages/compat/package.json
@@ -1,6 +1,6 @@
{
"name": "@embroider/compat",
- "version": "3.5.6",
+ "version": "3.6.1",
"private": false,
"description": "Backward compatibility layer for the Embroider build system.",
"repository": {
@@ -25,6 +25,7 @@
"dependencies": {
"@babel/code-frame": "^7.14.5",
"@babel/core": "^7.14.5",
+ "@babel/plugin-syntax-decorators": "^7.24.7",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.14.5",
"@babel/preset-env": "^7.14.5",
diff --git a/packages/compat/src/addon-dependency-rules/ember-power-select-with-create.ts b/packages/compat/src/addon-dependency-rules/ember-power-select-with-create.ts
index fe1daaac2..525b84736 100644
--- a/packages/compat/src/addon-dependency-rules/ember-power-select-with-create.ts
+++ b/packages/compat/src/addon-dependency-rules/ember-power-select-with-create.ts
@@ -3,6 +3,7 @@ import type { PackageRules } from '..';
const rules: PackageRules[] = [
{
package: 'ember-power-select-with-create',
+ semverRange: '<3.0.0',
components: {
'': {
acceptsComponentArguments: ['powerSelectComponentName', 'suggestedOptionComponent'],
diff --git a/packages/compat/src/index.ts b/packages/compat/src/index.ts
index 313e45051..d25e8674b 100644
--- a/packages/compat/src/index.ts
+++ b/packages/compat/src/index.ts
@@ -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, prebuild, PipelineOptions } from './default-pipeline';
+export { default as templateTagCodemod } from './template-tag-codemod';
export { PackageRules, ModuleRules } from './dependency-rules';
diff --git a/packages/compat/src/resolver-transform.ts b/packages/compat/src/resolver-transform.ts
index 395c804a3..d9343b11b 100644
--- a/packages/compat/src/resolver-transform.ts
+++ b/packages/compat/src/resolver-transform.ts
@@ -781,14 +781,14 @@ class TemplateResolver implements ASTPlugin {
if (node.path.type !== 'PathExpression') {
return;
}
- let rootName = node.path.parts[0];
+ let rootName = headOf(node.path);
if (this.scopeStack.inScope(rootName, path)) {
return;
}
- if (node.path.this === true) {
+ if (isThisHead(node.path)) {
return;
}
- if (node.path.parts.length > 1) {
+ if (parts(node.path).length > 1) {
// paths with a dot in them (which therefore split into more than
// one "part") are classically understood by ember to be contextual
// components, which means there's nothing to resolve at this
@@ -820,10 +820,10 @@ class TemplateResolver implements ASTPlugin {
if (node.path.type !== 'PathExpression') {
return;
}
- if (node.path.this === true) {
+ if (isThisHead(node.path)) {
return;
}
- if (this.scopeStack.inScope(node.path.parts[0], path)) {
+ if (this.scopeStack.inScope(headOf(node.path), path)) {
return;
}
if (node.path.original === 'component' && node.params.length > 0) {
@@ -859,14 +859,14 @@ class TemplateResolver implements ASTPlugin {
if (node.path.type !== 'PathExpression') {
return;
}
- let rootName = node.path.parts[0];
+ let rootName = headOf(node.path);
if (this.scopeStack.inScope(rootName, path)) {
return;
}
- if (node.path.this === true) {
+ if (isThisHead(node.path)) {
return;
}
- if (node.path.parts.length > 1) {
+ if (parts(node.path).length > 1) {
// paths with a dot in them (which therefore split into more than
// one "part") are classically understood by ember to be contextual
// components, which means there's nothing to resolve at this
@@ -921,16 +921,16 @@ class TemplateResolver implements ASTPlugin {
if (node.path.type !== 'PathExpression') {
return;
}
- if (this.scopeStack.inScope(node.path.parts[0], path)) {
+ if (this.scopeStack.inScope(headOf(node.path), path)) {
return;
}
- if (node.path.this === true) {
+ if (isThisHead(node.path)) {
return;
}
- if (node.path.data === true) {
+ if (isAtHead(node.path)) {
return;
}
- if (node.path.parts.length > 1) {
+ if (parts(node.path).length > 1) {
// paths with a dot in them (which therefore split into more than
// one "part") are classically understood by ember to be contextual
// components. With the introduction of `Template strict mode` in Ember 3.25
@@ -1161,3 +1161,35 @@ function appendArrays(objValue: any, srcValue: any) {
return objValue.concat(srcValue);
}
}
+
+function headOf(path: any) {
+ if (!path) return;
+
+ return 'head' in path ? path.head.name : path.parts[0];
+}
+
+function isThisHead(path: any) {
+ if (!path) return;
+
+ if ('head' in path) {
+ return path.head.type === 'ThisHead';
+ }
+
+ return path.this === true;
+}
+
+function isAtHead(path: any) {
+ if (!path) return;
+
+ if ('head' in path) {
+ return path.head.type === 'AtHead';
+ }
+
+ return path.data === true;
+}
+
+function parts(path: any) {
+ if (!path) return;
+
+ return 'original' in path ? path.original.split('.') : path.parts;
+}
diff --git a/packages/compat/src/template-tag-codemod.ts b/packages/compat/src/template-tag-codemod.ts
new file mode 100644
index 000000000..b523d154d
--- /dev/null
+++ b/packages/compat/src/template-tag-codemod.ts
@@ -0,0 +1,295 @@
+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 { spawn } from 'child_process';
+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 {
+ 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\/(?[^\/]+)\/(?.*)/;
+ 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;
+
+ const ember_template_compiler = await 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';
+ }
+
+ const embroider_compat_path = require.resolve('@embroider/compat', { paths: [process.cwd()] });
+ const babel_plugin_ember_template_compilation = require.resolve('babel-plugin-ember-template-compilation', {
+ paths: [embroider_compat_path],
+ });
+ const babel_plugin_syntax_decorators = require.resolve('@babel/plugin-syntax-decorators', {
+ paths: [embroider_compat_path],
+ });
+
+ 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();
+
+ 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',
+ },
+ ],
+ ],
+ filename: current_file,
+ })?.code ?? '';
+ const import_bucket: NodePath[] = [];
+ let transformed_template_value = '';
+ transformSync(src, {
+ plugins: [
+ function template_tag_extractor(): unknown {
+ return {
+ visitor: {
+ async ImportDeclaration(import_declaration: NodePath) {
+ const extractor = import_declaration.node.source.value.match(compatPattern);
+ if (extractor) {
+ const result = await 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) {
+ // 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 = `\n\t${path.node.arguments[0].value}\n`;
+ }
+ },
+ },
+ };
+ },
+ ],
+ });
+
+ //find backing class
+ const backing_class_resolution = await resolver.nodeResolve(
+ '#embroider_compat/' + relative(tmp_path, current_file).replace(/[\\]/g, '/').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();
+ 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) {
+ // 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) {
+ if (import_declaration.node.source.value.indexOf('@ember/component/template-only') !== -1) {
+ import_declaration.remove();
+ }
+ },
+ ExportDefaultDeclaration(path: NodePath) {
+ 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);
+ }
+ }
+ }
+ }
+}
+
+async function execute(
+ shellCommand: string,
+ opts?: { env?: Record; pwd?: string }
+): Promise<{
+ exitCode: number;
+ stderr: string;
+ stdout: string;
+ output: string;
+}> {
+ let env: Record | undefined;
+ if (opts?.env) {
+ env = { ...process.env, ...opts.env };
+ }
+ let child = spawn(shellCommand, {
+ stdio: ['inherit', 'pipe', 'pipe'],
+ cwd: opts?.pwd,
+ shell: true,
+ env,
+ });
+ let stderrBuffer: string[] = [];
+ let stdoutBuffer: string[] = [];
+ let combinedBuffer: string[] = [];
+ child.stderr.on('data', data => {
+ stderrBuffer.push(data);
+ combinedBuffer.push(data);
+ });
+ child.stdout.on('data', data => {
+ stdoutBuffer.push(data);
+ combinedBuffer.push(data);
+ });
+ return new Promise(resolve => {
+ child.on('close', (exitCode: number) => {
+ resolve({
+ exitCode,
+ get stdout() {
+ return stdoutBuffer.join('');
+ },
+ get stderr() {
+ return stderrBuffer.join('');
+ },
+ get output() {
+ return combinedBuffer.join('');
+ },
+ });
+ });
+ });
+}
diff --git a/packages/core/package.json b/packages/core/package.json
index 757c0c618..23bb444cc 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@embroider/core",
- "version": "3.4.14",
+ "version": "3.4.15",
"private": false,
"description": "A build system for EmberJS applications.",
"repository": {
diff --git a/packages/macros/package.json b/packages/macros/package.json
index c14fee21e..96555ad3d 100644
--- a/packages/macros/package.json
+++ b/packages/macros/package.json
@@ -1,6 +1,6 @@
{
"name": "@embroider/macros",
- "version": "1.16.5",
+ "version": "1.16.6",
"private": false,
"description": "Standardized build-time macros for ember apps.",
"keywords": [
diff --git a/packages/shared-internals/package.json b/packages/shared-internals/package.json
index c6aa44a28..a9bed2e38 100644
--- a/packages/shared-internals/package.json
+++ b/packages/shared-internals/package.json
@@ -1,6 +1,6 @@
{
"name": "@embroider/shared-internals",
- "version": "2.6.2",
+ "version": "2.6.3",
"private": false,
"description": "Utilities shared among the other embroider packages",
"repository": {
diff --git a/packages/shared-internals/src/ember-standard-modules.ts b/packages/shared-internals/src/ember-standard-modules.ts
index 8ccaf15c7..8250b8b72 100644
--- a/packages/shared-internals/src/ember-standard-modules.ts
+++ b/packages/shared-internals/src/ember-standard-modules.ts
@@ -31,6 +31,7 @@ emberVirtualPeerDeps.add('@ember/string');
// (like snowpack) not to worry about these packages.
emberVirtualPackages.add('@glimmer/env');
emberVirtualPackages.add('ember');
+emberVirtualPackages.add('ember-testing');
// this is a real package and even though most of its primary API is implemented
// as transforms, it does include some runtime code.
@@ -45,6 +46,13 @@ emberVirtualPeerDeps.add('ember-source');
// the modules-api-polyfill. Newer APIs need to be added here.
emberVirtualPackages.add('@ember/owner');
+// Added in ember-source 4.5.0-beta.1
+emberVirtualPackages.add('@ember/renderer');
+
+// Not provided by rfc176-data, but is needed for special librarys
+// that know the dangers of importing private APIs
+emberVirtualPackages.add('@ember/-internals');
+
// these are not public API but they're included in ember-source, so for
// correctness we still want to understand that they come from there.
emberVirtualPackages.add('@glimmer/validator');
diff --git a/packages/util/package.json b/packages/util/package.json
index 24237cc70..854e8db96 100644
--- a/packages/util/package.json
+++ b/packages/util/package.json
@@ -1,6 +1,6 @@
{
"name": "@embroider/util",
- "version": "1.13.1",
+ "version": "1.13.2",
"description": "Utilities for app and addon authors.",
"keywords": [
"ember-addon"
diff --git a/packages/webpack/package.json b/packages/webpack/package.json
index b90d73190..ed51eedaf 100644
--- a/packages/webpack/package.json
+++ b/packages/webpack/package.json
@@ -1,6 +1,6 @@
{
"name": "@embroider/webpack",
- "version": "4.0.4",
+ "version": "4.0.5",
"private": false,
"description": "Builds EmberJS apps with Webpack",
"repository": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 14e9e146d..dc9c402c0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -178,6 +178,9 @@ importers:
'@babel/core':
specifier: ^7.14.5
version: 7.25.2
+ '@babel/plugin-syntax-decorators':
+ specifier: ^7.24.7
+ version: 7.24.7(@babel/core@7.25.2)
'@babel/plugin-syntax-dynamic-import':
specifier: ^7.8.3
version: 7.8.3(@babel/core@7.25.2)
diff --git a/tests/scenarios/template-tag-codemod-test.ts b/tests/scenarios/template-tag-codemod-test.ts
new file mode 100644
index 000000000..2d9a08f7f
--- /dev/null
+++ b/tests/scenarios/template-tag-codemod-test.ts
@@ -0,0 +1,48 @@
+import { readFileSync } from 'fs-extra';
+import { appScenarios } from './scenarios';
+import QUnit from 'qunit';
+import { join } from 'path';
+
+const { module: Qmodule, test } = QUnit;
+
+appScenarios
+ .only('release')
+ .map('template-tag-codemod', project => {
+ project.mergeFiles({
+ app: {
+ components: {
+ 'face.hbs': ` this is a gjs file
`,
+ },
+ },
+ 'ember-cli-build.js': `'use strict';
+
+const EmberApp = require('ember-cli/lib/broccoli/ember-app');
+
+module.exports = function (defaults) {
+ const app = new EmberApp(defaults, {
+ // Add options here
+ });
+ return require('@embroider/compat').templateTagCodemod(app, {});
+};`,
+ });
+ })
+ .forEachScenario(async scenario => {
+ Qmodule(`${scenario.name}`, function (/* hooks */) {
+ test('running the codemod works', async function (assert) {
+ let app = await scenario.prepare();
+ await app.execute('node ./node_modules/ember-cli/bin/ember b');
+
+ // TODO figure out how to get assert.codeContains to understand template tag
+ const fileContents = readFileSync(join(app.dir, 'app/components/face.gjs'), 'utf-8');
+ assert.equal(
+ fileContents,
+ `export default
+ this is a gjs file
+;`
+ );
+ // TODO figure out how to get around the protection in place to not delete unversioned files
+ // we do git rm for the very reason we avoid possible destructive operations
+ // assert.ok(!existsSync(join(app.dir, 'app/components/face.hbs')), 'template only component gets deleted');
+ });
+ });
+ });