From e3a85e1f4778f681663d04e39c0b92b9eb225bce Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 1 Jul 2025 12:06:19 -0400 Subject: [PATCH 01/12] Add `--minimal` and `--no-compat` --- README.md | 43 +++++++++++ index.js | 161 ++++++++++++++++++++++++++++++++++++++++- tests/minimal.test.mjs | 156 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 358 insertions(+), 2 deletions(-) create mode 100644 tests/minimal.test.mjs diff --git a/README.md b/README.md index cf8f804a..1fe9d061 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,46 @@ If you have an existing app that you would like to upgrade to use Vite consider ``` pnpm dlx ember-cli@latest new my-app-name -b @ember/app-blueprint --pnpm ``` + +## Options + +### `--no-compat` + +``` +pnpm dlx ember-cli@latest new my-app-name \ + --blueprint @ember/app-blueprint@alpha \ + --pnpm \ + --no-compat +``` + +Does the following: +- enables `type=module` in package.json (required for vite-ssr, and many ESM tools) +- makes the build and boot _MUCH FASTER_ + (in large apps, this can have your app's boot be up to 1 minute faster) +- removes `@embroider/compat` + - removes support for: + - hbs (both for component files, and testing) + - content-for (in the HTML files) + - v1 addons + - node-land config/environment.js +- removes `ember-cli` + - ember-cli brings in a ton of old dependencies, so removing it makes installs much faster + - downside though is that you no longer have scaffolding (`ember g`) -- however, you could use `pnpm dlx ember-cli g ...` (or `npx ember-cli g`) + +### `--minimal` + +``` +pnpm dlx ember-cli@latest new my-app-name \ + --blueprint @ember/app-blueprint@alpha \ + --pnpm \ + --minimal +``` + +Does the following +- everything listed under `--no-compat` +- Removes all linting, formatting, and testing support + - leaves you with a minimal app that you can use for demos, and PRing to other repositories that have multi-framework support (and probably use other testing tools for that multi-framework support) +- different defaults: + - warp-drive becomes _opt-in_ (pass `--warp-drive` if you want it -- normally requires `--no-warp-drive` to remove) + - ember-welcome-page becomes _opt-in_ (normally requires `--no-welcome` to remove) + diff --git a/index.js b/index.js index 8da9e21a..489ab07b 100644 --- a/index.js +++ b/index.js @@ -80,6 +80,8 @@ module.exports = { options.packageManager === 'pnpm' && '"--pnpm"', options.ciProvider && `"--ci-provider=${options.ciProvider}"`, options.typescript && `"--typescript"`, + options.minimal && `"--minimal"`, + options.noCompat && `"--no-compat"`, !options.emberData && `"--no-ember-data"`, !options.warpDrive && `"--no-warp-drive"`, ] @@ -101,6 +103,37 @@ module.exports = { execBinPrefix = 'pnpm'; } + let welcome = options.welcome; + let warpDrive = options.warpDrive ?? options.emberData; + let minimal = false; + let compat = true; + /** + * --minimal overrides compat/no-compat + */ + if (options.minimal) { + minimal = true; + compat = false; + + // Invert defaults + { + welcome = options.welcome = process.argv.includes('--welcome'); + warpDrive = + options.emberData = + options.warpDrive = + process.argv.includes('--ember-data') || + process.argv.includes('--warp-drive'); + } + } + + if (!minimal) { + if (options.noCompat) { + compat = false; + } + } + + let noCompat = !compat; + let notMinimal = !minimal; + return { appDirectory: directoryForPackageName(name), name, @@ -113,14 +146,18 @@ module.exports = { options.packageManager !== 'yarn' && options.packageManager !== 'pnpm', invokeScriptPrefix, execBinPrefix, - welcome: options.welcome, + welcome, blueprint: 'app', blueprintOptions, lang: options.lang, - warpDrive: options.warpDrive ?? options.emberData, + warpDrive: warpDrive, ciProvider: options.ciProvider, typescript: options.typescript, packageManager: options.packageManager ?? 'npm', + compat, + noCompat, + minimal, + notMinimal, }; }, @@ -131,6 +168,10 @@ module.exports = { let files = this._super(); + // Locals is where we calculate defaults and such. + // Let's not duplicate that work here + options = this.locals(options); + if (options.ciProvider !== 'github') { files = files.filter((file) => file.indexOf('.github') < 0); } @@ -150,6 +191,43 @@ module.exports = { files = files.filter((file) => !file.includes('services/.gitkeep')); } + if (options.noCompat) { + files = files.filter((file) => { + return ( + !file.includes('deprecation-workflow') && + !file.includes('ember-cli') && + !file.includes('ember-cli-build.js') && + !file.includes('controllers/') && + !file.includes('config/environment.js') && + !file.includes('config/optional-features') && + !file.includes('config/targets') && + !file.includes('helpers/') + ); + }); + } + + if (options.minimal) { + files = files.filter((file) => { + return ( + !file.includes('.github/') && + !file.includes('.prettierignore') && + !file.includes('README') && + !file.includes('components/') && + !file.includes('eslint.config') && + !file.includes('prettierrc') && + !file.includes('public/') && + !file.includes('routes/') && + !file.includes('services/') && + !file.includes('stylelint') && + !file.includes('styles/') && + !file.includes('template-lintrc') && + !file.includes('testem') && + !file.includes('tests/') && + !file.includes('watchman') + ); + }); + } + this._files = files; return this._files; @@ -182,6 +260,85 @@ module.exports = { updatePackageJson(options, content) { let contents = JSON.parse(content); + if (options.minimal) { + // Remove linting + { + delete contents.scripts['format']; + delete contents.scripts['lint']; + delete contents.scripts['lint:format']; + delete contents.scripts['lint:fix']; + delete contents.scripts['lint:js']; + delete contents.scripts['lint:js:fix']; + delete contents.scripts['lint:css']; + delete contents.scripts['lint:css:fix']; + delete contents.scripts['lint:hbs']; + delete contents.scripts['lint:hbs:fix']; + + delete contents.devDependencies['@babel/eslint-parser']; + delete contents.devDependencies['@eslint/js']; + delete contents.devDependencies['concurrently']; + delete contents.devDependencies['ember-template-lint']; + delete contents.devDependencies['eslint']; + delete contents.devDependencies['eslint-config-prettier']; + delete contents.devDependencies['eslint-plugin-ember']; + delete contents.devDependencies['eslint-plugin-n']; + delete contents.devDependencies['eslint-plugin-qunit']; + delete contents.devDependencies['eslint-plugin-warp-drive']; + delete contents.devDependencies['globals']; + delete contents.devDependencies['prettier']; + delete contents.devDependencies['prettier-plugin-ember-template-tag']; + delete contents.devDependencies['stylelint']; + delete contents.devDependencies['stylelint-config-standard']; + delete contents.devDependencies['typescript-eslint']; + } + // Remove testing + { + delete contents.scripts['test']; + delete contents.devDependencies['@ember/test-helpers']; + delete contents.devDependencies['@ember/test-waiters']; + delete contents.devDependencies['ember-qunit']; + delete contents.devDependencies['qunit']; + delete contents.devDependencies['qunit-dom']; + delete contents.devDependencies['testem']; + } + // Extraneous / non-core deps. + // if folks go minimal, they know what they are doing + { + delete contents.devDependencies['ember-welcome-page']; + delete contents.devDependencies['tracked-built-ins']; + delete contents.devDependencies['ember-page-title']; + delete contents.devDependencies['ember-modifier']; + } + // common-in-the-vite-ecosystem alias + { + contents.scripts.dev = contents.scripts.start; + } + } + if (options.noCompat) { + contents.type = 'module'; + contents.engines.node = '>= 24'; + delete contents.directories; + delete contents.devDependencies['@ember/string']; + delete contents.devDependencies['@ember/optional-features']; + delete contents.devDependencies['@embroider/compat']; + delete contents.devDependencies['@embroider/config-meta-loader']; + delete contents.devDependencies['ember-resolver']; + // Users should use npx ember-cli instead + delete contents.devDependencies['ember-cli']; + delete contents.devDependencies['ember-cli-babel']; + delete contents.devDependencies['ember-load-initializers']; + // This arguable should still exist, but it's a v1 addon + delete contents.devDependencies['ember-cli-deprecation-workflow']; + + // A nice feature of modern apps is using sub-path imports + // Why specify the whole app name, when you can use `#`? + contents.imports = { + '#app/*': './app/*', + '#config': './app/config/environment', + '#components/*': './app/components/*', + }; + } + return stringifyAndNormalize(sortPackageJson(contents)); }, }; diff --git a/tests/minimal.test.mjs b/tests/minimal.test.mjs new file mode 100644 index 00000000..edb86685 --- /dev/null +++ b/tests/minimal.test.mjs @@ -0,0 +1,156 @@ +import { describe, it, expect } from 'vitest'; +import { join } from 'path'; +import { existsSync, writeFileSync } from 'fs'; +import stripAnsi from 'strip-ansi'; +import { newProjectWithFixtures } from './helpers.mjs'; + +const SCENARIOS = [ + { + name: 'no-compat', + flags: ['--no-compat'], + fixturePath: join(import.meta.dirname, 'fixture-gjs-only'), + }, + { + name: 'minimal', + flags: ['--minimal'], + fixturePath: join(import.meta.dirname, 'fixture-gjs-only'), + }, + { + name: 'typescript + no-compat', + flags: ['--typescript', '--no-compat'], + fixturePath: join(import.meta.dirname, 'fixture-gts-only'), + }, + { + name: 'typescript + minimal', + flags: ['--typescript', '--minimal'], + fixturePath: join(import.meta.dirname, 'fixture-gts-only'), + }, +]; + +describe('basic functionality', function () { + for (let { name, flags, packageJson, fixturePath } of SCENARIOS) { + describe(name, function () { + let project = newProjectWithFixtures({ + fixturePath, + flags, + packageJson, + }); + + it('verify files', async function () { + expect( + !existsSync(join(project.dir(), 'app/index.html')), + 'the app index.html has been removed', + ); + expect( + existsSync(join(project.dir(), 'index.html')), + 'the root index.html has been added', + ); + }); + + it('successfully lints', async function () { + let result = await project.execa('pnpm', ['lint']); + + console.log(result.stdout); + }); + + it('successfully builds', async function () { + let result = await project.execa('pnpm', ['build']); + + console.log(result.stdout); + }); + + it('successfully runs tests', async function () { + let result; + + try { + result = await project.execa('pnpm', ['test']); + } catch (err) { + console.log(err.stdout, err.stderr); + throw 'Failed to successfully run test'; + } + + // make sure that each of the tests that we expect actually show up + // alternatively we can change this to search for `pass 3` + expect(result.stdout).to.contain( + 'Acceptance | welcome page: visiting /index shows the welcome page', + ); + expect(result.stdout).to.contain( + 'Acceptance | styles: visiting /styles', + ); + + console.log(result.stdout); + }); + + it('successfully runs tests in dev mode', async function () { + await project.$`pnpm install --save-dev testem http-proxy`; + let appURL; + + let server; + + try { + server = project.execa('pnpm', ['start']); + + await new Promise((resolve) => { + server.stdout.on('data', (line) => { + let result = /Local:\s+(https?:\/\/.*)\//g.exec( + stripAnsi(line.toString()), + ); + + if (result) { + appURL = result[1]; + resolve(); + } + }); + }); + + writeFileSync( + join(project.dir(), 'testem-dev.js'), + `module.exports = { + test_page: 'tests/index.html?hidepassed', + disable_watching: true, + launch_in_ci: ['Chrome'], + launch_in_dev: ['Chrome'], + browser_start_timeout: 120, + browser_args: { + Chrome: { + ci: [ + // --no-sandbox is needed when running Chrome inside a container + process.env.CI ? '--no-sandbox' : null, + '--headless', + '--disable-dev-shm-usage', + '--disable-software-rasterizer', + '--mute-audio', + '--remote-debugging-port=0', + '--window-size=1440,900', + ].filter(Boolean), + }, + }, + middleware: [ + require(__dirname + '/testem-proxy.js')('${appURL}') + ], +}; +`, + ); + + let testResult = await project.execa('pnpm', [ + 'testem', + '--file', + 'testem-dev.js', + 'ci', + ]); + expect(testResult.exitCode).to.eq(0, testResult.output); + } finally { + server?.kill('SIGINT'); + } + }); + + it('successfully optimizes deps', function () { + return project.execa('pnpm', ['vite', 'optimize', '--force']); + }); + + it('can run generators', function () { + return project.execa('pnpm', ['ember', 'g', 'route', 'fancy']); + }); + }); + } +}); From 7c6909bc0ed49fd839500294fae06754245c3406 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:13:14 -0500 Subject: [PATCH 02/12] Remove classicEmberSupport --- files/vite.config.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/files/vite.config.mjs b/files/vite.config.mjs index 219253db..4d25021f 100644 --- a/files/vite.config.mjs +++ b/files/vite.config.mjs @@ -1,10 +1,10 @@ import { defineConfig } from 'vite'; -import { extensions, classicEmberSupport, ember } from '@embroider/vite'; +import { extensions<% if (!noCompat) { %>, classicEmberSupport<% } %>, ember } from '@embroider/vite'; import { babel } from '@rollup/plugin-babel'; export default defineConfig({ - plugins: [ - classicEmberSupport(), + plugins: [<% if (!noCompat) { %> + classicEmberSupport(),<% } %> ember(), // extra plugins here babel({ From e4d122ff85124eba5f14c2ace10c20652caf17cf Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:16:06 -0500 Subject: [PATCH 03/12] Update lint ignores --- eslint.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index 7f3a1fc2..3dbe8a43 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -27,6 +27,7 @@ export default [ { ignores: [ 'tests/fixtures/*', + 'files/vite.config.*', 'files/ember-cli-build.js', 'conditional-files/_js_*', 'conditional-files/_ts_*', From b43790ce30e5139c565c9cc5c1cec5ac8f8a9ae6 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:22:01 -0500 Subject: [PATCH 04/12] Update babel configs --- .../no-compat/_js_babel.config.mjs | 38 +++++++++++++++ .../no-compat/_ts_babel.config.mjs | 46 +++++++++++++++++++ index.js | 9 +++- 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 conditional-files/no-compat/_js_babel.config.mjs create mode 100644 conditional-files/no-compat/_ts_babel.config.mjs diff --git a/conditional-files/no-compat/_js_babel.config.mjs b/conditional-files/no-compat/_js_babel.config.mjs new file mode 100644 index 00000000..bb5b946c --- /dev/null +++ b/conditional-files/no-compat/_js_babel.config.mjs @@ -0,0 +1,38 @@ +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { buildMacros } from '@embroider/macros/babel'; + +const macros = buildMacros(); + +export default { + plugins: [ + [ + 'babel-plugin-ember-template-compilation', + { + compilerPath: 'ember-source/dist/ember-template-compiler.js', + transforms: [...macros.templateMacros], + }, + ], + [ + 'module:decorator-transforms', + { + runtime: { + import: import.meta.resolve('decorator-transforms/runtime-esm'), + }, + }, + ], + [ + '@babel/plugin-transform-runtime', + { + absoluteRuntime: dirname(fileURLToPath(import.meta.url)), + useESModules: true, + regenerator: false, + }, + ], + ...macros.babelMacros, + ], + + generatorOpts: { + compact: false, + }, +}; diff --git a/conditional-files/no-compat/_ts_babel.config.mjs b/conditional-files/no-compat/_ts_babel.config.mjs new file mode 100644 index 00000000..124ffe6a --- /dev/null +++ b/conditional-files/no-compat/_ts_babel.config.mjs @@ -0,0 +1,46 @@ +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { buildMacros } from '@embroider/macros/babel'; + +const macros = buildMacros(); + +export default { + plugins: [ + [ + '@babel/plugin-transform-typescript', + { + allExtensions: true, + onlyRemoveTypeImports: true, + allowDeclareFields: true, + }, + ], + [ + 'babel-plugin-ember-template-compilation', + { + compilerPath: 'ember-source/dist/ember-template-compiler.js', + transforms: [...macros.templateMacros()], + }, + ], + [ + 'module:decorator-transforms', + { + runtime: { + import: import.meta.resolve('decorator-transforms/runtime-esm'), + }, + }, + ], + [ + '@babel/plugin-transform-runtime', + { + absoluteRuntime: dirname(fileURLToPath(import.meta.url)), + useESModules: true, + regenerator: false, + }, + ], + ...macros.babelMacros(), + ], + + generatorOpts: { + compact: false, + }, +}; diff --git a/index.js b/index.js index 489ab07b..ea4fa608 100644 --- a/index.js +++ b/index.js @@ -37,7 +37,14 @@ const replacers = { }, 'babel.config.mjs'(locals) { let prefix = locals.typescript ? '_ts_' : '_js_'; - let filePath = join(CONDITIONAL_FILES, prefix + 'babel.config.mjs'); + + let filePath = join( + [ + CONDITIONAL_FILES, + locals.noCompat && 'no-compat', + prefix + 'babel.config.mjs', + ].filter(Boolean), + ); let raw = readFileSync(filePath).toString(); From 66f80650dda3c0276a0c480d5f1fe93b72fa3dc9 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:31:36 -0500 Subject: [PATCH 05/12] Update the HTMLs --- files/index.html | 11 ++++++----- files/tests/index.html | 14 ++++++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/files/index.html b/files/index.html index 05f81312..e5b7dd9a 100644 --- a/files/index.html +++ b/files/index.html @@ -5,25 +5,26 @@