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 = ``; + } + }, + }, + }; + }, + ], + }); + + //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 ;` + ); + // 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'); + }); + }); + });