├── .npmrc
├── .gitignore
├── .prettierignore
├── .prettierrc.js
├── tests
├── fixtures
│ ├── app
│ │ └── components
│ │ │ └── welcome-page.js
│ ├── tests
│ │ ├── acceptance
│ │ │ └── index-test.js
│ │ └── helpers
│ │ │ └── index.js
│ ├── testem-dev.js
│ └── testem-proxy.js
├── standard.test.js
├── typescript.test.js
├── webpack.test.js
└── test-helpers.js
├── vitest.config.js
├── lib
├── utils
│ ├── get-app-name.js
│ ├── detect-typescript.js
│ ├── exit.js
│ ├── resolve-version.js
│ ├── resolve-with-extension.js
│ ├── jsonc.js
│ ├── run.js
│ └── jsonc.test.js
├── tasks
│ ├── move-index.js
│ ├── remove-lingering-files.js
│ ├── check-modulePrefix-mismatch.js
│ ├── check-git-status.js
│ ├── add-missing-files.js
│ ├── add-missing-files.test.js
│ ├── transform-files.js
│ ├── ensure-no-unsupported-deps.js
│ ├── ensure-v2-addons.test.js
│ ├── update-package-json.js
│ ├── ensure-v2-addons.js
│ ├── ensure-no-unsupported-deps.test.js
│ └── update-package-json.test.js
└── transforms
│ ├── environment.js
│ ├── typesIndex.js
│ ├── tsconfig.js
│ ├── testem.js
│ ├── typesIndex.test.js
│ ├── app.js
│ ├── index-html.js
│ ├── test-helper.js
│ ├── ember-cli-build.test.js
│ └── ember-cli-build.js
├── eslint.config.js
├── .github
├── ISSUE_TEMPLATE
│ └── issue-template.md
└── workflows
│ ├── publish.yml
│ ├── ci.yml
│ └── plan-release.yml
├── .release-plan.json
├── RELEASE.md
├── package.json
├── index.js
├── CHANGELOG.md
└── README.md
/.npmrc:
--------------------------------------------------------------------------------
1 | use-node-version=20.18.3
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | .eslintcache
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /pnpm-lock.yaml
2 | CHANGELOG.md
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | export default {
2 | singleQuote: true,
3 | };
4 |
--------------------------------------------------------------------------------
/tests/fixtures/app/components/welcome-page.js:
--------------------------------------------------------------------------------
1 | // we need this because the classic build will nullify the welcome page :(
2 | export { default } from 'ember-welcome-page/components/welcome-page';
3 |
--------------------------------------------------------------------------------
/vitest.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config';
2 |
3 | export default defineConfig({
4 | test: {
5 | testTimeout: process.env.TEST_TIMEOUT ?? 600_000,
6 | },
7 | });
8 |
--------------------------------------------------------------------------------
/lib/utils/get-app-name.js:
--------------------------------------------------------------------------------
1 | import { join } from 'node:path';
2 | import { pathToFileURL } from 'node:url';
3 |
4 | export async function getAppName() {
5 | const ENV = await import(
6 | pathToFileURL(join(process.cwd(), 'config/environment.js'))
7 | );
8 | return ENV.default().modulePrefix;
9 | }
10 |
--------------------------------------------------------------------------------
/lib/utils/detect-typescript.js:
--------------------------------------------------------------------------------
1 | import { globSync } from 'glob';
2 |
3 | export function detectTypescript() {
4 | const typescriptFiles = globSync('{tsconfig.json,**/*.{ts,cts,mts,gts}}', {
5 | cwd: process.cwd(),
6 | ignore: 'node_modules/**',
7 | });
8 |
9 | return typescriptFiles.length > 0;
10 | }
11 |
--------------------------------------------------------------------------------
/lib/utils/exit.js:
--------------------------------------------------------------------------------
1 | export class ExitError extends Error {
2 | constructor(messages, ...args) {
3 | super(
4 | (Array.isArray(messages) ? messages : [messages]).join('\n'),
5 | ...args,
6 | );
7 | }
8 | }
9 |
10 | export function isExit(error) {
11 | return error instanceof ExitError;
12 | }
13 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import globals from 'globals';
2 | import pluginJs from '@eslint/js';
3 | import eslintConfigPrettier from 'eslint-config-prettier';
4 |
5 | /** @type {import('eslint').Linter.Config[]} */
6 | export default [
7 | { languageOptions: { globals: globals.node } },
8 | pluginJs.configs.recommended,
9 | eslintConfigPrettier,
10 | ];
11 |
--------------------------------------------------------------------------------
/lib/tasks/move-index.js:
--------------------------------------------------------------------------------
1 | import { rename } from 'node:fs/promises';
2 | import { existsSync } from 'node:fs';
3 |
4 | export default async function moveIndex() {
5 | if (existsSync('index.html')) {
6 | console.warn("Skiping file 'index.html' since it already exists.");
7 | return;
8 | }
9 | await rename('app/index.html', 'index.html');
10 | }
11 |
--------------------------------------------------------------------------------
/lib/transforms/environment.js:
--------------------------------------------------------------------------------
1 | import { getAppName } from '../utils/get-app-name.js';
2 |
3 | export default async function transformEnvironment(code) {
4 | const modulePrefix = await getAppName();
5 |
6 | // app/config/environment.js is created out of the app blueprint,
7 | // that uses <%= name %> placeholder for the app prefix.
8 | code = code.replace('<%= name %>', modulePrefix);
9 | return code;
10 | }
11 |
--------------------------------------------------------------------------------
/lib/transforms/typesIndex.js:
--------------------------------------------------------------------------------
1 | export default async function transformTypesIndex(code) {
2 | if (!code.includes('vite/client')) {
3 | code = `/// \n${code}`;
4 | }
5 | if (
6 | !code.includes('@embroider/core/virtual') &&
7 | !code.includes('@embroider/core/types/virtual')
8 | ) {
9 | code = `/// \n${code}`;
10 | }
11 | return code;
12 | }
13 |
--------------------------------------------------------------------------------
/tests/fixtures/tests/acceptance/index-test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from 'qunit';
2 | import { visit, currentURL } from '@ember/test-helpers';
3 | import { setupApplicationTest } from '../helpers';
4 |
5 | module('Acceptance | index', function (hooks) {
6 | setupApplicationTest(hooks);
7 |
8 | test('visiting /index', async function (assert) {
9 | await visit('/');
10 |
11 | assert.strictEqual(currentURL(), '/');
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/lib/utils/resolve-version.js:
--------------------------------------------------------------------------------
1 | import shared from '@embroider/shared-internals';
2 | const packageCache = new shared.PackageCache(process.cwd());
3 | const app = packageCache.get(packageCache.appRoot);
4 |
5 | export function resolveVersion(packageName) {
6 | try {
7 | return packageCache.resolve(packageName, app).version;
8 | } catch (err) {
9 | if (err.code === 'MODULE_NOT_FOUND') {
10 | return undefined;
11 | }
12 | throw err;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/lib/utils/resolve-with-extension.js:
--------------------------------------------------------------------------------
1 | import { existsSync } from 'node:fs';
2 |
3 | const extensions = ['.js', '.ts', '.cjs', '.mjs'];
4 |
5 | /**
6 | * @param {string} filePath
7 | */
8 | export function resolveWithExt(filePath) {
9 | for (let ext of extensions) {
10 | let candidate = filePath + ext;
11 |
12 | if (existsSync(candidate)) return candidate;
13 | }
14 |
15 | throw new Error(
16 | `Could not find ${filePath} with any of the extensions: ${extensions.join(', ')}`,
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/lib/tasks/remove-lingering-files.js:
--------------------------------------------------------------------------------
1 | import { rm } from 'node:fs/promises';
2 |
3 | const FILES_TO_REMOVE = {
4 | ts: ['app/config/environment.d.ts'],
5 | js: [],
6 | };
7 |
8 | export default async function addMissingFiles({ ts } = { ts: false }) {
9 | await Promise.all(
10 | FILES_TO_REMOVE[ts ? 'ts' : 'js'].map(async (file) => {
11 | try {
12 | await rm(file);
13 | } catch (error) {
14 | if (error.code !== 'ENOENT') {
15 | throw error;
16 | }
17 | }
18 | }),
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/tests/standard.test.js:
--------------------------------------------------------------------------------
1 | import { describe, it } from 'vitest';
2 |
3 | import { testVersions, executeTest, getPort } from './test-helpers';
4 |
5 | describe.concurrent('Test standard blueprint on Ember', function () {
6 | it.for(testVersions)(
7 | 'should work for ember version %s',
8 | async function ([version, packages], { expect }) {
9 | await executeTest(
10 | expect,
11 | version,
12 | packages,
13 | ['--skip-npm', '--pnpm'],
14 | getPort(),
15 | ['--skip-git', '--skip-v2-addon'],
16 | );
17 | },
18 | );
19 | });
20 |
--------------------------------------------------------------------------------
/lib/utils/jsonc.js:
--------------------------------------------------------------------------------
1 | import { modify, applyEdits } from 'jsonc-parser';
2 |
3 | /**
4 | *
5 | * @param {string} source
6 | * @param {string} propertyPath - eg. foo[2].bar.baz
7 | * @param {any} value
8 | * @returns {string}
9 | */
10 | export function modifyJsonc(source, propertyPath, value) {
11 | const paths =
12 | typeof propertyPath === 'string'
13 | ? propertyPath.split(/[.[\]]+/).filter(Boolean)
14 | : propertyPath;
15 | const edits = modify(source, paths, value, {
16 | formattingOptions: { insertSpaces: true, tabSize: 2 },
17 | });
18 |
19 | return applyEdits(source, edits);
20 | }
21 |
--------------------------------------------------------------------------------
/tests/typescript.test.js:
--------------------------------------------------------------------------------
1 | import { describe, it } from 'vitest';
2 |
3 | import { testVersions, executeTest, getPort } from './test-helpers';
4 |
5 | describe.concurrent('Test typescript blueprint on Ember', function () {
6 | it.for(testVersions)(
7 | 'should work for ember version %s with typescript',
8 | async function ([version, packages], { expect }) {
9 | await executeTest(
10 | expect,
11 | version,
12 | packages,
13 | ['--typescript', '--skip-npm', '--pnpm'],
14 | getPort(),
15 | ['--skip-git', '--skip-v2-addon', '--ts'],
16 | );
17 | },
18 | );
19 | });
20 |
--------------------------------------------------------------------------------
/lib/tasks/check-modulePrefix-mismatch.js:
--------------------------------------------------------------------------------
1 | import { ExitError } from '../utils/exit.js';
2 | import { getAppName } from '../utils/get-app-name.js';
3 | import { readFile } from 'node:fs/promises';
4 |
5 | export async function checkModulePrefixMisMatch() {
6 | const packageJSON = JSON.parse(await readFile('package.json', 'utf-8'));
7 | const modulePrefix = await getAppName();
8 |
9 | if (packageJSON.name === modulePrefix) return;
10 |
11 | throw new ExitError(
12 | `Unexpected modulePrefix mismatch! package.json#name is ${packageJSON.name}, but modulePrefix is ${modulePrefix}. These two values should match`,
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/tests/webpack.test.js:
--------------------------------------------------------------------------------
1 | import { describe, it } from 'vitest';
2 |
3 | import { testVersions, executeTest, getPort } from './test-helpers';
4 |
5 | describe.concurrent('Test webpack blueprint on Ember', function () {
6 | it.for(testVersions)(
7 | 'should work for ember version %s with webpack',
8 | async function ([version, packages], { expect }) {
9 | await executeTest(
10 | expect,
11 | version,
12 | packages,
13 | ['--embroider', '--skip-npm', '--pnpm'],
14 | getPort(),
15 | ['--skip-git', '--skip-v2-addon', '--embroider-webpack'],
16 | );
17 | },
18 | );
19 | });
20 |
--------------------------------------------------------------------------------
/lib/transforms/tsconfig.js:
--------------------------------------------------------------------------------
1 | import { get } from 'lodash-es';
2 | import { parse as jsoncParse } from 'jsonc-parser';
3 |
4 | import { modifyJsonc } from '../utils/jsonc.js';
5 |
6 | export default async function transformTsConfig(tsconfig) {
7 | const before = jsoncParse(tsconfig);
8 | const types = get(before, 'compilerOptions.types') ?? [];
9 | const hasTypes =
10 | types.includes('@embroider/core/virtual') && types.includes('vite/client');
11 |
12 | if (hasTypes) {
13 | return tsconfig;
14 | }
15 |
16 | const after = modifyJsonc(tsconfig, 'compilerOptions.types', [
17 | ...new Set([...types, '@embroider/core/virtual', 'vite/client']),
18 | ]);
19 |
20 | return after;
21 | }
22 |
--------------------------------------------------------------------------------
/tests/fixtures/testem-dev.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | test_page: '/tests?hidepassed',
5 | disable_watching: true,
6 | launch_in_ci: ['Chrome'],
7 | launch_in_dev: ['Chrome'],
8 | browser_start_timeout: 120,
9 | browser_args: {
10 | Chrome: {
11 | ci: [
12 | // --no-sandbox is needed when running Chrome inside a container
13 | process.env.CI ? '--no-sandbox' : null,
14 | '--headless',
15 | '--disable-dev-shm-usage',
16 | '--disable-software-rasterizer',
17 | '--mute-audio',
18 | '--remote-debugging-port=0',
19 | '--window-size=1440,900',
20 | ].filter(Boolean),
21 | },
22 | },
23 | middleware: [
24 | require('./testem-proxy').testemProxy(
25 | process.env.HOST ?? 'http://localhost:4200',
26 | '/',
27 | ),
28 | ],
29 | };
30 |
--------------------------------------------------------------------------------
/lib/utils/run.js:
--------------------------------------------------------------------------------
1 | import yocto from 'yocto-spinner';
2 |
3 | const nl = (msg) => (msg ? `\n${msg}` : msg);
4 |
5 | export async function run(message, fn, ...options) {
6 | const spinner = yocto().start(message);
7 | const oLog = console.log;
8 | const oWarn = console.warn;
9 | const oError = console.error;
10 |
11 | // Prefix messages with \n to not interfere with the spinner
12 | console.log = (message, ...args) => oLog(nl(message), ...args);
13 | console.warn = (message, ...args) => oWarn(nl(message), ...args);
14 | console.error = (message, ...args) => oError(nl(message), ...args);
15 |
16 | try {
17 | await fn(...options, spinner);
18 | spinner.success(message);
19 | } catch (error) {
20 | spinner.error(error.message);
21 |
22 | throw error;
23 | } finally {
24 | console.log = oLog;
25 | console.warn = oWarn;
26 | console.error = oError;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/lib/utils/jsonc.test.js:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { modifyJsonc } from './jsonc.js';
3 |
4 | describe('jsonc utils', () => {
5 | describe('modifyJsonc()', () => {
6 | it('modifies jsonc content without affecting commments', async () => {
7 | const after = modifyJsonc(
8 | `
9 | {
10 | "aString": "foo",
11 | "aNumber": 42,
12 | // comment
13 | "anArray": ["foo", "bar"],
14 | "anObject": {
15 | "foo": 1337,
16 | },
17 | }
18 | `,
19 | 'anArray',
20 | ['baz'],
21 | );
22 |
23 | expect(after).toMatchInlineSnapshot(`
24 | "
25 | {
26 | "aString": "foo",
27 | "aNumber": 42,
28 | // comment
29 | "anArray": [
30 | "baz"
31 | ],
32 | "anObject": {
33 | "foo": 1337,
34 | },
35 | }
36 | "
37 | `);
38 | });
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/issue-template.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Issue template
3 | about: Issue opens with a comment addressed to the developer
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 | ---
8 |
9 |
20 |
--------------------------------------------------------------------------------
/lib/tasks/check-git-status.js:
--------------------------------------------------------------------------------
1 | import { exec } from 'node:child_process';
2 | import { promisify } from 'node:util';
3 | import { ExitError, isExit } from '../utils/exit.js';
4 |
5 | export default async function checkGitStatus() {
6 | const promiseExec = promisify(exec);
7 |
8 | try {
9 | const { stdout } = await promiseExec(`git status --porcelain`);
10 | if (stdout) {
11 | throw new ExitError([
12 | 'The git repository is not clean.',
13 | `This codemod will add, move and modify files in your Ember app. It's highly recommended to commit or stash your local changes before you run it. To skip this error and execute the codemod anyway, run it with the option --skip-git.`,
14 | ]);
15 | }
16 | } catch (e) {
17 | if (isExit(e)) {
18 | throw e;
19 | }
20 |
21 | throw new ExitError([
22 | e.message,
23 | `Could not check if the git repository is clean. Are you running the script out of a git repository?`,
24 | `This codemod will add, move and modify files in your Ember app. Make sure you can deal confidently with changes before running the script. To skip this error and execute the codemod, run it with the option --skip-git.`,
25 | ]);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tests/fixtures/testem-proxy.js:
--------------------------------------------------------------------------------
1 | const httpProxy = require('http-proxy');
2 |
3 | /*
4 | This can be installed as a testem middleware to make testem run against an
5 | arbitrary real webserver at targetURL.
6 |
7 | It allows testem to handle the well-known testem-specific paths and proxies
8 | everything else, while rewriting the testem-added prefix out of your
9 | "/tests/index.html" URL.
10 | */
11 |
12 | module.exports.testemProxy = function testemProxy(targetURL, base = '/') {
13 | return function testemProxyHandler(app) {
14 | const proxy = httpProxy.createProxyServer({
15 | changeOrigin: true,
16 | ignorePath: true,
17 | });
18 |
19 | proxy.on('error', (err, _req, res) => {
20 | res && res.status && res.status(500).json(err);
21 | });
22 |
23 | app.all('*', (req, res, next) => {
24 | let url = req.url;
25 | if (url === `${base}testem.js` || url.startsWith('/testem/')) {
26 | req.url = req.url.replace(base, '/');
27 | return next();
28 | }
29 | let m = /^(\/\d+).*\/tests($|.)+/.exec(url);
30 | if (m) {
31 | url = url.slice(m[1].length);
32 | }
33 | proxy.web(req, res, { target: targetURL + url });
34 | });
35 | };
36 | };
37 |
--------------------------------------------------------------------------------
/.release-plan.json:
--------------------------------------------------------------------------------
1 | {
2 | "solution": {
3 | "ember-vite-codemod": {
4 | "impact": "minor",
5 | "oldVersion": "1.3.0",
6 | "newVersion": "1.4.0",
7 | "constraints": [
8 | {
9 | "impact": "minor",
10 | "reason": "Appears in changelog section :rocket: Enhancement"
11 | },
12 | {
13 | "impact": "patch",
14 | "reason": "Appears in changelog section :bug: Bug Fix"
15 | }
16 | ],
17 | "pkgJSONPath": "./package.json"
18 | }
19 | },
20 | "description": "## Release (2025-11-29)\n\nember-vite-codemod 1.4.0 (minor)\n\n#### :rocket: Enhancement\n* `ember-vite-codemod`\n * [#124](https://github.com/mainmatter/ember-vite-codemod/pull/124) Merge `ensure-ember-cli` and `ensure-no-unsupported-deps` ([@BlueCutOfficial](https://github.com/BlueCutOfficial))\n * [#126](https://github.com/mainmatter/ember-vite-codemod/pull/126) Reduce the size of the output for ensure-v2-addons ([@BlueCutOfficial](https://github.com/BlueCutOfficial))\n\n#### :bug: Bug Fix\n* `ember-vite-codemod`\n * [#125](https://github.com/mainmatter/ember-vite-codemod/pull/125) Add `ember-cli-deprecation-workflow` to the list of v1 compatible addons ([@BlueCutOfficial](https://github.com/BlueCutOfficial))\n\n#### Committers: 1\n- Marine Dunstetter ([@BlueCutOfficial](https://github.com/BlueCutOfficial))\n"
21 | }
22 |
--------------------------------------------------------------------------------
/lib/transforms/testem.js:
--------------------------------------------------------------------------------
1 | import { types } from 'recast';
2 |
3 | const b = types.builders;
4 |
5 | export default async function transformTestem(ast) {
6 | // Add: if (typeof module !== 'undefined')
7 | const moduleExportsIndex = ast.program.body.findIndex((node) => {
8 | return (
9 | node.type === 'ExpressionStatement' &&
10 | node.expression.type === 'AssignmentExpression' &&
11 | node.expression.left.type === 'MemberExpression' &&
12 | node.expression.left.object.type === 'Identifier' &&
13 | node.expression.left.object.name === 'module' &&
14 | node.expression.left.property.type === 'Identifier' &&
15 | node.expression.left.property.name === 'exports'
16 | );
17 | });
18 |
19 | if (moduleExportsIndex < 0) {
20 | console.log(
21 | "testem.js: 'module.export = {...}' expression was not found at the first level of testem.js; does the file already fit the new requirements?",
22 | );
23 | return ast;
24 | }
25 |
26 | const moduleExport = ast.program.body[moduleExportsIndex];
27 |
28 | const ifStatement = b.ifStatement(
29 | b.binaryExpression(
30 | '!==',
31 | b.unaryExpression('typeof', b.identifier('module')),
32 | b.literal('undefined'),
33 | ),
34 | b.blockStatement([moduleExport]),
35 | );
36 |
37 | ast.program.body[moduleExportsIndex] = ifStatement;
38 |
39 | return ast;
40 | }
41 |
--------------------------------------------------------------------------------
/tests/fixtures/tests/helpers/index.js:
--------------------------------------------------------------------------------
1 | // this is only a fixture because ember@3.28 didn't have this structure and the index test wouldn't work
2 |
3 | import {
4 | setupApplicationTest as upstreamSetupApplicationTest,
5 | setupRenderingTest as upstreamSetupRenderingTest,
6 | setupTest as upstreamSetupTest,
7 | } from 'ember-qunit';
8 |
9 | // This file exists to provide wrappers around ember-qunit's / ember-mocha's
10 | // test setup functions. This way, you can easily extend the setup that is
11 | // needed per test type.
12 |
13 | function setupApplicationTest(hooks, options) {
14 | upstreamSetupApplicationTest(hooks, options);
15 |
16 | // Additional setup for application tests can be done here.
17 | //
18 | // For example, if you need an authenticated session for each
19 | // application test, you could do:
20 | //
21 | // hooks.beforeEach(async function () {
22 | // await authenticateSession(); // ember-simple-auth
23 | // });
24 | //
25 | // This is also a good place to call test setup functions coming
26 | // from other addons:
27 | //
28 | // setupIntl(hooks); // ember-intl
29 | // setupMirage(hooks); // ember-cli-mirage
30 | }
31 |
32 | function setupRenderingTest(hooks, options) {
33 | upstreamSetupRenderingTest(hooks, options);
34 |
35 | // Additional setup for rendering tests can be done here.
36 | }
37 |
38 | function setupTest(hooks, options) {
39 | upstreamSetupTest(hooks, options);
40 |
41 | // Additional setup for unit tests can be done here.
42 | }
43 |
44 | export { setupApplicationTest, setupRenderingTest, setupTest };
45 |
--------------------------------------------------------------------------------
/lib/transforms/typesIndex.test.js:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import transformTypesIndex from './typesIndex';
3 |
4 | describe('transformTypesIndex() function', () => {
5 | it('does not add the code for @embroider/core/virtual if already found', async () => {
6 | let code = `/// `;
7 | code = await transformTypesIndex(code);
8 | expect(code).toMatchInlineSnapshot(`
9 | "///
10 | /// "
11 | `);
12 | });
13 |
14 | it('does not add the code for @embroider/core/virtual if @embroider/core/types/virtual is found', async () => {
15 | let code = `import '@embroider/core/types/virtual';`;
16 | code = await transformTypesIndex(code);
17 | expect(code).toMatchInlineSnapshot(`
18 | "///
19 | import '@embroider/core/types/virtual';"
20 | `);
21 | });
22 |
23 | it('does not add the code for vite/client if already found', async () => {
24 | let code = `/// `;
25 | code = await transformTypesIndex(code);
26 | expect(code).toMatchInlineSnapshot(`
27 | "///
28 | /// "
29 | `);
30 | });
31 |
32 | it('adds the code for @embroider/core/virtual and vite/client', async () => {
33 | let code = ``;
34 | code = await transformTypesIndex(code);
35 | expect(code).toMatchInlineSnapshot(`
36 | "///
37 | ///
38 | "
39 | `);
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/RELEASE.md:
--------------------------------------------------------------------------------
1 | # Release Process
2 |
3 | Releases in this repo are mostly automated using [release-plan](https://github.com/embroider-build/release-plan/). Once you label all your PRs correctly (see below) you will have an automatically generated PR that updates your CHANGELOG.md file and a `.release-plan.json` that is used to prepare the release once the PR is merged.
4 |
5 | ## Preparation
6 |
7 | Since the majority of the actual release process is automated, the remaining tasks before releasing are:
8 |
9 | - correctly labeling **all** pull requests that have been merged since the last release
10 | - updating pull request titles so they make sense to our users
11 |
12 | Some great information on why this is important can be found at [keepachangelog.com](https://keepachangelog.com/en/1.1.0/), but the overall
13 | guiding principle here is that changelogs are for humans, not machines.
14 |
15 | When reviewing merged PR's the labels to be used are:
16 |
17 | - breaking - Used when the PR is considered a breaking change.
18 | - enhancement - Used when the PR adds a new feature or enhancement.
19 | - bug - Used when the PR fixes a bug included in a previous release.
20 | - documentation - Used when the PR adds or updates documentation.
21 | - internal - Internal changes or things that don't fit in any other category.
22 |
23 | **Note:** `release-plan` requires that **all** PRs are labeled. If a PR doesn't fit in a category it's fine to label it as `internal`
24 |
25 | ## Release
26 |
27 | Once the prep work is completed, the actual release is straight forward: you just need to merge the open [Plan Release](https://github.com/mainmatter/ember-vite-codemod/pulls?q=is%3Apr+is%3Aopen+%22Prepare+Release%22+in%3Atitle) PR
28 |
--------------------------------------------------------------------------------
/lib/tasks/add-missing-files.js:
--------------------------------------------------------------------------------
1 | import { existsSync, mkdirSync } from 'node:fs';
2 | import { readFile, stat, writeFile } from 'node:fs/promises';
3 | import { createRequire } from 'node:module';
4 | import { dirname, join } from 'node:path';
5 | import { removeTypes } from 'babel-remove-types';
6 |
7 | const require = createRequire(import.meta.url);
8 |
9 | const blueprintPath = dirname(require.resolve('@ember/app-blueprint'));
10 |
11 | const files = [
12 | ['vite.config.mjs', 'files/vite.config.mjs'],
13 | ['.env.development', 'files/.env.development'],
14 | ];
15 |
16 | const jsFiles = [
17 | ['babel.config.cjs', 'files/_js_babel.config.cjs'],
18 | ['app/config/environment.js', 'files/app/config/environment.ts'],
19 | ];
20 |
21 | const tsFiles = [
22 | ['babel.config.cjs', 'files/_ts_babel.config.cjs'],
23 | ['app/config/environment.ts', 'files/app/config/environment.ts'],
24 | ];
25 |
26 | async function writeIfMissing([destination, source], options = { ts: false }) {
27 | try {
28 | await stat(destination);
29 | } catch {
30 | if (!existsSync(dirname(destination))) {
31 | mkdirSync(dirname(destination));
32 | }
33 |
34 | let sourceContents = await readFile(join(blueprintPath, source), 'utf8');
35 | if (!options.ts && source.endsWith('.ts')) {
36 | sourceContents = await removeTypes(sourceContents);
37 | }
38 | return writeFile(destination, sourceContents);
39 | }
40 |
41 | console.warn(`Skipping file '${destination}' since it already exists.`);
42 | }
43 |
44 | export default async function addMissingFiles(options = { ts: false }) {
45 | const filesToAdd = [...files];
46 |
47 | if (options.ts) {
48 | filesToAdd.push(...tsFiles);
49 | } else {
50 | filesToAdd.push(...jsFiles);
51 | }
52 |
53 | for (let file of filesToAdd) {
54 | await writeIfMissing(file, options);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | # For every push to the master branch, this checks if the release-plan was
2 | # updated and if it was it will publish stable npm packages based on the
3 | # release plan
4 |
5 | name: Publish Stable
6 |
7 | on:
8 | workflow_dispatch:
9 | push:
10 | branches:
11 | - main
12 | - master
13 |
14 | concurrency:
15 | group: publish-${{ github.head_ref || github.ref }}
16 | cancel-in-progress: true
17 |
18 | jobs:
19 | check-plan:
20 | name: 'Check Release Plan'
21 | runs-on: ubuntu-latest
22 | outputs:
23 | command: ${{ steps.check-release.outputs.command }}
24 |
25 | steps:
26 | - uses: actions/checkout@v4
27 | with:
28 | fetch-depth: 0
29 | ref: 'main'
30 | # This will only cause the `check-plan` job to have a result of `success`
31 | # when the .release-plan.json file was changed on the last commit. This
32 | # plus the fact that this action only runs on main will be enough of a guard
33 | - id: check-release
34 | run: if git diff --name-only HEAD HEAD~1 | grep -w -q ".release-plan.json"; then echo "command=release"; fi >> $GITHUB_OUTPUT
35 |
36 | publish:
37 | name: 'NPM Publish'
38 | runs-on: ubuntu-latest
39 | needs: check-plan
40 | if: needs.check-plan.outputs.command == 'release'
41 | permissions:
42 | contents: write
43 | pull-requests: write
44 | id-token: write
45 | attestations: write
46 |
47 | steps:
48 | - uses: actions/checkout@v4
49 | - uses: pnpm/action-setup@v4
50 | - uses: actions/setup-node@v4
51 | with:
52 | node-version: 18
53 | # This creates an .npmrc that reads the NODE_AUTH_TOKEN environment variable
54 | registry-url: 'https://registry.npmjs.org'
55 | cache: pnpm
56 | - run: pnpm install --frozen-lockfile
57 | - name: npm publish
58 | run: NPM_CONFIG_PROVENANCE=true pnpm release-plan publish
59 | env:
60 | GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }}
61 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
62 |
--------------------------------------------------------------------------------
/lib/transforms/app.js:
--------------------------------------------------------------------------------
1 | import { types, visit } from 'recast';
2 | import { getAppName } from '../utils/get-app-name.js';
3 |
4 | const b = types.builders;
5 |
6 | export default async function transformApp(ast) {
7 | const imports = ast.program.body.filter(
8 | (node) => node.type === 'ImportDeclaration',
9 | );
10 | const modulePrefix = await getAppName();
11 |
12 | // New location app/config/environment
13 | const configImport = imports.find(
14 | (node) => node.source.value === `${modulePrefix}/config/environment`,
15 | );
16 | if (configImport) {
17 | configImport.source = b.literal('./config/environment');
18 | }
19 |
20 | // Add import @embroider/virtual/compat-modules
21 | const compatModulesImport = imports.find(
22 | (node) => node.source.value === '@embroider/virtual/compat-modules',
23 | );
24 |
25 | if (!compatModulesImport) {
26 | const insertImportIndex = imports.length;
27 | ast.program.body.splice(
28 | insertImportIndex,
29 | 0,
30 | b.importDeclaration(
31 | [b.importDefaultSpecifier(b.identifier('compatModules'))],
32 | b.literal('@embroider/virtual/compat-modules'),
33 | ),
34 | );
35 | }
36 |
37 | visit(ast, {
38 | // Change Resolver value to Resolver.withModules(compatModules)
39 | visitClassProperty(path) {
40 | if (
41 | path.value.key.type === 'Identifier' &&
42 | path.value.key.name === 'Resolver'
43 | ) {
44 | path.value.value = b.callExpression(
45 | b.memberExpression(
46 | b.identifier('Resolver'),
47 | b.identifier('withModules'),
48 | ),
49 | [b.identifier('compatModules')],
50 | );
51 | }
52 | return false;
53 | },
54 |
55 | // Add compatModules argument to loadInitializers
56 | visitCallExpression(path) {
57 | if (
58 | path.value.callee.type === 'Identifier' &&
59 | path.value.callee.name === 'loadInitializers' &&
60 | !path.value.arguments.find(
61 | (arg) => arg.type === 'Identifier' && arg.name === 'compatModules',
62 | )
63 | ) {
64 | path.value.arguments.push(b.identifier('compatModules'));
65 | }
66 | return false;
67 | },
68 | });
69 |
70 | return ast;
71 | }
72 |
--------------------------------------------------------------------------------
/lib/transforms/index-html.js:
--------------------------------------------------------------------------------
1 | import { getAppName } from '../utils/get-app-name.js';
2 |
3 | export default async function transformIndexHTML(code, { isTest }) {
4 | const sharedReplacements = [
5 | {
6 | before: '{{rootURL}}assets/vendor.css',
7 | after: '/@embroider/virtual/vendor.css',
8 | },
9 | {
10 | before: '{{rootURL}}assets/${MODULE_PREFIX}.css',
11 | after: '/@embroider/virtual/app.css',
12 | },
13 | {
14 | before: `{{rootURL}}assets/vendor.js`,
15 | after: `/@embroider/virtual/vendor.js`,
16 | },
17 | ];
18 |
19 | const appReplacements = [
20 | {
21 | before: '',
22 | after: ``,
28 | },
29 | ];
30 |
31 | const testReplacements = [
32 | {
33 | before: '{{rootURL}}assets/test-support.css',
34 | after: '/@embroider/virtual/test-support.css',
35 | },
36 | {
37 | before: '{{rootURL}}assets/test-support.js',
38 | after: '/@embroider/virtual/test-support.js',
39 | },
40 | {
41 | before: '',
42 | after: '',
43 | },
44 | {
45 | before: '',
46 | after: `
47 | `,
52 | },
53 | {
54 | before: '{{content-for "test-body-footer"}}',
55 | after: '',
56 | },
57 | ];
58 |
59 | const replacements = isTest
60 | ? [...sharedReplacements, ...testReplacements]
61 | : [...sharedReplacements, ...appReplacements];
62 |
63 | const modulePrefix = await getAppName();
64 | for (const replacement of replacements) {
65 | code = code.replaceAll(
66 | replacement.before.replace('${MODULE_PREFIX}', modulePrefix),
67 | replacement.after.replace('${MODULE_PREFIX}', modulePrefix),
68 | );
69 | }
70 | return code;
71 | }
72 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ember-vite-codemod",
3 | "version": "1.4.0",
4 | "description": "",
5 | "keywords": [],
6 | "repository": {
7 | "type": "git",
8 | "url": "git@github.com:mainmatter/ember-vite-codemod.git"
9 | },
10 | "license": "MIT",
11 | "author": "",
12 | "type": "module",
13 | "bin": "index.js",
14 | "scripts": {
15 | "format": "prettier . --cache --write",
16 | "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto",
17 | "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\" --prefixColors auto && npm run format",
18 | "lint:format": "prettier . --cache --check",
19 | "lint:js": "eslint . --cache",
20 | "lint:js:fix": "eslint . --fix",
21 | "test": "vitest",
22 | "test:lib": "vitest lib/**/*.test.js"
23 | },
24 | "dependencies": {
25 | "@ember/app-blueprint": "^6.7.2",
26 | "@embroider/shared-internals": "^3.0.1",
27 | "babel-remove-types": "^1.0.1",
28 | "commander": "^13.1.0",
29 | "execa": "^9.5.2",
30 | "glob": "^11.0.3",
31 | "jsonc-parser": "^3.3.1",
32 | "lodash-es": "^4.17.21",
33 | "recast": "^0.23.9",
34 | "semver": "^7.7.1",
35 | "yocto-spinner": "^1.0.0",
36 | "yoctocolors": "^2.1.2"
37 | },
38 | "devDependencies": {
39 | "@eslint/js": "^9.20.0",
40 | "concurrently": "^9.1.2",
41 | "ember-cli-3.28": "npm:ember-cli@~3.28.0",
42 | "ember-cli-4.12": "npm:ember-cli@~4.12.0",
43 | "ember-cli-4.4": "npm:ember-cli@~4.4.0",
44 | "ember-cli-4.8": "npm:ember-cli@~4.8.0",
45 | "ember-cli-5.12": "npm:ember-cli@~5.12.0",
46 | "ember-cli-5.4": "npm:ember-cli@~5.4.0",
47 | "ember-cli-5.8": "npm:ember-cli@~5.8.0",
48 | "ember-cli-6.4": "npm:ember-cli@~6.4.0",
49 | "ember-cli-latest": "npm:ember-cli@latest",
50 | "eslint": "^9.20.1",
51 | "eslint-config-prettier": "^10.0.1",
52 | "fixturify": "^3.0.0",
53 | "globals": "^15.15.0",
54 | "package-up": "^5.0.0",
55 | "prettier": "^3.5.1",
56 | "release-plan": "^0.13.1",
57 | "strip-ansi": "^7.1.0",
58 | "tmp": "^0.2.3",
59 | "vitest": "^3.0.7"
60 | },
61 | "packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af",
62 | "pnpm": {
63 | "onlyBuiltDependencies": [
64 | "core-js",
65 | "esbuild"
66 | ]
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/lib/tasks/add-missing-files.test.js:
--------------------------------------------------------------------------------
1 | import { describe, beforeEach, it, expect, vi } from 'vitest';
2 | import addMissingFiles from './add-missing-files';
3 |
4 | let initialFiles;
5 | let addedFiles;
6 |
7 | vi.mock('node:fs', async () => {
8 | return {
9 | existsSync: () => {},
10 | mkdirSync: () => {},
11 | };
12 | });
13 |
14 | vi.mock('node:fs/promises', async (importOriginal) => {
15 | const actual = await importOriginal();
16 | return {
17 | ...actual,
18 | stat: (destination) => {
19 | if (!initialFiles[destination]) {
20 | throw Error('This file is not part of the initial files fixture.');
21 | }
22 | return initialFiles[destination];
23 | },
24 | writeFile: (filename, content) => {
25 | addedFiles[filename] = content;
26 | },
27 | };
28 | });
29 |
30 | const consoleWarn = vi
31 | .spyOn(console, 'warn')
32 | .mockImplementation(() => undefined);
33 |
34 | describe('addMissingFiles() function', () => {
35 | beforeEach(() => {
36 | initialFiles = {};
37 | addedFiles = {};
38 | consoleWarn.mockClear();
39 | });
40 |
41 | it('it adds files that are not language-specific', async () => {
42 | await addMissingFiles();
43 | expect(addedFiles['vite.config.mjs']).toBeDefined();
44 | expect(addedFiles['.env.development']).toBeDefined();
45 | });
46 |
47 | it('it does not add files that are already present', async () => {
48 | initialFiles = {
49 | '.env.development': '# env content',
50 | };
51 | await addMissingFiles();
52 |
53 | expect(addedFiles['vite.config.mjs']).toBeDefined();
54 | expect(addedFiles['.env.development']).toBeUndefined();
55 | expect(consoleWarn).toBeCalledTimes(1);
56 | expect(consoleWarn).toBeCalledWith(
57 | "Skipping file '.env.development' since it already exists.",
58 | );
59 | });
60 |
61 | it('it adds JS-specific files with .js extension, but not TS-specific files', async () => {
62 | await addMissingFiles();
63 | expect(addedFiles['babel.config.cjs']).not.toMatch(
64 | /@babel\/plugin-transform-typescript/,
65 | );
66 | expect(addedFiles['app/config/environment.js']).toBeDefined();
67 | expect(addedFiles['app/config/environment.ts']).toBeUndefined();
68 | });
69 |
70 | it('it adds TS-specific files, but not JS-specific files', async () => {
71 | await addMissingFiles({ ts: true });
72 | expect(addedFiles['babel.config.cjs']).toMatch(
73 | /@babel\/plugin-transform-typescript/,
74 | );
75 | expect(addedFiles['app/config/environment.ts']).toBeDefined();
76 | expect(addedFiles['app/config/environment.js']).toBeUndefined();
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - master
8 | pull_request: {}
9 |
10 | concurrency:
11 | group: ci-${{ github.head_ref || github.ref }}
12 | cancel-in-progress: true
13 |
14 | jobs:
15 | lint:
16 | runs-on: ubuntu-latest
17 | timeout-minutes: 10
18 | steps:
19 | - uses: actions/checkout@v4
20 | - uses: pnpm/action-setup@v4
21 | - uses: actions/setup-node@v4
22 | with:
23 | node-version: 18
24 | cache: pnpm
25 | - run: pnpm i --frozen-lockfile
26 | - run: pnpm run lint
27 |
28 | test-lib:
29 | runs-on: ubuntu-latest
30 | timeout-minutes: 10
31 | steps:
32 | - uses: actions/checkout@v4
33 | - uses: pnpm/action-setup@v4
34 | - uses: actions/setup-node@v4
35 | with:
36 | node-version: 18
37 | cache: pnpm
38 | - run: pnpm i --frozen-lockfile
39 | - run: pnpm test:lib
40 |
41 | test:
42 | runs-on: ${{matrix.os}}
43 | timeout-minutes: 20
44 | strategy:
45 | fail-fast: false
46 | matrix:
47 | file:
48 | - standard
49 | - typescript
50 | - webpack
51 | version:
52 | - ember-cli-3.28
53 | - ember-cli-4.4
54 | - ember-cli-4.8
55 | - ember-cli-4.12
56 | - ember-cli-5.4
57 | - ember-cli-5.8
58 | - ember-cli-5.12
59 | - ember-cli-6.4
60 | - ember-cli-latest
61 | os:
62 | - ubuntu-latest
63 | include:
64 | - version: ember-cli-latest
65 | os: windows-latest
66 | file: standard
67 | - version: ember-cli-latest
68 | os: windows-latest
69 | file: typescript
70 | - version: ember-cli-latest
71 | os: windows-latest
72 | file: webpack
73 | exclude:
74 | - file: webpack
75 | version: ember-cli-3.28
76 | - file: webpack
77 | version: ember-cli-4.4
78 | - file: webpack
79 | version: ember-cli-4.8
80 | - file: webpack
81 | version: ember-cli-4.12
82 | - file: typescript
83 | version: ember-cli-3.28
84 | - file: typescript
85 | version: ember-cli-4.4
86 | - file: typescript
87 | version: ember-cli-4.8
88 | - file: typescript
89 | version: ember-cli-4.12
90 | steps:
91 | - uses: actions/checkout@v4
92 | - uses: pnpm/action-setup@v4
93 | - uses: actions/setup-node@v4
94 | with:
95 | node-version: 18
96 | cache: pnpm
97 | - run: pnpm i --frozen-lockfile
98 | - name: Set TEMP to D:/Temp on windows
99 | if: ${{matrix.os}} == windows-latest
100 | run: |
101 | mkdir "D:\\Temp"
102 | echo "TEMP=D:\\Temp" >> $env:GITHUB_ENV
103 | - run: pnpm vitest -t ${{matrix.version}} tests/${{ matrix.file }}.test.js
104 | env:
105 | TEST_TIMEOUT: "${{ matrix.os == 'windows-latest' && '1200000' || '' }}" # because windows is VERY slow :(
106 |
107 |
--------------------------------------------------------------------------------
/lib/tasks/transform-files.js:
--------------------------------------------------------------------------------
1 | import { parse, print } from 'recast';
2 | import babelParser from 'recast/parsers/babel.js';
3 | import { readFile, writeFile } from 'node:fs/promises';
4 | import transformEmberCliBuild from '../transforms/ember-cli-build.js';
5 | import transformEnvironment from '../transforms/environment.js';
6 | import transformIndexHTML from '../transforms/index-html.js';
7 | import transformTestem from '../transforms/testem.js';
8 | import transformTestHelper from '../transforms/test-helper.js';
9 | import transformApp from '../transforms/app.js';
10 | import transformTsConfig from '../transforms/tsconfig.js';
11 | import transformTypesIndex from '../transforms/typesIndex.js';
12 | import { resolveWithExt } from '../utils/resolve-with-extension.js';
13 | import { existsSync } from 'node:fs';
14 |
15 | export default async function modifyFiles(options) {
16 | await transformWithReplace('index.html', transformIndexHTML, {
17 | isTest: false,
18 | });
19 | await transformWithReplace('tests/index.html', transformIndexHTML, {
20 | isTest: true,
21 | });
22 |
23 | if (options.ts) {
24 | await transformWithReplace(
25 | 'app/config/environment.ts',
26 | transformEnvironment,
27 | );
28 | } else {
29 | await transformWithReplace(
30 | 'app/config/environment.js',
31 | transformEnvironment,
32 | );
33 | }
34 |
35 | if (options.ts) {
36 | if (existsSync('types/index.d.ts')) {
37 | await transformWithReplace('types/index.d.ts', transformTypesIndex);
38 | } else {
39 | await transformWithReplace('tsconfig.json', transformTsConfig);
40 | }
41 | }
42 |
43 | await transformWithAst(
44 | resolveWithExt('ember-cli-build'),
45 | transformEmberCliBuild,
46 | options,
47 | );
48 | await transformWithAst(resolveWithExt('testem'), transformTestem, options);
49 | await transformWithAst(resolveWithExt('app/app'), transformApp, options);
50 | await transformWithAst(
51 | resolveWithExt('tests/test-helper'),
52 | transformTestHelper,
53 | options,
54 | );
55 |
56 | try {
57 | let code = await readFile('.gitignore', 'utf-8');
58 | if (!code.match(/\/?tmp\/?[\n\r]/)) {
59 | await writeFile('.gitignore', `/tmp/\n\n${code}`, 'utf-8');
60 | }
61 | } catch (e) {
62 | console.log('Skiping file .gitignore since it was not found.');
63 | if (options.errorTrace) {
64 | console.log(e);
65 | }
66 | }
67 | }
68 |
69 | async function transformWithReplace(file, transformFunction, options) {
70 | let code = await readFile(file, 'utf-8');
71 | code = await transformFunction(code, options);
72 | await writeFile(file, code, 'utf-8');
73 | }
74 |
75 | async function transformWithAst(
76 | file,
77 | transformFunction,
78 | { embroiderWebpack, errorTrace },
79 | ) {
80 | try {
81 | let ast = await getAst(file);
82 | ast = await transformFunction(ast, embroiderWebpack);
83 | await setCode(file, ast);
84 | } catch (e) {
85 | console.error(
86 | `${file}: Could not parse and transform. Use the option --error-trace to show the whole error.`,
87 | );
88 | if (errorTrace) {
89 | console.error(e);
90 | }
91 | }
92 | }
93 |
94 | async function getAst(file) {
95 | const code = await readFile(file, 'utf-8');
96 | return parse(code, { parser: babelParser });
97 | }
98 |
99 | async function setCode(file, ast) {
100 | const output = print(ast).code;
101 | await writeFile(file, output, 'utf-8');
102 | }
103 |
--------------------------------------------------------------------------------
/.github/workflows/plan-release.yml:
--------------------------------------------------------------------------------
1 | name: Release Plan Review
2 | on:
3 | push:
4 | branches:
5 | - main
6 | - master
7 | pull_request_target: # This workflow has permissions on the repo, do NOT run code from PRs in this workflow. See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
8 | types:
9 | - labeled
10 | - unlabeled
11 |
12 | concurrency:
13 | group: plan-release # only the latest one of these should ever be running
14 | cancel-in-progress: true
15 |
16 | jobs:
17 | check-plan:
18 | name: 'Check Release Plan'
19 | runs-on: ubuntu-latest
20 | outputs:
21 | command: ${{ steps.check-release.outputs.command }}
22 |
23 | steps:
24 | - uses: actions/checkout@v4
25 | with:
26 | fetch-depth: 0
27 | ref: 'main'
28 | # This will only cause the `check-plan` job to have a "command" of `release`
29 | # when the .release-plan.json file was changed on the last commit.
30 | - id: check-release
31 | run: if git diff --name-only HEAD HEAD~1 | grep -w -q ".release-plan.json"; then echo "command=release"; fi >> $GITHUB_OUTPUT
32 |
33 | prepare-release-notes:
34 | name: Prepare Release Notes
35 | runs-on: ubuntu-latest
36 | timeout-minutes: 5
37 | needs: check-plan
38 | permissions:
39 | contents: write
40 | issues: read
41 | pull-requests: write
42 | outputs:
43 | explanation: ${{ steps.explanation.outputs.text }}
44 | # only run on push event if plan wasn't updated (don't create a release plan when we're releasing)
45 | # only run on labeled event if the PR has already been merged
46 | if: (github.event_name == 'push' && needs.check-plan.outputs.command != 'release') || (github.event_name == 'pull_request_target' && github.event.pull_request.merged == true)
47 |
48 | steps:
49 | - uses: actions/checkout@v4
50 | # We need to download lots of history so that
51 | # github-changelog can discover what's changed since the last release
52 | with:
53 | fetch-depth: 0
54 | ref: 'main'
55 | - uses: pnpm/action-setup@v4
56 | - uses: actions/setup-node@v4
57 | with:
58 | node-version: 18
59 | cache: pnpm
60 | - run: pnpm install --frozen-lockfile
61 | - name: 'Generate Explanation and Prep Changelogs'
62 | id: explanation
63 | run: |
64 | set +e
65 | pnpm release-plan prepare 2> >(tee -a release-plan-stderr.txt >&2)
66 |
67 | if [ $? -ne 0 ]; then
68 | echo 'text<> $GITHUB_OUTPUT
69 | cat release-plan-stderr.txt >> $GITHUB_OUTPUT
70 | echo 'EOF' >> $GITHUB_OUTPUT
71 | else
72 | echo 'text<> $GITHUB_OUTPUT
73 | jq .description .release-plan.json -r >> $GITHUB_OUTPUT
74 | echo 'EOF' >> $GITHUB_OUTPUT
75 | rm release-plan-stderr.txt
76 | fi
77 | env:
78 | GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }}
79 |
80 | - uses: peter-evans/create-pull-request@v7
81 | with:
82 | commit-message: "Prepare Release using 'release-plan'"
83 | labels: 'internal'
84 | branch: release-preview
85 | title: Prepare Release
86 | body: |
87 | This PR is a preview of the release that [release-plan](https://github.com/embroider-build/release-plan) has prepared. To release you should just merge this PR 👍
88 |
89 | -----------------------------------------
90 |
91 | ${{ steps.explanation.outputs.text }}
92 |
--------------------------------------------------------------------------------
/lib/tasks/ensure-no-unsupported-deps.js:
--------------------------------------------------------------------------------
1 | import { readFile } from 'node:fs/promises';
2 | import { ExitError } from '../utils/exit.js';
3 | import semver from 'semver';
4 | import { resolveVersion } from '../utils/resolve-version.js';
5 |
6 | function checkDependency(packageJSON, dep, message) {
7 | const hasDep =
8 | packageJSON['dependencies']?.[dep] || packageJSON['devDependencies']?.[dep];
9 | if (hasDep) {
10 | return message;
11 | }
12 | }
13 |
14 | export default async function ensureNoUnsupportedDeps() {
15 | const emberResults = await ensureEmberCli();
16 | const addonsResults = await ensureKnownAddons();
17 | const logs = [...emberResults, ...addonsResults];
18 | if (logs.length) {
19 | throw new ExitError([
20 | 'Detected unsupported dependencies:',
21 | ...logs.map((log) => `\n* ${log}`),
22 | ]);
23 | }
24 | }
25 |
26 | export async function ensureKnownAddons() {
27 | const packageJSON = JSON.parse(await readFile('package.json', 'utf-8'));
28 | const logs = [
29 | checkDependency(
30 | packageJSON,
31 | 'ember-fetch',
32 | `Your app contains a dependency to ember-fetch. ember-fetch behaves a way that is incompatible with modern JavaScript tooling, including building with Vite.
33 | Please remove ember-fetch dependency then run this codemod again. Check out https://rfcs.emberjs.com/id/1065-remove-ember-fetch to see recommended alternatives.`,
34 | ),
35 | checkDependency(
36 | packageJSON,
37 | 'ember-composable-helpers',
38 | `Your app contains a dependency to ember-composable-helpers. ember-composable-helpers contains a "won't fix" Babel issue that makes it incompatible with Vite.
39 | Please move from the original ember-composable-helpers to @nullvoxpopuli/ember-composable-helpers then run this codemod again. Checkout the first section of the repository's README: https://github.com/NullVoxPopuli/ember-composable-helpers`,
40 | ),
41 | checkDependency(
42 | packageJSON,
43 | 'ember-cli-mirage',
44 | `Your app contains a dependency to ember-cli-mirage. ember-cli-mirage doesn't work correctly with Vite.
45 | Please move from ember-cli-mirage to ember-mirage then run this codemod again. Checkout https://github.com/bgantzler/ember-mirage/blob/main/docs/migration.md for guidance.`,
46 | ),
47 | checkDependency(
48 | packageJSON,
49 | 'ember-css-modules',
50 | `Your app contains a dependency to ember-css-modules. ember-css-modules behavior is incompatible with Vite, you should migrate to a different solution to manage your CSS modules.
51 | There is a recommended migration path that you can follow for a file by file migration to ember-scoped-css, which is compatible with Vite. Checkout https://github.com/BlueCutOfficial/css-modules-to-scoped-css`,
52 | ),
53 | ];
54 |
55 | return logs.filter((log) => Boolean(log));
56 | }
57 |
58 | export async function ensureEmberCli() {
59 | const emberSource = resolveVersion('ember-source');
60 | const logs = [];
61 |
62 | if (
63 | semver.lt(
64 | semver.coerce(emberSource, { includePrerelease: true }),
65 | semver.coerce('3.28.0', { includePrerelease: true }),
66 | )
67 | ) {
68 | logs.push(
69 | `ember-source ${emberSource} (< 3.28) was detected. Vite support is available from Ember 3.28 onwards.`,
70 | );
71 | }
72 |
73 | const emberCli = resolveVersion('ember-cli');
74 |
75 | if (
76 | semver.lt(
77 | semver.coerce(emberCli, { includePrerelease: true }),
78 | semver.coerce('4.12.0', { includePrerelease: true }),
79 | )
80 | ) {
81 | logs.push(
82 | `ember-cli ${emberCli} (< 4.12) was detected. Vite support requires at least ember-cli 4.12. You can update ember-cli independently of ember-source, Vite support is available from ember-source 3.28 onwards.`,
83 | );
84 | }
85 |
86 | return logs;
87 | }
88 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { program, Option } from 'commander';
3 | import { readFile } from 'node:fs/promises';
4 | import { dirname, join } from 'node:path';
5 | import { fileURLToPath } from 'node:url';
6 | import addMissingFiles from './lib/tasks/add-missing-files.js';
7 | import checkGitStatus from './lib/tasks/check-git-status.js';
8 | import ensureNoUnsupportedDeps from './lib/tasks/ensure-no-unsupported-deps.js';
9 | import ensureV2Addons from './lib/tasks/ensure-v2-addons.js';
10 | import moveIndex from './lib/tasks/move-index.js';
11 | import transformFiles from './lib/tasks/transform-files.js';
12 | import removeLingeringFiles from './lib/tasks/remove-lingering-files.js';
13 | import updatePackageJson from './lib/tasks/update-package-json.js';
14 | import { checkModulePrefixMisMatch } from './lib/tasks/check-modulePrefix-mismatch.js';
15 | import { detectTypescript } from './lib/utils/detect-typescript.js';
16 | import { isExit } from './lib/utils/exit.js';
17 | import { run } from './lib/utils/run.js';
18 |
19 | const __dirname = dirname(fileURLToPath(import.meta.url));
20 | const pkg = JSON.parse(await readFile(join(__dirname, 'package.json'), 'utf8'));
21 | const appPkg = JSON.parse(await readFile('package.json', 'utf-8'));
22 |
23 | program
24 | .option(
25 | '--skip-v2-addon',
26 | 'pursue the execution even when an upgradable v1 addon is detected',
27 | false,
28 | )
29 | .option(
30 | '--skip-git',
31 | 'pursue the execution even when not working in a clean git repository',
32 | false,
33 | )
34 | .option(
35 | '--embroider-webpack',
36 | 'indicate the app to migrate uses @embroider/webpack to build',
37 | false,
38 | )
39 | .addOption(
40 | new Option(
41 | '--ts',
42 | 'indicate the app to migrate uses TypeScript (default: true when TypeScript files are detected)',
43 | ).conflicts('js'),
44 | )
45 | .addOption(
46 | new Option(
47 | '--js',
48 | 'indicate the app to migrate uses JavaScript (default: true when no TypeScript files are detected)',
49 | ).conflicts('ts'),
50 | )
51 | .option('--error-trace', 'print the whole error trace when available', false)
52 | .version(pkg.version)
53 | .action(async (options) => {
54 | options.ts ??= !options.js && detectTypescript();
55 | delete options.js;
56 |
57 | if (options.embroiderWebpack) {
58 | console.warn(
59 | '--embroider-webpack option ignored. The codemod now adapts automatically if @embroider/webpack is found.\n',
60 | );
61 | }
62 | // Add an automatic option when @embroider/webpack is detected
63 | options.embroiderWebpack =
64 | appPkg['dependencies']?.['@embroider/webpack'] ||
65 | appPkg['devDependencies']?.['@embroider/webpack'] ||
66 | false;
67 | });
68 |
69 | await program.parseAsync();
70 |
71 | const options = program.opts();
72 |
73 | try {
74 | console.log(`\n🐹 Moving ${appPkg.name} to Vite\n`);
75 |
76 | // Tasks order is important
77 | if (!options.skipGit) {
78 | await run('Checking Git status', checkGitStatus, options);
79 | }
80 |
81 | await run('Checking modulePrefix', checkModulePrefixMisMatch, options);
82 | await run(
83 | 'Checking for unsupported dependencies',
84 | ensureNoUnsupportedDeps,
85 | options,
86 | );
87 |
88 | if (!options.skipV2Addon) {
89 | await run('Checking for v2 addons', ensureV2Addons, options);
90 | }
91 |
92 | await run('Creating new required files...', addMissingFiles, options);
93 | await run('Moving index.html', moveIndex, options);
94 |
95 | await run('Running code replacements...', transformFiles, options);
96 | await run('Updating package.json', updatePackageJson, options);
97 | await run('Removing lingering files', removeLingeringFiles, options);
98 |
99 | console.log(
100 | '\nAll set! Re-install the app dependencies then run your linter',
101 | );
102 | } catch (error) {
103 | if (!isExit(error)) {
104 | throw error;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/lib/transforms/test-helper.js:
--------------------------------------------------------------------------------
1 | import { types, visit } from 'recast';
2 |
3 | const b = types.builders;
4 |
5 | export default async function transformTestHelper(ast) {
6 | let exportStart = ast.program.body.find((node) => {
7 | return (
8 | node.type === 'ExportNamedDeclaration' &&
9 | node.declaration.type === 'FunctionDeclaration' &&
10 | node.declaration.id.type === 'Identifier' &&
11 | node.declaration.id.name === 'start'
12 | );
13 | });
14 | if (exportStart) {
15 | console.log(
16 | "test-helper.js: found 'export function start' at the first level of test-helper.js; does the file already fit the new requirements?",
17 | );
18 | return ast;
19 | }
20 |
21 | const allImports = ast.program.body.filter(
22 | (node) => node.type === 'ImportDeclaration',
23 | );
24 | const imports = allImports.filter(
25 | (node) => node.source.value !== 'ember-qunit/test-loader',
26 | );
27 |
28 | // Local ember-qunit.start is qunitStart to prevent conflict with the start function we define
29 | const emberQunitImport = imports.find(
30 | (node) => node.source.value === 'ember-qunit',
31 | );
32 | if (emberQunitImport) {
33 | const startSpecifier = emberQunitImport.specifiers.find((specifier) => {
34 | return (
35 | specifier.type === 'ImportSpecifier' &&
36 | specifier.imported.type === 'Identifier' &&
37 | specifier.imported.name === 'start'
38 | );
39 | });
40 | if (startSpecifier) {
41 | startSpecifier.local = b.identifier('qunitStart');
42 | // This instruction is a workaround to force recast reprint the specifier correctly,
43 | // related to https://github.com/benjamn/recast/issues/1171.
44 | // When this line is removed, we end up with 'qunitStart' instead of 'start as qunitStart'.
45 | startSpecifier.original = null;
46 | }
47 | } else {
48 | console.log(
49 | "tests/test-helper.js: No import from 'ember-qunit' was found. This is unexpected and it prevents this file to be transformed.",
50 | );
51 | return ast;
52 | }
53 |
54 | // Move all import declarations at the top
55 | ast.program.body.sort((a, b) => {
56 | const isImportA = a.type === 'ImportDeclaration';
57 | const isImportB = b.type === 'ImportDeclaration';
58 | if (isImportA && !isImportB) {
59 | return -1;
60 | } else if (!isImportA && isImportB) {
61 | return 1;
62 | }
63 | return 0;
64 | });
65 |
66 | visit(ast, {
67 | visitCallExpression(path) {
68 | if (path.value.callee.type === 'Identifier') {
69 | const { name } = path.value.callee;
70 | if (name === 'start') {
71 | // Replace call to start with qunitStart
72 | path.value.callee = b.identifier('qunitStart');
73 | return false;
74 | } else if (name === 'loadTests') {
75 | // Remove call to loadTests
76 | if (
77 | path.parentPath.value.type !== 'ExpressionStatement' ||
78 | path.parentPath.parentPath.name !== 'body'
79 | ) {
80 | console.log(
81 | 'tests/test-helper.js: loadTests() seems to be called in an unexpected context. Please remove this call manually, it is no longer used when building with Vite.',
82 | );
83 | } else {
84 | const body = path.parentPath.parentPath.value;
85 | const indexToRemove = body.findIndex(
86 | (statement) => statement === path.parentPath.value,
87 | );
88 | body.splice(indexToRemove, 1);
89 | }
90 | return false;
91 | }
92 | }
93 | this.traverse(path);
94 | },
95 | });
96 |
97 | // Wrap everything that is not an import in the start function
98 | const nodesToWrap = ast.program.body.slice(allImports.length);
99 | exportStart = b.exportNamedDeclaration(
100 | b.functionDeclaration(
101 | b.identifier('start'),
102 | [],
103 | b.blockStatement(nodesToWrap),
104 | ),
105 | );
106 | ast.program.body = [...imports, exportStart];
107 |
108 | return ast;
109 | }
110 |
--------------------------------------------------------------------------------
/lib/transforms/ember-cli-build.test.js:
--------------------------------------------------------------------------------
1 | import { parse, print } from 'recast';
2 | import babelParser from 'recast/parsers/babel.js';
3 | import { describe, it, expect } from 'vitest';
4 | import transformEmberCliBuild from './ember-cli-build.js';
5 |
6 | describe('ember-cli-build() function', () => {
7 | // Includes:
8 | // - it adds @embroider/compat import
9 | // - it adds @embroider/vite dynamic import
10 | // - if uses compatBuild and buildOnce in the return statement
11 | it('transforms a default ember-cli-build', async () => {
12 | let ast = await parse(
13 | `
14 | const EmberApp = require('ember-cli/lib/broccoli/ember-app');
15 | module.exports = function (defaults) {
16 | const app = new EmberApp(defaults, {});
17 | return app.toTree();
18 | };`,
19 | { parser: babelParser },
20 | );
21 |
22 | ast = await transformEmberCliBuild(ast, false);
23 | const output = print(ast).code;
24 |
25 | expect(output).toMatchInlineSnapshot(`
26 | "
27 | const EmberApp = require('ember-cli/lib/broccoli/ember-app');
28 |
29 | const {
30 | compatBuild
31 | } = require("@embroider/compat");
32 |
33 | module.exports = async function(defaults) {
34 | const {
35 | buildOnce
36 | } = await import("@embroider/vite");
37 |
38 | const app = new EmberApp(defaults, {});
39 | return compatBuild(app, buildOnce);
40 | };"
41 | `);
42 | });
43 |
44 | it('removes @embroider/webpack import at the top of the file (isEmbroiderWebpack)', async () => {
45 | let ast = await parse(
46 | `
47 | const EmberApp = require('ember-cli/lib/broccoli/ember-app');
48 | const { Webpack } = require('@embroider/webpack');`,
49 | { parser: babelParser },
50 | );
51 |
52 | ast = await transformEmberCliBuild(ast, true);
53 | const output = print(ast).code;
54 | expect(output).toMatchInlineSnapshot(`
55 | "
56 | const EmberApp = require('ember-cli/lib/broccoli/ember-app');
57 | const {
58 | compatBuild
59 | } = require("@embroider/compat");"
60 | `);
61 | });
62 |
63 | it('removes @embroider/webpack import in the exported function (isEmbroiderWebpack)', async () => {
64 | let ast = await parse(
65 | `
66 | const EmberApp = require('ember-cli/lib/broccoli/ember-app');
67 | module.exports = function (defaults) {
68 | const { Webpack } = require('@embroider/webpack');
69 | return require('@embroider/compat').compatBuild(app, Webpack, {});
70 | };`,
71 | { parser: babelParser },
72 | );
73 |
74 | ast = await transformEmberCliBuild(ast, true);
75 | const output = print(ast).code;
76 | expect(output).toMatchInlineSnapshot(`
77 | "
78 | const EmberApp = require('ember-cli/lib/broccoli/ember-app');
79 |
80 | const {
81 | compatBuild
82 | } = require("@embroider/compat");
83 |
84 | module.exports = async function(defaults) {
85 | const {
86 | buildOnce
87 | } = await import("@embroider/vite");
88 | return compatBuild(app, buildOnce, {});
89 | };"
90 | `);
91 | });
92 |
93 | it('preserves build options, except skipBabel (isEmbroiderWebpack)', async () => {
94 | let ast = await parse(
95 | `
96 | const EmberApp = require('ember-cli/lib/broccoli/ember-app');
97 | module.exports = function (defaults) {
98 | return require('@embroider/compat').compatBuild(app, Webpack, {
99 | staticEmberSource: true,
100 | staticAddonTrees: true,
101 | staticAddonTestSupportTrees: true,
102 | skipBabel: [
103 | {
104 | package: 'qunit',
105 | },
106 | ],
107 | });
108 | };`,
109 | { parser: babelParser },
110 | );
111 |
112 | ast = await transformEmberCliBuild(ast, true);
113 | const output = print(ast).code;
114 | expect(output).toMatchInlineSnapshot(`
115 | "
116 | const EmberApp = require('ember-cli/lib/broccoli/ember-app');
117 |
118 | const {
119 | compatBuild
120 | } = require("@embroider/compat");
121 |
122 | module.exports = async function(defaults) {
123 | const {
124 | buildOnce
125 | } = await import("@embroider/vite");
126 |
127 | return compatBuild(app, buildOnce, {
128 | staticEmberSource: true,
129 | staticAddonTrees: true,
130 | staticAddonTestSupportTrees: true
131 | });
132 | };"
133 | `);
134 | });
135 | });
136 |
--------------------------------------------------------------------------------
/lib/tasks/ensure-v2-addons.test.js:
--------------------------------------------------------------------------------
1 | import { describe, it, beforeEach, expect, vi } from 'vitest';
2 |
3 | import ensureV2Addons from './ensure-v2-addons';
4 | import { ExitError } from '../utils/exit.js';
5 |
6 | let files;
7 |
8 | vi.mock('node:fs/promises', () => {
9 | return {
10 | readFile: (filename) => {
11 | return files[filename];
12 | },
13 | };
14 | });
15 |
16 | const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined);
17 |
18 | const consoleWarn = vi
19 | .spyOn(console, 'warn')
20 | .mockImplementation(() => undefined);
21 |
22 | describe('ensureV2Addons() function', () => {
23 | beforeEach(() => {
24 | files = {};
25 | consoleLog.mockClear();
26 | consoleWarn.mockClear();
27 | });
28 |
29 | it('wont error with an empty package.json', async () => {
30 | files = {
31 | 'package.json': '{}',
32 | };
33 |
34 | await ensureV2Addons();
35 | });
36 |
37 | it('shows no error when you have a dependency that does not use ember-addon meta', async () => {
38 | files = {
39 | 'package.json': JSON.stringify({
40 | dependencies: {
41 | '@babel/eslint-parser': '^7.26.10',
42 | },
43 | }),
44 | };
45 |
46 | await ensureV2Addons();
47 | });
48 |
49 | it('shows no error when you have a dependency that was upgraded to v2', async () => {
50 | files = {
51 | 'package.json': JSON.stringify({
52 | dependencies: {
53 | 'ember-welcome-page': '^6.0.0',
54 | },
55 | }),
56 | };
57 |
58 | await ensureV2Addons();
59 | });
60 |
61 | it('notifies and stops when you have a dependency that was not yet upgraded to v2', async () => {
62 | files = {
63 | 'package.json': JSON.stringify({
64 | devDependencies: {
65 | 'ember-welcome-page': '^5.0.0',
66 | },
67 | }),
68 | };
69 |
70 | await expect(() => ensureV2Addons()).rejects.toThrowError(ExitError);
71 | expect(consoleLog).toBeCalledTimes(1);
72 | expect(consoleLog).toBeCalledWith(
73 | expect.stringMatching(
74 | /addon\(s\) whose latest version is now a .*v2 addon.*\..*ember-welcome-page/s,
75 | ),
76 | );
77 | });
78 |
79 | it('notifies and continues when you have a dependency that is still a v1 addon', async () => {
80 | /*
81 | * ember-test-selectors is a relatively safe case to use because it will remain a v1 addon.
82 | * The way to move to "v2" is to configure strip-test-selectors plugins in the Babel config
83 | * directly. This test may have to change if we want to figure out a way to notify users
84 | * about this, but that could be an overkill.
85 | */
86 | files = {
87 | 'package.json': JSON.stringify({
88 | devDependencies: {
89 | 'ember-test-selectors': '^7.1.0',
90 | },
91 | }),
92 | };
93 |
94 | await ensureV2Addons();
95 |
96 | expect(consoleLog).toBeCalledTimes(1);
97 | expect(consoleLog).toBeCalledWith(
98 | expect.stringMatching(
99 | /addon\(s\) which are .*v1 only.*and cannot be updated to v2 format\..*ember-test-selectors/s,
100 | ),
101 | );
102 | });
103 |
104 | it('notifies and continues when you have a package that can not be looked up publically', async () => {
105 | files = {
106 | 'package.json': JSON.stringify({
107 | devDependencies: {
108 | '@mainmatter/super-private-does-really-exist-i-promise': '^5.0.0',
109 | },
110 | }),
111 | };
112 |
113 | await ensureV2Addons();
114 |
115 | expect(consoleWarn).toBeCalledTimes(1);
116 | expect(consoleWarn).toBeCalledWith(
117 | 'Could not look up information about "@mainmatter/super-private-does-really-exist-i-promise". You need to verify if this addon is a v2 addon manually.',
118 | );
119 | });
120 |
121 | it('looks into local package.json first', async () => {
122 | files = {
123 | 'package.json': JSON.stringify({
124 | devDependencies: {
125 | '@mainmatter/super-private-does-really-exist-i-promise': '^5.0.0',
126 | },
127 | }),
128 | 'node_modules/@mainmatter/super-private-does-really-exist-i-promise/package.json':
129 | JSON.stringify({
130 | 'ember-addon': {
131 | configPath: 'tests/dummy/config',
132 | },
133 | }),
134 | };
135 |
136 | await ensureV2Addons();
137 |
138 | expect(consoleLog).toBeCalledTimes(1);
139 | expect(consoleLog).toBeCalledWith(
140 | expect.stringMatching(
141 | /Is this package private\?.*@mainmatter\/super-private-does-really-exist-i-promise/s,
142 | ),
143 | );
144 | });
145 | });
146 |
--------------------------------------------------------------------------------
/lib/tasks/update-package-json.js:
--------------------------------------------------------------------------------
1 | import { readFile, writeFile } from 'node:fs/promises';
2 | import semver from 'semver';
3 | import { resolveVersion } from '../utils/resolve-version.js';
4 |
5 | /*
6 | * These packages are used in the latest classic app blueprint,
7 | * but they are no longer used in the Vite app blueprint so the
8 | * codemod should remove them.
9 | */
10 | export const PACKAGES_TO_REMOVE = [
11 | '@embroider/webpack',
12 | 'broccoli-asset-rev',
13 | 'ember-cli-app-version',
14 | 'ember-cli-clean-css',
15 | 'ember-cli-dependency-checker',
16 | 'ember-cli-inject-live-reload',
17 | 'ember-cli-sri',
18 | 'ember-cli-terser',
19 | 'ember-template-imports',
20 | 'loader.js',
21 | 'webpack',
22 | ];
23 |
24 | /*
25 | * These packages are new requirements for the Vite app.
26 | * The version is the minimum required version. Depending
27 | * on the context, the codemod can: install the package,
28 | * change its version to the minimum required version, or
29 | * keep it as it is.
30 | */
31 | export const PACKAGES_TO_ADD = [
32 | ['@babel/plugin-transform-runtime', '^7.26.9'],
33 | ['@ember/string', '^4.0.0'],
34 | ['@ember/test-helpers', '^4.0.0'],
35 | ['@embroider/compat', '^4.0.3'],
36 | ['@embroider/config-meta-loader', '^1.0.0'],
37 | ['@embroider/core', '^4.0.3'],
38 | ['@embroider/vite', '^1.1.1'],
39 | ['@rollup/plugin-babel', '^6.0.4'],
40 | ['babel-plugin-ember-template-compilation', '^2.3.0'],
41 | ['decorator-transforms', '^2.3.0'],
42 | ['ember-load-initializers', '^3.0.0'],
43 | ['ember-resolver', '^13.0.0'],
44 | ['ember-qunit', '^9.0.0'],
45 | ['vite', '^6.0.0'],
46 | ];
47 |
48 | export const PACKAGES_TO_ADD_TS = [
49 | ['@babel/plugin-transform-typescript', '^7.28.0'],
50 | ];
51 |
52 | /*
53 | * These packages are not requirements for the Vite app.
54 | * However, if they are used, then they have a minimum
55 | * required version, and the codemod will make sure it's
56 | * installed.
57 | */
58 | export const PACKAGES_TO_UPDATE = [['@embroider/router', '^3.0.1']];
59 |
60 | export default async function updatePackageJson(options = { ts: false }) {
61 | const packageJSON = JSON.parse(await readFile('package.json', 'utf-8'));
62 |
63 | removePackages(packageJSON, options);
64 | addPackages(packageJSON, options);
65 | updatePackages(packageJSON, options);
66 |
67 | // Add app v2 meta
68 | packageJSON['ember-addon'] = {
69 | type: 'app',
70 | version: 2,
71 | };
72 | packageJSON.exports = {
73 | './tests/*': './tests/*',
74 | './*': './app/*',
75 | };
76 |
77 | // Update commands
78 | packageJSON.scripts = {
79 | ...packageJSON.scripts,
80 | build: 'vite build',
81 | start: 'vite',
82 | 'test:ember': 'vite build --mode development && ember test --path dist',
83 | };
84 |
85 | await writeFile(
86 | 'package.json',
87 | JSON.stringify(packageJSON, undefined, 2),
88 | 'utf-8',
89 | );
90 | }
91 |
92 | function removePackages(packageJSON) {
93 | console.log('Removing dependencies that are no longer used.');
94 | if (packageJSON['devDependencies']) {
95 | packageJSON['devDependencies'] = Object.fromEntries(
96 | Object.entries(packageJSON['devDependencies']).filter(
97 | ([dep]) => !PACKAGES_TO_REMOVE.includes(dep),
98 | ),
99 | );
100 | }
101 | if (packageJSON['dependencies']) {
102 | packageJSON['dependencies'] = Object.fromEntries(
103 | Object.entries(packageJSON['dependencies']).filter(
104 | ([dep]) => !PACKAGES_TO_REMOVE.includes(dep),
105 | ),
106 | );
107 | }
108 | }
109 |
110 | function addPackages(packageJSON, options = { ts: false }) {
111 | console.log('Adding new required dependencies.');
112 |
113 | const packagesToAdd = [
114 | ...PACKAGES_TO_ADD,
115 | ...(options.ts ? PACKAGES_TO_ADD_TS : []),
116 | ];
117 | let hasNewDevDep = false;
118 |
119 | for (const [dep, version] of packagesToAdd) {
120 | let isUpdated =
121 | updateVersion(packageJSON['dependencies'], dep, version) ||
122 | updateVersion(packageJSON['devDependencies'], dep, version);
123 | if (!isUpdated) {
124 | packageJSON['devDependencies'] = packageJSON['devDependencies'] ?? {};
125 | addNewDependency(packageJSON['devDependencies'], dep, version);
126 | hasNewDevDep = true;
127 | }
128 | }
129 | if (hasNewDevDep) {
130 | packageJSON['devDependencies'] = sortDependencies(
131 | packageJSON['devDependencies'],
132 | );
133 | }
134 | }
135 |
136 | function updatePackages(packageJSON) {
137 | console.log('Updating optional dependencies.');
138 | for (const [dep, version] of PACKAGES_TO_UPDATE) {
139 | updateVersion(packageJSON['dependencies'], dep, version) ||
140 | updateVersion(packageJSON['devDependencies'], dep, version);
141 | }
142 | }
143 |
144 | function updateVersion(deps, depToAdd, minimumVersion) {
145 | if (!deps || !depToAdd) return;
146 | if (deps[depToAdd]) {
147 | const version = resolveVersion(depToAdd);
148 | if (
149 | semver.lt(
150 | semver.coerce(version, { includePrerelease: true }),
151 | semver.coerce(minimumVersion, { includePrerelease: true }),
152 | )
153 | ) {
154 | deps[depToAdd] = minimumVersion;
155 | }
156 | return true;
157 | }
158 | return false;
159 | }
160 |
161 | function addNewDependency(deps, depToAdd, minimumVersion) {
162 | if (!deps || !depToAdd) return;
163 | deps[depToAdd] = minimumVersion;
164 | }
165 |
166 | function sortDependencies(field) {
167 | return Object.keys(field)
168 | .sort()
169 | .reduce((Obj, key) => {
170 | Obj[key] = field[key];
171 | return Obj;
172 | }, {});
173 | }
174 |
--------------------------------------------------------------------------------
/tests/test-helpers.js:
--------------------------------------------------------------------------------
1 | import { execaNode, execa } from 'execa';
2 | import { dirname, join } from 'path';
3 | import { packageUp } from 'package-up';
4 | import fixturify from 'fixturify';
5 | import stripAnsi from 'strip-ansi';
6 | import tmp from 'tmp';
7 | import { readFileSync, writeFileSync } from 'fs';
8 |
9 | export async function getCliPath(version) {
10 | const path = dirname(require.resolve(version));
11 | return join(dirname(await packageUp({ cwd: path })), 'bin', 'ember');
12 | }
13 |
14 | export async function generateEmberApp(
15 | tmpDir,
16 | version,
17 | packages,
18 | cliPath,
19 | cliOptions,
20 | ) {
21 | console.log(`🤖 generating ember app for version ${version} 🐹`);
22 | const cwd = join(tmpDir, 'test-app');
23 |
24 | await execaNode({
25 | cwd: tmpDir,
26 | })`${cliPath} new test-app ${cliOptions}`;
27 | await execa({ cwd })`pnpm i`;
28 | if (packages?.length) {
29 | await execa({ cwd })`pnpm i --save-dev ${packages}`;
30 | }
31 |
32 | const fixture = fixturify.readSync('./tests/fixtures');
33 | fixturify.writeSync(cwd, fixture);
34 | }
35 |
36 | export async function testEmber(cwd, expect, testemPort) {
37 | console.log('🤖 testing ember app 🐹');
38 | await execa({
39 | cwd,
40 | env: {
41 | FORCE_BUILD_TESTS: true,
42 | EMBER_CLI_TEST_COMMAND: true, // this forces ember-cli to build test config
43 | },
44 | stdio: 'inherit',
45 | })`npm run build`;
46 |
47 | let { stdout: stdoutProduction } = await execa({
48 | cwd,
49 | })`npm run test:ember -- --test-port=${testemPort} --path dist`;
50 | console.log(stdoutProduction);
51 |
52 | expect(stdoutProduction).to.include('# fail 0');
53 | let { stdout } = await execa({
54 | cwd,
55 | })`npm run test:ember -- --test-port=${testemPort}`;
56 | console.log(stdout);
57 |
58 | expect(stdout).to.include('# fail 0');
59 | }
60 |
61 | export async function applyPatches(cwd, patchedDependencies) {
62 | const packagePath = join(cwd, 'package.json');
63 | const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
64 |
65 | pkg.pnpm = {
66 | patchedDependencies,
67 | };
68 |
69 | writeFileSync(packagePath, JSON.stringify(pkg, null, 2));
70 | }
71 |
72 | export async function runCodemod(cwd, codemodOptions) {
73 | console.log('🤖 running ember-vite-codemod 🐹');
74 | // ember-fetch is part of the classic app blueprint, but
75 | // removing it is a prerequisite to running the codemod.
76 | await execa({ cwd })`pnpm uninstall ember-fetch`;
77 |
78 | const updateScriptPath = join(__dirname, '../index.js');
79 | await execaNode({
80 | cwd,
81 | stdio: 'inherit',
82 | })`${updateScriptPath} ${codemodOptions}`;
83 |
84 | /**
85 | * If you want to apply patches to Embroider this is a good place to do it, You can do it with the following kind of snippet:
86 | *
87 | * ```js
88 | * await applyPatches(cwd, {
89 | * '@embroider/compat': 'patches/@embroider__compat.patch',
90 | * });
91 | * ```
92 | */
93 |
94 | await execa({ cwd, stdio: 'inherit' })`pnpm i --no-frozen-lockfile`;
95 | }
96 |
97 | export async function testWithTestem(cwd, expect, testemPort) {
98 | console.log('🤖 running dev tests with testem 🐹');
99 | await execa({ cwd })`pnpm i --save-dev testem http-proxy`;
100 |
101 | const viteExecaProcess = execa({
102 | cwd,
103 | })`pnpm vite --force --clearScreen false`;
104 | viteExecaProcess.stdout.setEncoding('utf8');
105 |
106 | const HOST = await new Promise((resolve) => {
107 | viteExecaProcess.stdout.on('data', (chunk) => {
108 | const matches = /Local:\s+(https?:\/\/.*)\//g.exec(stripAnsi(chunk));
109 |
110 | if (matches) {
111 | resolve(matches[1]);
112 | }
113 | });
114 | });
115 |
116 | let result = await execa({
117 | cwd,
118 | env: {
119 | HOST,
120 | },
121 | })`pnpm testem --port ${testemPort} --file testem-dev.js ci`;
122 |
123 | expect(result.exitCode, result.output).to.equal(0);
124 | console.log(result.stdout);
125 | }
126 |
127 | export const testVersions = [
128 | [
129 | 'ember-cli-3.28',
130 | [
131 | 'ember-data@^5.3.0',
132 | 'ember-inflector',
133 | 'ember-cli@~4.12.0',
134 | 'ember-auto-import@^2.0.0', // ember 3.28 came with ember-auto-import@1 which is too old
135 | 'webpack@^5.0.0', // ember-auto-import@2 needs webpack
136 | 'ember-welcome-page@^7.0.2', // we're importing from welcome page for the production test and we need the import location to be stable
137 | ],
138 | ],
139 | [
140 | 'ember-cli-4.4',
141 | ['ember-data@^5.3.0', 'ember-inflector', 'ember-cli@~4.12.0'],
142 | ],
143 | [
144 | 'ember-cli-4.8',
145 | ['ember-data@^5.3.0', 'ember-inflector', 'ember-cli@~4.12.0'], // ember-cli 4.12 is the earliest version that will work
146 | ],
147 | ['ember-cli-4.12', ['ember-data@^5.3.0', 'ember-inflector']], // ember-cli 5.3 is currently the earliest version that can support Vite, and needs ember-inflector installed
148 | // // test helpers seems to be broken for most ember versions 😭
149 | ['ember-cli-5.4', ['@ember/test-helpers@latest']],
150 | ['ember-cli-5.8', ['@ember/test-helpers@latest']],
151 | ['ember-cli-5.12', ['@ember/test-helpers@latest']],
152 | ['ember-cli-6.4'],
153 | ['ember-cli-latest'],
154 | ];
155 |
156 | let port = 7357;
157 | export function getPort() {
158 | return port++;
159 | }
160 |
161 | export async function executeTest(
162 | expect,
163 | version,
164 | packages,
165 | cliOptions,
166 | testemPort,
167 | codemodOptions,
168 | ) {
169 | let tmpobj = tmp.dirSync({ unsafeCleanup: true });
170 | const cwd = join(tmpobj.name, 'test-app');
171 | const cliPath = await getCliPath(version);
172 |
173 | await generateEmberApp(tmpobj.name, version, packages, cliPath, cliOptions);
174 | await testEmber(cwd, expect, testemPort);
175 | await runCodemod(cwd, codemodOptions);
176 | await testEmber(cwd, expect, testemPort);
177 | await testWithTestem(cwd, expect, testemPort);
178 | if (codemodOptions.includes('--ts')) {
179 | console.log('running lint:types');
180 | let result = await execa({ cwd })`pnpm lint:types`;
181 | expect(result.exitCode, result.output).to.equal(0);
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/lib/tasks/ensure-v2-addons.js:
--------------------------------------------------------------------------------
1 | import { readFile } from 'node:fs/promises';
2 | import { join } from 'node:path';
3 | import { execa } from 'execa';
4 | import {
5 | PACKAGES_TO_ADD,
6 | PACKAGES_TO_ADD_TS,
7 | PACKAGES_TO_REMOVE,
8 | } from './update-package-json.js';
9 | import { ExitError } from '../utils/exit.js';
10 | import { bold, cyan } from 'yoctocolors';
11 |
12 | // These addons are v1, but we know for sure Embroider can deal with it,
13 | // because they are part of the "Vite app" blueprint or the Embroider ecosystem.
14 | const v1CompatibleAddons = [
15 | '@ember/optional-features',
16 | '@embroider/macros',
17 | '@embroider/util',
18 | '@glimmer/tracking',
19 | 'ember-auto-import',
20 | 'ember-cli-babel',
21 | 'ember-cli-deprecation-workflow',
22 | 'ember-cli-htmlbars',
23 | 'ember-template-imports',
24 | // ember-source latest is v2 but Vite support is available back to 3.28,
25 | // so it's not a problem if the currently installed version is v1.
26 | // The task ensure-ember-cli has a dedicated pre-check for the version.
27 | 'ember-source',
28 | ];
29 |
30 | export default async function ensureV2Addons(options = { ts: false }) {
31 | let shouldProcessExit = false;
32 | const v1Addons = await getV1Addons(options);
33 | const packagesToAdd = [
34 | ...PACKAGES_TO_ADD,
35 | ...(options.ts ? PACKAGES_TO_ADD_TS : []),
36 | ];
37 |
38 | const logs = {
39 | hasV2: [],
40 | doesNotHaveV2: [],
41 | hasBasicPackage: [],
42 | isPrivate: [],
43 | };
44 |
45 | for (let addon of v1Addons) {
46 | if (
47 | PACKAGES_TO_REMOVE.includes(addon) ||
48 | v1CompatibleAddons.includes(addon) ||
49 | packagesToAdd.some(([p]) => p === addon)
50 | ) {
51 | // don't report:
52 | // - v1 addons the codemod will remove from package.json
53 | // - v1 addons that are part of the default "Vite app" blueprint
54 | continue;
55 | }
56 |
57 | try {
58 | const { stdout } = await execa`npm view ${addon} ember-addon`;
59 | if (stdout) {
60 | // viewing ember-addon outputs something so it's still an ember-addon
61 | if (stdout.includes('version: 2')) {
62 | logs.hasV2.push(addon);
63 | shouldProcessExit = true;
64 | } else {
65 | logs.doesNotHaveV2.push(addon);
66 | }
67 | } else {
68 | // viewing ember-addon doesn't output anything so it's now a basic npm package
69 | logs.hasBasicPackage.push(addon);
70 | shouldProcessExit = true;
71 | }
72 | } catch (e) {
73 | logs.isPrivate.push(addon);
74 | if (options.errorTrace) {
75 | console.error(e.message ?? e);
76 | }
77 | }
78 | }
79 |
80 | printSummary(logs);
81 | if (shouldProcessExit) {
82 | throw new ExitError(
83 | `If you prefer to move to Vite now, and upgrade the addons above at a later time, pass the option ${bold('--skip-v2-addon')} when running this codemod.`,
84 | );
85 | }
86 | }
87 |
88 | async function getV1Addons({ errorTrace = false } = {}) {
89 | const packageJSON = JSON.parse(await readFile('package.json', 'utf8'));
90 | const deps = { ...packageJSON.devDependencies, ...packageJSON.dependencies };
91 | const v1packages = [];
92 | for (const [depName, version] of Object.entries(deps)) {
93 | let pkg;
94 | let pkgInfo;
95 | try {
96 | pkg = JSON.parse(
97 | await readFile(join('node_modules', depName, 'package.json'), 'utf8'),
98 | );
99 | if (pkg) {
100 | pkgInfo = pkg['ember-addon'];
101 | }
102 | } catch (e) {
103 | if (errorTrace) {
104 | console.warn(
105 | `The package "${depName}" was found in the dependencies, but its package.json could not be read. Falling back to npm public information.`,
106 | );
107 | console.error(e.message ?? e);
108 | }
109 | }
110 |
111 | if (!pkg) {
112 | try {
113 | const { stdout } =
114 | await execa`npm view --json ${depName}@${version} ember-addon`;
115 | if (stdout) {
116 | pkgInfo = JSON.parse(stdout);
117 | if (pkgInfo.length) {
118 | // npm view will return all results matching the version based on semver (e.g ^6.0.0)
119 | pkgInfo = pkgInfo[pkgInfo.length - 1];
120 | }
121 | }
122 | } catch (e) {
123 | console.warn(
124 | `Could not look up information about "${depName}". You need to verify if this addon is a v2 addon manually.`,
125 | );
126 | if (errorTrace) {
127 | console.error(e.message ?? e);
128 | }
129 | }
130 | }
131 |
132 | if (pkgInfo && pkgInfo.version !== 2) {
133 | v1packages.push(depName);
134 | }
135 | }
136 | return v1packages;
137 | }
138 |
139 | function printSummary(logs) {
140 | const { hasBasicPackage, hasV2, doesNotHaveV2, isPrivate } = logs;
141 | let summary = bold(`📝 Your Ember V1 addons summary\n`);
142 |
143 | if (
144 | !hasBasicPackage.length &&
145 | !hasV2.length &&
146 | !doesNotHaveV2.length &&
147 | !isPrivate.length
148 | ) {
149 | summary += 'Nothing to report.';
150 | console.log(summary);
151 | return;
152 | }
153 | summary += `Embroider generally auto-fixes v1 addons so they keep working with Vite. However:
154 | * v2 addons are more performant, so we highly recommend to update everything that can be updated now.
155 | * it might happen for some v1 addons that the auto-fix does not work; if you notice an issue when running your Vite app, you can refer to the list below to potentially spot an incompatible addon.\n\n`;
156 |
157 | if (hasBasicPackage.length) {
158 | summary += `${cyan(bold(hasBasicPackage.length))} addon(s) whose latest version is now a ${cyan(bold('basic npm package'))} (it's no longer an ember-addon). Update whenever you can:\n`;
159 | summary += hasBasicPackage.map((name) => ` * ${name}`).join('\n');
160 | summary += '\n\n';
161 | }
162 | if (hasV2.length) {
163 | summary += `${cyan(bold(hasV2.length))} addon(s) whose latest version is now a ${cyan(bold('v2 addon'))}. Update whenever you can:\n`;
164 | summary += hasV2.map((name) => ` * ${name}`).join('\n');
165 | summary += '\n\n';
166 | }
167 | if (doesNotHaveV2.length) {
168 | summary += `${cyan(bold(doesNotHaveV2.length))} addon(s) which are ${cyan(bold('v1 only'))} and cannot be updated to v2 format. If you notice an issue, consider replacing this dependency, or contributing to the addon to make it v2:\n`;
169 | summary += doesNotHaveV2.map((name) => ` * ${name}`).join('\n');
170 | summary += '\n\n';
171 | }
172 | if (isPrivate.length) {
173 | summary += `${cyan(bold(isPrivate.length))} addon(s) identified as a v1 addon, but it was ${cyan(bold('not possible to check'))} if a v2 format was released. Is this package private?\n`;
174 | summary += isPrivate.map((name) => ` * ${name}`).join('\n');
175 | summary += '\n\n';
176 | }
177 | console.log(summary);
178 | }
179 |
--------------------------------------------------------------------------------
/lib/transforms/ember-cli-build.js:
--------------------------------------------------------------------------------
1 | import { types, visit } from 'recast';
2 |
3 | const b = types.builders;
4 |
5 | export default async function transformEmberCliBuild(ast, isEmbroiderWebpack) {
6 | if (isEmbroiderWebpack) {
7 | ast = removeWebpackImport(ast);
8 | }
9 |
10 | ast = addEmbroiderCompatImport(ast);
11 |
12 | visit(ast, {
13 | visitAssignmentExpression(path) {
14 | if (
15 | path.value.left.type === 'MemberExpression' &&
16 | path.value.left.object.type === 'Identifier' &&
17 | path.value.left.object.name === 'module' &&
18 | path.value.left.property.type === 'Identifier' &&
19 | path.value.left.property.name === 'exports'
20 | ) {
21 | // module.exports function is async
22 | path.value.right.async = true;
23 |
24 | // Add "const { buildOnce } = await import('@embroider/vite');"
25 | const statements = path.value.right.body.body;
26 | let propBuildOnce = statements.find((statement) => {
27 | return (
28 | statement.type === 'VariableDeclaration' &&
29 | statement.declarations[0].id.type === 'ObjectPattern' &&
30 | statement.declarations[0].id.properties[0].key.type ===
31 | 'Identifier' &&
32 | statement.declarations[0].id.properties[0].key.name === 'buildOnce'
33 | );
34 | });
35 | if (!propBuildOnce) {
36 | propBuildOnce = b.objectProperty(
37 | b.identifier('buildOnce'),
38 | b.identifier('buildOnce'),
39 | );
40 | propBuildOnce.shorthand = true;
41 | path.value.right.body.body.unshift(
42 | b.variableDeclaration('const', [
43 | b.variableDeclarator(
44 | b.objectPattern([propBuildOnce]),
45 | b.awaitExpression(
46 | b.callExpression(b.import(), [b.literal('@embroider/vite')]),
47 | ),
48 | ),
49 | ]),
50 | );
51 | }
52 | }
53 |
54 | // return compatBuild(app, buildOnce);
55 | this.traverse(path, {
56 | visitReturnStatement(path) {
57 | let optionsNode;
58 | // Retrieve options to pass them to the new return statement
59 | if (
60 | isEmbroiderWebpack &&
61 | path.value.argument.type === 'CallExpression' &&
62 | path.value.argument.arguments.length === 3
63 | ) {
64 | optionsNode = removeSkipBabelQunit(
65 | path.value.argument.arguments[2],
66 | );
67 | }
68 | path.value.argument = b.callExpression(b.identifier('compatBuild'), [
69 | b.identifier('app'),
70 | b.identifier('buildOnce'),
71 | ]);
72 | if (optionsNode) {
73 | path.value.argument.arguments.push(optionsNode);
74 | }
75 | return false;
76 | },
77 | });
78 | },
79 | });
80 |
81 | return ast;
82 | }
83 |
84 | // Add "const { compatBuild } = require('@embroider/compat');"
85 | function addEmbroiderCompatImport(ast) {
86 | let propCompatBuild = ast.program.body.find((node) => {
87 | return (
88 | node.type === 'VariableDeclaration' &&
89 | node.declarations[0].id.type === 'ObjectPattern' &&
90 | node.declarations[0].id.properties[0].key.type === 'Identifier' &&
91 | node.declarations[0].id.properties[0].key.name === 'compatBuild'
92 | );
93 | });
94 | if (!propCompatBuild) {
95 | let lastDeclarationIndex = ast.program.body.findLastIndex(
96 | (node) => node.type === 'VariableDeclaration',
97 | );
98 | lastDeclarationIndex = Math.max(lastDeclarationIndex, 0);
99 |
100 | propCompatBuild = b.objectProperty(
101 | b.identifier('compatBuild'),
102 | b.identifier('compatBuild'),
103 | );
104 | propCompatBuild.shorthand = true;
105 | const requireCompat = b.variableDeclaration('const', [
106 | b.variableDeclarator(
107 | b.objectPattern([propCompatBuild]),
108 | b.callExpression(b.identifier('require'), [
109 | b.literal('@embroider/compat'),
110 | ]),
111 | ),
112 | ]);
113 | ast.program.body.splice(lastDeclarationIndex + 1, 0, requireCompat);
114 | }
115 | return ast;
116 | }
117 |
118 | // Remove "const { Webpack } = require('@embroider/webpack');"
119 | // Only for apps building with @embroider/webpack
120 | function removeWebpackImport(ast) {
121 | visit(ast, {
122 | visitVariableDeclaration(path) {
123 | if (
124 | path.value.declarations[0].init.type === 'CallExpression' &&
125 | path.value.declarations[0].init.arguments.length === 1 &&
126 | path.value.declarations[0].init.arguments[0].type === 'StringLiteral' &&
127 | path.value.declarations[0].init.arguments[0].value ===
128 | '@embroider/webpack'
129 | ) {
130 | // Expect parent.path to be a "body" array
131 | let body = path.parentPath;
132 | if (body.value.length) {
133 | let propWebpackIndex = body.value.findIndex(
134 | (node) => node === path.value,
135 | );
136 | body.value.splice(propWebpackIndex, 1);
137 | }
138 | }
139 | return false;
140 | },
141 | });
142 | return ast;
143 | }
144 |
145 | /*
146 | * The transform generally preserves build options when moving from Webpack to Vite,
147 | * but we do one exception for skipBabel on qunit package. This option is present in
148 | * the default --embroider app blueprint but needs to be removed. Additionally, this
149 | * codemod updates ember-qunit to a minimum required version that no longer need the
150 | * skip Babel on qunit.
151 | * Any other package in skipBabel is preserved, so it's up to the developer to ignore
152 | * them it using the new Babel config after the codemod has run.
153 | */
154 | function removeSkipBabelQunit(optionsNode) {
155 | const { properties } = optionsNode;
156 | const skipBabelOptionIndex = properties.findIndex((propertyNode) => {
157 | return (
158 | propertyNode.key.type === 'Identifier' &&
159 | propertyNode.key.name === 'skipBabel'
160 | );
161 | });
162 | const skipBabelOption = properties[skipBabelOptionIndex];
163 |
164 | if (skipBabelOption) {
165 | const { elements } = skipBabelOption.value;
166 | let qunitIndex = elements.findIndex((elm) => {
167 | return (
168 | elm.type === 'ObjectExpression' &&
169 | elm.properties.some((skipProperty) => {
170 | return (
171 | skipProperty.key.name === 'package' &&
172 | skipProperty.value.type === 'StringLiteral' &&
173 | skipProperty.value.value === 'qunit'
174 | );
175 | })
176 | );
177 | });
178 |
179 | if (qunitIndex >= 0) {
180 | elements.splice(qunitIndex, 1);
181 | if (elements.length === 0) {
182 | properties.splice(skipBabelOptionIndex, 1);
183 | }
184 | }
185 | }
186 | return optionsNode;
187 | }
188 |
--------------------------------------------------------------------------------
/lib/tasks/ensure-no-unsupported-deps.test.js:
--------------------------------------------------------------------------------
1 | import { describe, it, beforeEach, expect, vi } from 'vitest';
2 |
3 | import ensureNoUnsupportedDeps, {
4 | ensureKnownAddons,
5 | ensureEmberCli,
6 | } from './ensure-no-unsupported-deps';
7 |
8 | let files;
9 | let mockedVersions = {};
10 |
11 | vi.mock('../utils/resolve-version.js', () => {
12 | return {
13 | resolveVersion: function (packageName) {
14 | return mockedVersions[packageName];
15 | },
16 | };
17 | });
18 |
19 | vi.mock('node:fs/promises', () => {
20 | return {
21 | readFile: (filename) => {
22 | return files[filename];
23 | },
24 | };
25 | });
26 |
27 | const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined);
28 |
29 | vi.spyOn(process, 'exit');
30 |
31 | describe('ensureNoUnsupportedDeps() function', () => {
32 | beforeEach(() => {
33 | consoleLog.mockClear();
34 | });
35 |
36 | it('does not error when all dependencies are supported', async () => {
37 | files = {
38 | 'package.json': JSON.stringify({
39 | dependencies: {
40 | supportedDep: '3.0.0',
41 | },
42 | devDependencies: {
43 | supportedDevDep: '3.0.0',
44 | 'ember-source': '~3.28.0',
45 | 'ember-cli': '~4.12.0',
46 | },
47 | }),
48 | };
49 | mockedVersions = {
50 | 'ember-source': '3.28.0',
51 | 'ember-cli': '4.12.0',
52 | };
53 |
54 | await ensureNoUnsupportedDeps();
55 | });
56 |
57 | it('throws when unsupported dependencies for Ember and addons', async () => {
58 | files = {
59 | 'package.json': JSON.stringify({
60 | devDependencies: {
61 | 'ember-source': '~3.28.0',
62 | 'ember-cli': '~3.28.0',
63 | 'ember-fetch': '3.0.0',
64 | },
65 | }),
66 | };
67 | mockedVersions = {
68 | 'ember-source': '3.28.0',
69 | 'ember-cli': '3.28.0',
70 | };
71 |
72 | await expect(() => ensureNoUnsupportedDeps()).rejects.toThrowError(
73 | `Detected unsupported dependencies:
74 |
75 | * ember-cli 3.28.0 (< 4.12) was detected. Vite support requires at least ember-cli 4.12. You can update ember-cli independently of ember-source, Vite support is available from ember-source 3.28 onwards.
76 |
77 | * Your app contains a dependency to ember-fetch. ember-fetch behaves a way that is incompatible with modern JavaScript tooling, including building with Vite.
78 | Please remove ember-fetch dependency then run this codemod again. Check out https://rfcs.emberjs.com/id/1065-remove-ember-fetch to see recommended alternatives.`,
79 | );
80 | });
81 | });
82 |
83 | describe('ensureKnownAddons() function', () => {
84 | it('does not error when all dependencies are supported', async () => {
85 | files = {
86 | 'package.json': JSON.stringify({
87 | dependencies: {
88 | supportedDep: '3.0.0',
89 | },
90 | devDependencies: {
91 | supportedDevDep: '3.0.0',
92 | },
93 | }),
94 | };
95 | let output = await ensureKnownAddons();
96 | expect(output).toMatchInlineSnapshot(`[]`);
97 | });
98 |
99 | it('detects unsupported dependency', async () => {
100 | files = {
101 | 'package.json': JSON.stringify({
102 | dependencies: {
103 | 'ember-fetch': '3.0.0',
104 | },
105 | }),
106 | };
107 | let output = await ensureKnownAddons();
108 | expect(output).toMatchInlineSnapshot(`
109 | [
110 | "Your app contains a dependency to ember-fetch. ember-fetch behaves a way that is incompatible with modern JavaScript tooling, including building with Vite.
111 | Please remove ember-fetch dependency then run this codemod again. Check out https://rfcs.emberjs.com/id/1065-remove-ember-fetch to see recommended alternatives.",
112 | ]
113 | `);
114 | });
115 |
116 | it('detects unsupported dev dependency', async () => {
117 | files = {
118 | 'package.json': JSON.stringify({
119 | devDependencies: {
120 | 'ember-cli-mirage': '3.0.0',
121 | },
122 | }),
123 | };
124 | let output = await ensureKnownAddons();
125 | expect(output).toMatchInlineSnapshot(`
126 | [
127 | "Your app contains a dependency to ember-cli-mirage. ember-cli-mirage doesn't work correctly with Vite.
128 | Please move from ember-cli-mirage to ember-mirage then run this codemod again. Checkout https://github.com/bgantzler/ember-mirage/blob/main/docs/migration.md for guidance.",
129 | ]
130 | `);
131 | });
132 |
133 | it('returns several messages at once', async () => {
134 | files = {
135 | 'package.json': JSON.stringify({
136 | devDependencies: {
137 | 'ember-css-modules': '3.0.0',
138 | 'ember-cli-mirage': '3.0.0',
139 | },
140 | }),
141 | };
142 | let output = await ensureKnownAddons();
143 | expect(output).toMatchInlineSnapshot(`
144 | [
145 | "Your app contains a dependency to ember-cli-mirage. ember-cli-mirage doesn't work correctly with Vite.
146 | Please move from ember-cli-mirage to ember-mirage then run this codemod again. Checkout https://github.com/bgantzler/ember-mirage/blob/main/docs/migration.md for guidance.",
147 | "Your app contains a dependency to ember-css-modules. ember-css-modules behavior is incompatible with Vite, you should migrate to a different solution to manage your CSS modules.
148 | There is a recommended migration path that you can follow for a file by file migration to ember-scoped-css, which is compatible with Vite. Checkout https://github.com/BlueCutOfficial/css-modules-to-scoped-css",
149 | ]
150 | `);
151 | });
152 | });
153 |
154 | describe('ensureEmberCli() function', () => {
155 | beforeEach(() => {
156 | files = {};
157 | mockedVersions = {};
158 | });
159 |
160 | it('detects if ember-source < 3.28', async () => {
161 | files = {
162 | 'package.json': JSON.stringify({
163 | devDependencies: {
164 | 'ember-source': '~3.8.0',
165 | 'ember-cli': '~3.8.0',
166 | },
167 | }),
168 | };
169 | mockedVersions = {
170 | 'ember-source': '3.8.0',
171 | 'ember-cli': '3.8.0',
172 | };
173 |
174 | let output = await ensureEmberCli();
175 | expect(output).toMatchInlineSnapshot(`
176 | [
177 | "ember-source 3.8.0 (< 3.28) was detected. Vite support is available from Ember 3.28 onwards.",
178 | "ember-cli 3.8.0 (< 4.12) was detected. Vite support requires at least ember-cli 4.12. You can update ember-cli independently of ember-source, Vite support is available from ember-source 3.28 onwards.",
179 | ]
180 | `);
181 | });
182 |
183 | it('detects if ember-cli < 4.12', async () => {
184 | files = {
185 | 'package.json': JSON.stringify({
186 | devDependencies: {
187 | 'ember-source': '~3.28.0',
188 | 'ember-cli': '~3.28.0',
189 | },
190 | }),
191 | };
192 | mockedVersions = {
193 | 'ember-source': '3.28.0',
194 | 'ember-cli': '3.28.0',
195 | };
196 |
197 | let output = await ensureEmberCli();
198 | expect(output).toMatchInlineSnapshot(`
199 | [
200 | "ember-cli 3.28.0 (< 4.12) was detected. Vite support requires at least ember-cli 4.12. You can update ember-cli independently of ember-source, Vite support is available from ember-source 3.28 onwards.",
201 | ]
202 | `);
203 | });
204 |
205 | it('ends without error when required versions are installed', async () => {
206 | files = {
207 | 'package.json': JSON.stringify({
208 | devDependencies: {
209 | 'ember-source': '~3.28.0',
210 | 'ember-cli': '~4.12.0',
211 | },
212 | }),
213 | };
214 | mockedVersions = {
215 | 'ember-source': '3.28.0',
216 | 'ember-cli': '4.12.0',
217 | };
218 | let output = await ensureEmberCli();
219 | expect(output).toMatchInlineSnapshot(`[]`);
220 | });
221 | });
222 |
--------------------------------------------------------------------------------
/lib/tasks/update-package-json.test.js:
--------------------------------------------------------------------------------
1 | import { describe, it, beforeEach, expect, vi } from 'vitest';
2 |
3 | import updatePackageJson from './update-package-json';
4 |
5 | let files;
6 |
7 | vi.mock('node:fs/promises', () => {
8 | return {
9 | readFile: (filename) => {
10 | return files[filename];
11 | },
12 | writeFile: (filename, content) => {
13 | files[filename] = JSON.parse(content);
14 | },
15 | };
16 | });
17 |
18 | vi.mock('../utils/resolve-version.js', () => {
19 | return {
20 | resolveVersion: function (packageName) {
21 | switch (packageName) {
22 | case '@ember/string':
23 | return '3.0.0';
24 | case 'vite':
25 | return '100.0.0';
26 | case '@embroider/compat':
27 | return '4.0.0';
28 | case '@embroider/core':
29 | return '100.0.0';
30 | case '@embroider/router':
31 | return '2.1.8';
32 | default:
33 | return undefined;
34 | }
35 | },
36 | };
37 | });
38 |
39 | describe('updatePackageJson() function', () => {
40 | beforeEach(() => {
41 | files = {};
42 | });
43 |
44 | it('does not error with an empty package.json', async () => {
45 | files = {
46 | 'package.json': '{}',
47 | };
48 | await updatePackageJson();
49 | });
50 |
51 | it('updates the scripts', async () => {
52 | files = {
53 | 'package.json': JSON.stringify({
54 | scripts: {
55 | build: 'ember build --environment=production',
56 | lint: 'concurrently "pnpm:lint:*(!fix)" --names "lint:" --prefixColors auto',
57 | 'lint:hbs': 'ember-template-lint .',
58 | 'lint:js': 'eslint . --cache',
59 | start: 'ember serve',
60 | test: 'concurrently "pnpm:lint" "pnpm:test:*" --names "lint,test:" --prefixColors auto',
61 | 'test:ember': 'ember test',
62 | },
63 | }),
64 | };
65 | await updatePackageJson();
66 | expect(files['package.json'].scripts).toMatchInlineSnapshot(`
67 | {
68 | "build": "vite build",
69 | "lint": "concurrently "pnpm:lint:*(!fix)" --names "lint:" --prefixColors auto",
70 | "lint:hbs": "ember-template-lint .",
71 | "lint:js": "eslint . --cache",
72 | "start": "vite",
73 | "test": "concurrently "pnpm:lint" "pnpm:test:*" --names "lint,test:" --prefixColors auto",
74 | "test:ember": "vite build --mode development && ember test --path dist",
75 | }
76 | `);
77 | });
78 |
79 | it('adds the expected meta', async () => {
80 | files = {
81 | 'package.json': '{}',
82 | };
83 | await updatePackageJson();
84 | expect(files['package.json']['ember-addon']).toMatchInlineSnapshot(`
85 | {
86 | "type": "app",
87 | "version": 2,
88 | }
89 | `);
90 | expect(files['package.json']['exports']).toMatchInlineSnapshot(`
91 | {
92 | "./*": "./app/*",
93 | "./tests/*": "./tests/*",
94 | }
95 | `);
96 | });
97 |
98 | it('adds missing dependencies in devDependencies', async () => {
99 | files = {
100 | 'package.json': JSON.stringify({
101 | devDependencies: {},
102 | }),
103 | };
104 |
105 | await updatePackageJson();
106 | expect(files['package.json']['devDependencies']).toMatchInlineSnapshot(`
107 | {
108 | "@babel/plugin-transform-runtime": "^7.26.9",
109 | "@ember/string": "^4.0.0",
110 | "@ember/test-helpers": "^4.0.0",
111 | "@embroider/compat": "^4.0.3",
112 | "@embroider/config-meta-loader": "^1.0.0",
113 | "@embroider/core": "^4.0.3",
114 | "@embroider/vite": "^1.1.1",
115 | "@rollup/plugin-babel": "^6.0.4",
116 | "babel-plugin-ember-template-compilation": "^2.3.0",
117 | "decorator-transforms": "^2.3.0",
118 | "ember-load-initializers": "^3.0.0",
119 | "ember-qunit": "^9.0.0",
120 | "ember-resolver": "^13.0.0",
121 | "vite": "^6.0.0",
122 | }
123 | `);
124 | });
125 |
126 | it('adds missing dependencies in devDependencies for --ts', async () => {
127 | files = {
128 | 'package.json': JSON.stringify({
129 | devDependencies: {},
130 | }),
131 | };
132 |
133 | await updatePackageJson({ ts: true });
134 | expect(files['package.json']['devDependencies']).toMatchInlineSnapshot(`
135 | {
136 | "@babel/plugin-transform-runtime": "^7.26.9",
137 | "@babel/plugin-transform-typescript": "^7.28.0",
138 | "@ember/string": "^4.0.0",
139 | "@ember/test-helpers": "^4.0.0",
140 | "@embroider/compat": "^4.0.3",
141 | "@embroider/config-meta-loader": "^1.0.0",
142 | "@embroider/core": "^4.0.3",
143 | "@embroider/vite": "^1.1.1",
144 | "@rollup/plugin-babel": "^6.0.4",
145 | "babel-plugin-ember-template-compilation": "^2.3.0",
146 | "decorator-transforms": "^2.3.0",
147 | "ember-load-initializers": "^3.0.0",
148 | "ember-qunit": "^9.0.0",
149 | "ember-resolver": "^13.0.0",
150 | "vite": "^6.0.0",
151 | }
152 | `);
153 | });
154 |
155 | it('removes dependencies', async () => {
156 | files = {
157 | 'package.json': JSON.stringify({
158 | dependencies: {
159 | 'loader.js': 'x.x.x',
160 | webpack: 'x.x.x',
161 | },
162 | devDependencies: {
163 | '@embroider/webpack': 'x.x.x',
164 | 'broccoli-asset-rev': 'x.x.x',
165 | },
166 | }),
167 | };
168 | await updatePackageJson();
169 | const pck = files['package.json'];
170 | expect(pck['dependencies']).toStrictEqual({});
171 | expect(pck['devDependencies']).not.toHaveProperty('@embroider/webpack');
172 | expect(pck['devDependencies']).not.toHaveProperty('broccoli-asset-rev');
173 | expect(pck['devDependencies']).toHaveProperty('vite');
174 | });
175 |
176 | // Note: the snapshot approach also asserts the absence of duplicated deps between dependencies and devDependencies
177 | it('updates dependencies to the minimum required version (packagesToAdd)', async () => {
178 | files = {
179 | 'package.json': JSON.stringify({
180 | dependencies: {
181 | '@ember/string': '^3.0.0',
182 | vite: '100.0.0', // make sure the test always go through a case where the current version is bigger
183 | },
184 | devDependencies: {
185 | '@embroider/compat': '^4.0.0',
186 | '@embroider/core': '100.0.0', // make sure the test always go through a case where the current version is bigger
187 | },
188 | }),
189 | };
190 | await updatePackageJson();
191 | expect(files['package.json']['dependencies']).toMatchInlineSnapshot(`
192 | {
193 | "@ember/string": "^4.0.0",
194 | "vite": "100.0.0",
195 | }
196 | `);
197 | expect(files['package.json']['devDependencies']).toMatchInlineSnapshot(`
198 | {
199 | "@babel/plugin-transform-runtime": "^7.26.9",
200 | "@ember/test-helpers": "^4.0.0",
201 | "@embroider/compat": "^4.0.3",
202 | "@embroider/config-meta-loader": "^1.0.0",
203 | "@embroider/core": "100.0.0",
204 | "@embroider/vite": "^1.1.1",
205 | "@rollup/plugin-babel": "^6.0.4",
206 | "babel-plugin-ember-template-compilation": "^2.3.0",
207 | "decorator-transforms": "^2.3.0",
208 | "ember-load-initializers": "^3.0.0",
209 | "ember-qunit": "^9.0.0",
210 | "ember-resolver": "^13.0.0",
211 | }
212 | `);
213 | });
214 |
215 | it('updates dependencies to the minimum required version (packagesToUpdate)', async () => {
216 | files = {
217 | 'package.json': JSON.stringify({
218 | devDependencies: {
219 | '@embroider/router': '2.1.8',
220 | },
221 | }),
222 | };
223 | await updatePackageJson();
224 | let pck = files['package.json'];
225 | expect(pck['devDependencies']).toHaveProperty(
226 | '@embroider/router',
227 | '^3.0.1',
228 | );
229 |
230 | files = {
231 | 'package.json': JSON.stringify({
232 | dependencies: {
233 | '@embroider/router': '2.1.8',
234 | },
235 | }),
236 | };
237 | await updatePackageJson();
238 | pck = files['package.json'];
239 | expect(pck['dependencies']).toHaveProperty('@embroider/router', '^3.0.1');
240 | });
241 | });
242 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## Release (2025-11-29)
4 |
5 | ember-vite-codemod 1.4.0 (minor)
6 |
7 | #### :rocket: Enhancement
8 | * `ember-vite-codemod`
9 | * [#124](https://github.com/mainmatter/ember-vite-codemod/pull/124) Merge `ensure-ember-cli` and `ensure-no-unsupported-deps` ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
10 | * [#126](https://github.com/mainmatter/ember-vite-codemod/pull/126) Reduce the size of the output for ensure-v2-addons ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
11 |
12 | #### :bug: Bug Fix
13 | * `ember-vite-codemod`
14 | * [#125](https://github.com/mainmatter/ember-vite-codemod/pull/125) Add `ember-cli-deprecation-workflow` to the list of v1 compatible addons ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
15 |
16 | #### Committers: 1
17 | - Marine Dunstetter ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
18 |
19 | ## Release (2025-11-07)
20 |
21 | ember-vite-codemod 1.3.0 (minor)
22 |
23 | #### :rocket: Enhancement
24 | * `ember-vite-codemod`
25 | * [#112](https://github.com/mainmatter/ember-vite-codemod/pull/112) Print all no unsupported deps ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
26 |
27 | #### :bug: Bug Fix
28 | * `ember-vite-codemod`
29 | * [#119](https://github.com/mainmatter/ember-vite-codemod/pull/119) Update test:ember script to match default blueprint ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
30 |
31 | #### :memo: Documentation
32 | * `ember-vite-codemod`
33 | * [#110](https://github.com/mainmatter/ember-vite-codemod/pull/110) Docs/ Add a section for ember-exam in the README ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
34 |
35 | #### :house: Internal
36 | * `ember-vite-codemod`
37 | * [#109](https://github.com/mainmatter/ember-vite-codemod/pull/109) Add a unit test for add-missing-files ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
38 |
39 | #### Committers: 2
40 | - Marine Dunstetter ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
41 | - [@NullVoxPopuli](https://github.com/NullVoxPopuli)
42 |
43 | ## Release (2025-10-30)
44 |
45 | ember-vite-codemod 1.2.0 (minor)
46 |
47 | #### :rocket: Enhancement
48 | * `ember-vite-codemod`
49 | * [#115](https://github.com/mainmatter/ember-vite-codemod/pull/115) Resolve actual dependency versions ([@ef4](https://github.com/ef4))
50 |
51 | #### :memo: Documentation
52 | * `ember-vite-codemod`
53 | * [#116](https://github.com/mainmatter/ember-vite-codemod/pull/116) Fix formatting in README for Embroider and Webpack ([@ef4](https://github.com/ef4))
54 |
55 | #### Committers: 1
56 | - Edward Faulkner ([@ef4](https://github.com/ef4))
57 |
58 | ## Release (2025-10-10)
59 |
60 | ember-vite-codemod 1.1.0 (minor)
61 |
62 | #### :rocket: Enhancement
63 | * `ember-vite-codemod`
64 | * [#106](https://github.com/mainmatter/ember-vite-codemod/pull/106) make sure we install the .env.development file from the blueprint ([@mansona](https://github.com/mansona))
65 |
66 | #### :bug: Bug Fix
67 | * `ember-vite-codemod`
68 | * [#67](https://github.com/mainmatter/ember-vite-codemod/pull/67) add some windows tests ([@mansona](https://github.com/mansona))
69 |
70 | #### :house: Internal
71 | * `ember-vite-codemod`
72 | * [#104](https://github.com/mainmatter/ember-vite-codemod/pull/104) Remove workaround for missing @glimmer/component import ([@pichfl](https://github.com/pichfl))
73 | * [#103](https://github.com/mainmatter/ember-vite-codemod/pull/103) Unskip TypeScript test runs ([@pichfl](https://github.com/pichfl))
74 |
75 | #### Committers: 2
76 | - Chris Manson ([@mansona](https://github.com/mansona))
77 | - Florian Pichler ([@pichfl](https://github.com/pichfl))
78 |
79 | ## Release (2025-10-06)
80 |
81 | ember-vite-codemod 1.0.0 (major)
82 |
83 | #### :boom: Breaking Change
84 | * `ember-vite-codemod`
85 | * [#96](https://github.com/mainmatter/ember-vite-codemod/pull/96) start using the @ember/app-blueprint ([@mansona](https://github.com/mansona))
86 |
87 | #### Committers: 1
88 | - Chris Manson ([@mansona](https://github.com/mansona))
89 |
90 | ## Release (2025-09-24)
91 |
92 | ember-vite-codemod 0.17.0 (minor)
93 |
94 | #### :rocket: Enhancement
95 | * `ember-vite-codemod`
96 | * [#100](https://github.com/mainmatter/ember-vite-codemod/pull/100) Improve console output ([@pichfl](https://github.com/pichfl))
97 | * [#98](https://github.com/mainmatter/ember-vite-codemod/pull/98) Default to TypeScript if detected within the project ([@pichfl](https://github.com/pichfl))
98 |
99 | #### :bug: Bug Fix
100 | * `ember-vite-codemod`
101 | * [#101](https://github.com/mainmatter/ember-vite-codemod/pull/101) Add `@babel/plugin-transform-typescript` if `--ts` is enabled ([@pichfl](https://github.com/pichfl))
102 |
103 | #### :memo: Documentation
104 | * `ember-vite-codemod`
105 | * [#93](https://github.com/mainmatter/ember-vite-codemod/pull/93) Docs/ more details about the common issues that our out of scope ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
106 |
107 | #### :house: Internal
108 | * `ember-vite-codemod`
109 | * [#95](https://github.com/mainmatter/ember-vite-codemod/pull/95) docs: issue template ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
110 |
111 | #### Committers: 2
112 | - Florian Pichler ([@pichfl](https://github.com/pichfl))
113 | - Marine Dunstetter ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
114 |
115 | ## Release (2025-05-09)
116 |
117 | ember-vite-codemod 0.16.0 (minor)
118 |
119 | #### :rocket: Enhancement
120 | * `ember-vite-codemod`
121 | * [#91](https://github.com/mainmatter/ember-vite-codemod/pull/91) update embroider dependencies to point at stable releases ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
122 |
123 | #### Committers: 1
124 | - [@NullVoxPopuli](https://github.com/NullVoxPopuli)
125 |
126 | ## Release (2025-04-24)
127 |
128 | ember-vite-codemod 0.15.0 (minor)
129 |
130 | #### :rocket: Enhancement
131 | * `ember-vite-codemod`
132 | * [#89](https://github.com/mainmatter/ember-vite-codemod/pull/89) Detect `@embroider/webpack` automatically ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
133 | * [#88](https://github.com/mainmatter/ember-vite-codemod/pull/88) Don't report `ember-source` is available as v2 ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
134 | * [#85](https://github.com/mainmatter/ember-vite-codemod/pull/85) Pre-check Ember version before running transforms ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
135 | * [#83](https://github.com/mainmatter/ember-vite-codemod/pull/83) Required minimum version for optional packages ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
136 |
137 | #### Committers: 1
138 | - Marine Dunstetter ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
139 |
140 | ## Release (2025-04-11)
141 |
142 | ember-vite-codemod 0.14.0 (minor)
143 |
144 | #### :rocket: Enhancement
145 | * `ember-vite-codemod`
146 | * [#80](https://github.com/mainmatter/ember-vite-codemod/pull/80) Import missing `@embroider/core/types/virtual` when using --ts ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
147 |
148 | #### Committers: 1
149 | - Marine Dunstetter ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
150 |
151 | ## Release (2025-03-25)
152 |
153 | ember-vite-codemod 0.13.0 (minor)
154 |
155 | #### :rocket: Enhancement
156 | * `ember-vite-codemod`
157 | * [#74](https://github.com/mainmatter/ember-vite-codemod/pull/74) Fix/ in ensure-v2-addons, look for local package.json first ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
158 |
159 | #### Committers: 1
160 | - Marine Dunstetter ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
161 |
162 | ## Release (2025-03-21)
163 |
164 | ember-vite-codemod 0.12.0 (minor)
165 |
166 | #### :rocket: Enhancement
167 | * `ember-vite-codemod`
168 | * [#70](https://github.com/mainmatter/ember-vite-codemod/pull/70) Feat/ Add ember-css-modules to unsupported deps ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
169 |
170 | #### :bug: Bug Fix
171 | * `ember-vite-codemod`
172 | * [#69](https://github.com/mainmatter/ember-vite-codemod/pull/69) Fix/ dependencies duplication ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
173 |
174 | #### Committers: 1
175 | - Marine Dunstetter ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
176 |
177 | ## Release (2025-03-20)
178 |
179 | ember-vite-codemod 0.11.0 (minor)
180 |
181 | #### :rocket: Enhancement
182 | * `ember-vite-codemod`
183 | * [#68](https://github.com/mainmatter/ember-vite-codemod/pull/68) bump minimum version of @embroider/compat ([@mansona](https://github.com/mansona))
184 | * [#60](https://github.com/mainmatter/ember-vite-codemod/pull/60) Enable ember 3.28 ([@mansona](https://github.com/mansona))
185 | * [#56](https://github.com/mainmatter/ember-vite-codemod/pull/56) enable testing for 4.4 ([@mansona](https://github.com/mansona))
186 |
187 | #### Committers: 1
188 | - Chris Manson ([@mansona](https://github.com/mansona))
189 |
190 | ## Release (2025-03-18)
191 |
192 | ember-vite-codemod 0.10.0 (minor)
193 |
194 | #### :rocket: Enhancement
195 | * `ember-vite-codemod`
196 | * [#63](https://github.com/mainmatter/ember-vite-codemod/pull/63) Remove `@embroider/webpack` + don't report `@embroider/macros` and `@embroider/util` ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
197 | * [#54](https://github.com/mainmatter/ember-vite-codemod/pull/54) enable testing for 4.8 ([@mansona](https://github.com/mansona))
198 | * [#53](https://github.com/mainmatter/ember-vite-codemod/pull/53) enable testing for ember 4.12 ([@mansona](https://github.com/mansona))
199 |
200 | #### :bug: Bug Fix
201 | * `ember-vite-codemod`
202 | * [#52](https://github.com/mainmatter/ember-vite-codemod/pull/52) Throw error when modulePrefix does not match package.json#name ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
203 |
204 | #### :house: Internal
205 | * `ember-vite-codemod`
206 | * [#59](https://github.com/mainmatter/ember-vite-codemod/pull/59) split CI by ember version ([@mansona](https://github.com/mansona))
207 | * [#57](https://github.com/mainmatter/ember-vite-codemod/pull/57) add a --environment=production test ([@mansona](https://github.com/mansona))
208 |
209 | #### Committers: 3
210 | - Chris Manson ([@mansona](https://github.com/mansona))
211 | - Marine Dunstetter ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
212 | - [@NullVoxPopuli](https://github.com/NullVoxPopuli)
213 |
214 | ## Release (2025-03-14)
215 |
216 | ember-vite-codemod 0.9.0 (minor)
217 |
218 | #### :rocket: Enhancement
219 | * `ember-vite-codemod`
220 | * [#24](https://github.com/mainmatter/ember-vite-codemod/pull/24) enable tests for Ember 5.4 ([@mansona](https://github.com/mansona))
221 |
222 | #### Committers: 1
223 | - Chris Manson ([@mansona](https://github.com/mansona))
224 |
225 | ## Release (2025-03-14)
226 |
227 | ember-vite-codemod 0.8.0 (minor)
228 |
229 | #### :rocket: Enhancement
230 | * `ember-vite-codemod`
231 | * [#15](https://github.com/mainmatter/ember-vite-codemod/pull/15) bump minimum embroider versions and enable testing for ember-5.8 ([@mansona](https://github.com/mansona))
232 |
233 | #### :house: Internal
234 | * `ember-vite-codemod`
235 | * [#51](https://github.com/mainmatter/ember-vite-codemod/pull/51) add fail-fast: false to CI matrix ([@mansona](https://github.com/mansona))
236 | * [#49](https://github.com/mainmatter/ember-vite-codemod/pull/49) split out webpack and typescript tests properly ([@mansona](https://github.com/mansona))
237 |
238 | #### Committers: 1
239 | - Chris Manson ([@mansona](https://github.com/mansona))
240 |
241 | ## Release (2025-03-13)
242 |
243 | ember-vite-codemod 0.7.0 (minor)
244 |
245 | #### :rocket: Enhancement
246 | * `ember-vite-codemod`
247 | * [#47](https://github.com/mainmatter/ember-vite-codemod/pull/47) Add `ember-template-imports` to packagesToRemove ([@johanrd](https://github.com/johanrd))
248 |
249 | #### Committers: 1
250 | - [@johanrd](https://github.com/johanrd)
251 |
252 | ## Release (2025-03-12)
253 |
254 | ember-vite-codemod 0.6.0 (minor)
255 |
256 | #### :rocket: Enhancement
257 | * `ember-vite-codemod`
258 | * [#37](https://github.com/mainmatter/ember-vite-codemod/pull/37) Support transforming files with any extension ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
259 |
260 | #### Committers: 1
261 | - [@NullVoxPopuli](https://github.com/NullVoxPopuli)
262 |
263 | ## Release (2025-03-11)
264 |
265 | ember-vite-codemod 0.5.0 (minor)
266 |
267 | #### :rocket: Enhancement
268 | * `ember-vite-codemod`
269 | * [#39](https://github.com/mainmatter/ember-vite-codemod/pull/39) make sure the codemod doesn't exit if it comes across private packages ([@mansona](https://github.com/mansona))
270 |
271 | #### Committers: 1
272 | - Chris Manson ([@mansona](https://github.com/mansona))
273 |
274 | ## Release (2025-03-10)
275 |
276 | ember-vite-codemod 0.4.1 (patch)
277 |
278 | #### :bug: Bug Fix
279 | * `ember-vite-codemod`
280 | * [#35](https://github.com/mainmatter/ember-vite-codemod/pull/35) move recast to a dependency ([@mansona](https://github.com/mansona))
281 |
282 | #### Committers: 1
283 | - Chris Manson ([@mansona](https://github.com/mansona))
284 |
285 | ## Release (2025-03-07)
286 |
287 | ember-vite-codemod 0.4.0 (minor)
288 |
289 | #### :rocket: Enhancement
290 | * `ember-vite-codemod`
291 | * [#27](https://github.com/mainmatter/ember-vite-codemod/pull/27) Support apps building with @embroider/webpack ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
292 |
293 | #### :house: Internal
294 | * `ember-vite-codemod`
295 | * [#33](https://github.com/mainmatter/ember-vite-codemod/pull/33) Remove devDep portfinder ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
296 | * [#31](https://github.com/mainmatter/ember-vite-codemod/pull/31) start testing vite dev mode with testem ([@mansona](https://github.com/mansona))
297 | * [#30](https://github.com/mainmatter/ember-vite-codemod/pull/30) refactor tests to be easier to follow ([@mansona](https://github.com/mansona))
298 |
299 | #### Committers: 2
300 | - Chris Manson ([@mansona](https://github.com/mansona))
301 | - Marine Dunstetter ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
302 |
303 | ## Release (2025-03-05)
304 |
305 | ember-vite-codemod 0.3.0 (minor)
306 |
307 | #### :rocket: Enhancement
308 | * `ember-vite-codemod`
309 | * [#26](https://github.com/mainmatter/ember-vite-codemod/pull/26) Add ember-composable-helpers and ember-cli-mirage to unsupported deps ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
310 |
311 | #### Committers: 1
312 | - Marine Dunstetter ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
313 |
314 | ## Release (2025-02-27)
315 |
316 | ember-vite-codemod 0.2.0 (minor)
317 |
318 | #### :rocket: Enhancement
319 | * `ember-vite-codemod`
320 | * [#21](https://github.com/mainmatter/ember-vite-codemod/pull/21) Support Ember 5.12 (by updating packages) ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
321 |
322 | #### :house: Internal
323 | * `ember-vite-codemod`
324 | * [#19](https://github.com/mainmatter/ember-vite-codemod/pull/19) Enable testing for Ember 5.12 ([@mansona](https://github.com/mansona))
325 | * [#22](https://github.com/mainmatter/ember-vite-codemod/pull/22) ignore changelog for linting ([@mansona](https://github.com/mansona))
326 |
327 | #### Committers: 2
328 | - Chris Manson ([@mansona](https://github.com/mansona))
329 | - Marine Dunstetter ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
330 |
331 | ## Release (2025-02-27)
332 |
333 | ember-vite-codemod 0.1.0 (minor)
334 |
335 | #### :rocket: Enhancement
336 | * `ember-vite-codemod`
337 | * [#17](https://github.com/mainmatter/ember-vite-codemod/pull/17) Add `/tmp/` at to the top of .gitignore if not found ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
338 | * [#18](https://github.com/mainmatter/ember-vite-codemod/pull/18) Iterate on ensure-v2-addon task ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
339 | * [#16](https://github.com/mainmatter/ember-vite-codemod/pull/16) Check if the repository is clean ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
340 | * [#10](https://github.com/mainmatter/ember-vite-codemod/pull/10) Implement each stage of the codemod ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
341 | * [#2](https://github.com/mainmatter/ember-vite-codemod/pull/2) Initial basic implementation - adding missing files ([@mansona](https://github.com/mansona))
342 |
343 | #### :memo: Documentation
344 | * `ember-vite-codemod`
345 | * [#14](https://github.com/mainmatter/ember-vite-codemod/pull/14) Add a README.md ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
346 |
347 | #### :house: Internal
348 | * `ember-vite-codemod`
349 | * [#11](https://github.com/mainmatter/ember-vite-codemod/pull/11) add a basic CI ([@mansona](https://github.com/mansona))
350 | * [#12](https://github.com/mainmatter/ember-vite-codemod/pull/12) set up release-plan ([@mansona](https://github.com/mansona))
351 |
352 | #### Committers: 2
353 | - Chris Manson ([@mansona](https://github.com/mansona))
354 | - Marine Dunstetter ([@BlueCutOfficial](https://github.com/BlueCutOfficial))
355 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ember-vite-codemod
2 |
3 | Migrate your Ember app to build with Vite.
4 |
5 | - [Promise and compatibility](#promise-and-compatibility)
6 | - [Usage](#usage)
7 | - [Steps](#steps)
8 | - [Common out of scope changes](#common-out-of-scope-changes)
9 | - [Build options](#build-options)
10 | - [Linter](#linter)
11 | - [ember-exam](#ember-exam)
12 |
13 | ## Promise and compatibility
14 |
15 | This codemod does the minimum amount of changes in your Ember app to make it build with Vite. Its behavior is aligned with the app blueprint: it means that if you generate a new Classic Ember app with the app blueprint then run the codmod on it, the app will build correctly with Vite. This line we draw corresponds to what's tested by this codemod's CI:
16 |
17 | - Classic Ember apps: the codemod can be run on Ember apps >= 3.28
18 |
19 | - Embroider + Webpack apps: the codemod can be run on Ember apps >= 3.28
20 |
21 | - Apps using TypeScript: the codemod can be run on Ember apps >= 5.4 (It doesn't mean you can't have a TypeScript app building with Vite prior to 5.4, but the codemod currently doesn't do all the job to support this.)
22 |
23 | Depending on your app customization, the codemod may not be designed to perform all the steps to get your specific app building correctly with Vite, **[there could be additional steps](#common-out-of-scope-changes)** that you still need to manage yourself. You can use the present document to have a better overwiew of what the codemod is expected to do for you.
24 |
25 | ## Usage
26 |
27 | This codemod will add, move and modify files in your Ember app to make it build with Vite. Make sure your git repository is clean before running this command so you can easily compare the changes.
28 |
29 | In your Ember app folder, execute:
30 |
31 | ```
32 | npx ember-vite-codemod@latest [options]
33 | ```
34 |
35 | ### options
36 |
37 | | Option | Default | Description |
38 | | :-------------- | :-----: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
39 | | --skip-git | false | By default, the process exits if the git repository is not clean. Use this option to execute the command anyway at your own risk. |
40 | | --skip-v2-addon | false | By default, the process exits if it detects v1 addons you could update or remove before switching to Vite. Use this option to execute the rest of the codemod anyway and discover if Embroider can deal with your v1 addons without issue. |
41 | | --ts | false | Use this option to indicate your app uses typescript. It will impact what files the codemod creates and the packages it installs. |
42 | | --error-trace | false | In case of error, use this option to print the full error trace when it's available. |
43 |
44 | ## Steps
45 |
46 | The codemod executes a sequence of several ordered tasks. At the end of the process, reinstall your dependencies; if your app follows the standards the codemod expects, it should now builds correctly using Vite. If something went wrong, the sections below detail what the codemod expectations are and what it tries to output so you can figure out how to complete the migration manually.
47 |
48 | ### Checking for unsupported dependencies
49 |
50 | This is a verification task, it doesn't change your code.
51 |
52 | The codemod will first check there are no known dependency that is incompatible with the way Vite works. e.g. `ember-fetch` will not work with Vite and needs to be removed.
53 |
54 | ### Checking addons are v2
55 |
56 | This is a verification task, it doesn't change your code.
57 |
58 | The codemod will look at all your Ember dependencies and will advise you for updates.
59 |
60 | When you use Embroider in an app that depends on v1 addons, Embroider will try to auto-fix the v1 addons in a way that makes them compatible with Vite. This approach works for a number of known addons, but we cannot guarantee it will work for any v1 addon you use. The best way to avoid issues is that your classic app already relies only on v2 addons, and the codemod will guide you in that direction:
61 |
62 | - If one of your addons is v1 but the latest version on npm is v2 or has become a basic package, it's recommended to update.
63 | - If one of your addons is v1, no v2 format is available, and we don't know for sure if Embroider can rewrite it, the codemod will display a warning so you pay attention to this addon's behavior when building with Vite.
64 | - If one of your v1 addons comes from Ember app blueprint and is no longer used in Embroider+Vite world, the codemod won't notify you about it (because it will be removed in a later step).
65 |
66 | ### Creating new required files
67 |
68 | From this step, the codemod is done with verifications and starts modifying your code.
69 |
70 | First, it creates new files that are now required for Embroider+Vite. These files are copied direclty from the [embroider-build/app-blueprint](https://github.com/embroider-build/app-blueprint), so you can refer to this repository to see the expected content:
71 |
72 | - [app/config/environment.js](https://github.com/embroider-build/app-blueprint/blob/main/files-override/shared/app/config/environment.js)
73 | - [vite.config.mjs](https://github.com/embroider-build/app-blueprint/blob/main/files-override/shared/vite.config.mjs)
74 | - [babel.config.cjs](https://github.com/embroider-build/app-blueprint/blob/main/files/js/babel.config.cjs)
75 |
76 | ### Moving `index.html` to the root
77 |
78 | Vite expects the `index.html` to be at the root of the app. The codemod will move your existing `app/index.html` to the root rather than creating a new file, this way it keeps all the customizations you added.
79 |
80 | ### Running code replacements on... `index.html`
81 |
82 | When running the Vite dev server, the files `vendor.js` and `vendor.css` are no longer physical files, it's Embroider that generates their content and returns it to Vite. To let Vite identify these _virtual_ files, the URL is changed as follow:
83 |
84 | ```diff
85 | -
86 | -
87 | +
88 | +
89 |
90 | -
91 | +
92 | ```
93 |
94 | Additionaly, we no longer import an asset `my-classic-app.js`. Instead, the script that boots the app is defined directly inline as a module. If you use any v1 addon implementing a content-for "app-boot" and you want to keep its behavior, this is where the implementation should go. The default content is the following:
95 |
96 | ```diff
97 | -
98 | +
104 | ```
105 |
106 | ### Running code replacements on... `tests/index.html`
107 |
108 | The changes in `tests/index.html` follow the same principle as for `index.html`. Additionally, we remove `{{content-for "test-body-footer"}}` because it checks tests are loaded at a time they are not.
109 |
110 | ```diff
111 | -
112 | -
113 | -
114 | +
115 | +
116 | +
117 |
118 | -
119 | -
120 | +
121 | +
122 |
123 | -
124 | -
125 | +
130 |
131 | - {{content-for "test-body-footer"}}
132 | ```
133 |
134 | ### Running code replacements on... `app/config/environment.js`
135 |
136 | Since the file `app/config/environment.js` is created out of the app blueprint, it has a placeholder `<%= name %>` for your app name. Replace it with the correct value. The name to use can be read in `ENV.modulePrefix` in your `config/environment.js`.
137 |
138 | ### Running code replacements on... `ember-cli-build.js`
139 |
140 | #### From a classic Ember app
141 |
142 | Instead of building the app directly with Broccoli, we use `@embroider/compat`, a module that essentially serves as a bridge between classic apps and modern Embroider apps. The Broccoli app instance is still there, but it's passed in argument to Embroider.
143 |
144 | Considering an empty 6.2 Ember app, the codemod does the following:
145 |
146 | ```diff
147 | 'use strict';
148 |
149 | const EmberApp = require('ember-cli/lib/broccoli/ember-app');
150 | + const { compatBuild } = require('@embroider/compat');
151 |
152 | module.exports = function (defaults) {
153 | + const { buildOnce } = await import('@embroider/vite');
154 | const app = new EmberApp(defaults, {
155 | // Add options here
156 | });
157 |
158 | - return app.toTree();
159 | + return compatBuild(app, buildOnce);
160 | };
161 | ```
162 |
163 | ⚠️ Adding extra Broccoli trees doesn't make sense in this context. If you used to return `app.toTree(extraTree);`, the corresponding feature will no longer work.
164 |
165 | #### From @embroider/webpack
166 |
167 | `@embroider/compat` will now rely on `@embroider/vite` instead of `@embroider/webpack` to build the app:
168 |
169 | ```diff
170 | 'use strict';
171 |
172 | const EmberApp = require('ember-cli/lib/broccoli/ember-app');
173 | - const { Webpack } = require('@embroider/webpack');
174 | + const { compatBuild } = require('@embroider/compat');
175 |
176 | module.exports = function (defaults) {
177 | + const { buildOnce } = await import('@embroider/vite');
178 | const app = new EmberApp(defaults, {
179 | // Add options here
180 | });
181 |
182 | - return require('@embroider/compat').compatBuild(app, Webpack, {
183 | + return compatBuild(app, buildOnce, {
184 | ...buildOptions,
185 | - skipBabel: [{
186 | - package: 'qunit'
187 | - }]
188 | });
189 | };
190 | ```
191 |
192 | All your build options will be preserved as is (except `skipBabel` for `'qunit'` that is removed), but be aware:
193 |
194 | ⚠️ The codemod will remove your app dependency to Webpack. If you use options that relate specifically to Webpack behavior, `@embroider/vite` won't use them and the corresponding feature won't be supported. Also, note that when using some build options like `skipBabel`, Embroider triggers an informative build error to teach developers how to migrate to the modern system. In other words, just because you have a build error after running the codemod doesn't mean that something went wrong. It can be the expected follow-up step to fully complete your migration.
195 |
196 | ### Running code replacements on... `testem.js`
197 |
198 | The change in this file introduces a new way to prevent its execution when tests run directly in the browser.
199 |
200 | The codemod looks for the `module.exports` and wraps it in a conditional that checks the `module` existence.
201 |
202 | ```diff
203 | 'use strict';
204 |
205 | + if (typeof module !== 'undefined') {
206 | module.exports = { ... }
207 | + }
208 | ```
209 |
210 | ### Running code replacements on... `tests/test-helper.js`
211 |
212 | If you go back to the modifications done in `tests/index.html`, you can see the new script imports ` { start } from './test-helper`, registers the test files then calls `start`. The `start` function is the one the codemod creates in this step.
213 |
214 | Instead of loading the tests then calling `start` from `ember-qunit` directly in the module scope of the file, we instead export a `start` function that can be called later.
215 |
216 | Considering an empty 6.2 Ember app, the codemod does the following:
217 |
218 | ```diff
219 | import Application from 'my-empty-classic-app/app';
220 | import config from 'my-empty-classic-app/config/environment';
221 | import * as QUnit from 'qunit';
222 | import { setApplication } from '@ember/test-helpers';
223 | import { setup } from 'qunit-dom';
224 | - import { loadTests } from 'ember-qunit/test-loader';
225 | - import { start, setupEmberOnerrorValidation } from 'ember-qunit';
226 | + import { start as qunitStart, setupEmberOnerrorValidation } from 'ember-qunit';
227 |
228 | + export function start() {
229 | setApplication(Application.create(config.APP));
230 |
231 | setup(QUnit.assert);
232 | setupEmberOnerrorValidation();
233 | - loadTests();
234 | - start();
235 | + qunitStart();
236 | + }
237 | ```
238 |
239 | ### Running replacements on... `package.json`
240 |
241 | Last but not least, the codemod will modify three sorts of things in the `package.json`:
242 |
243 | - It will replace the build and test commands to use Vite instead of the legacy Ember commands.
244 | - It will add meta that identify your app as a v2 Ember app.
245 | - It will remove and add a bunch of dependencies.
246 |
247 | The codemod looks for the commands `build`, `start`, and `test:ember`, and rewrites them as:
248 |
249 | ```json
250 | "build": "vite build",
251 | "start": "vite",
252 | "test:ember": "vite build --mode development && ember test --path dist"
253 | ```
254 |
255 | It will create the following fields with the following content:
256 |
257 | ```json
258 | "ember-addon": {
259 | "type": "app",
260 | "version": 2
261 | },
262 | "exports": {
263 | "./tests/*": "./tests/*",
264 | "./*": "./app/*"
265 | }
266 | ```
267 |
268 | The list of packages that are removed and added can be found in the codemod source:
269 |
270 | - [Packages the codemod adds](https://github.com/mainmatter/ember-vite-codemod/blob/main/lib/tasks/update-package-json.js#L18).
271 | - [Packages the codemod removes](https://github.com/mainmatter/ember-vite-codemod/blob/main/lib/tasks/update-package-json.js#L6).
272 |
273 | ## Common out of scope changes
274 |
275 | This codemod focuses entirely on the minimum amount of code changes to make your Ember app build with Vite. It generally avoids touching the configuration of your app (even though there are common configuration changes when moving to Vite) because it doesn't want to make a wrong assumption about why you configured things this way.
276 |
277 | ### Build options
278 |
279 | This codemod won't touch your build options. A common change when moving from `@embroider/webpack` to `@embroider/vite` is to remove build options like `staticEmberSource: true` because they are now true by default. However, it's not this codemod's job to tell whether the current options still make sense in the new Vite context; it's Embroider's job, and that's why Embroider warns you at build time if you can remove some of them. It's an expected follow-up action after running the codemod.
280 |
281 | ### Linter
282 |
283 | The codemod won't touch anything about your linter configuration, as the linter doesn't relate to how the app builds. Depending on the plugins you use, you may encounter issues to solve manually. For instance, a common issue is that the codemod adds a dependency to `decorator-transforms` which is used in the new Babel config `babel.config.cjs`. If `'@babel/plugin-proposal-decorators'` was included in your `eslint.config.mjs`, then your linter will throw a parsing error "Cannot use the decorators and decorators-legacy plugin together", so you must adjust the ESLint config:
284 |
285 | ```diff
286 | const parserOptions = {
287 | esm: {
288 | js: {
289 | ecmaFeatures: { modules: true },
290 | ecmaVersion: 'latest',
291 | - requireConfigFile: false,
292 | - babelOptions: {
293 | - plugins: [
294 | - [
295 | - '@babel/plugin-proposal-decorators',
296 | - { decoratorsBeforeExport: true },
297 | - ],
298 | - ],
299 | - },
300 | },
301 | },
302 | }
303 | ```
304 |
305 | ### ember-exam
306 |
307 | If your application relies on ember-exam, then the changes made by this codemod in your test infrastructure are not yet sufficient for everything to work. After you executed the codemod successfully, follow the documentation [How to use with Vite](https://github.com/ember-cli/ember-exam?tab=readme-ov-file#how-to-use-with-vite) in ember-exam README.md.
308 |
--------------------------------------------------------------------------------