diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/package.json index e1cf53b312..565828c9bc 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/package.json +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/package.json @@ -1,7 +1,7 @@ { "name": "migration-from-tsdown", "scripts": { - "build": "tsdown", + "build": "tsdown --public-dir public", "build:watch": "tsdown --watch", "build:dts": "tsdown --dts" }, diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/snapshots/migration_from_tsdown.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/snapshots/migration_from_tsdown.md index 20c439d0cb..cc682f3edb 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/snapshots/migration_from_tsdown.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/snapshots/migration_from_tsdown.md @@ -26,6 +26,9 @@ export default defineConfig({ outDir: 'dist', format: ['esm', 'cjs'], dts: true, + unbundle: true, + copy: 'public', + deps: { resolveDepSubpath: true, onlyBundle: false }, }); ``` @@ -56,7 +59,7 @@ check package.json { "name": "migration-from-tsdown", "scripts": { - "build": "vp pack", + "build": "vp pack --copy public", "build:watch": "vp pack --watch", "build:dts": "vp pack --dts", "prepare": "vp config" @@ -114,6 +117,9 @@ export default defineConfig({ outDir: 'dist', format: ['esm', 'cjs'], dts: true, + unbundle: true, + copy: 'public', + deps: { resolveDepSubpath: true, onlyBundle: false }, }); ``` @@ -144,7 +150,7 @@ check package.json { "name": "migration-from-tsdown", "scripts": { - "build": "vp pack", + "build": "vp pack --copy public", "build:watch": "vp pack --watch", "build:dts": "vp pack --dts", "prepare": "vp config" diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/snapshots/migration_from_tsdown_tsgo.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/snapshots/migration_from_tsdown_tsgo.md index e7efe051de..35dddb6632 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/snapshots/migration_from_tsdown_tsgo.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/snapshots/migration_from_tsdown_tsgo.md @@ -29,6 +29,9 @@ export default defineConfig({ outDir: 'dist', format: ['esm', 'cjs'], dts: { generator: 'tsgo' }, + unbundle: true, + copy: 'public', + deps: { resolveDepSubpath: true, onlyBundle: false }, }); ``` @@ -52,5 +55,8 @@ export default defineConfig({ outDir: 'dist', format: ['esm', 'cjs'], dts: { generator: 'tsgo' }, + unbundle: true, + copy: 'public', + deps: { resolveDepSubpath: true, onlyBundle: false }, }); ``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/tsdown.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/tsdown.config.ts index 5e58c4443e..58c96f6c34 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/tsdown.config.ts +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/tsdown.config.ts @@ -5,4 +5,7 @@ export default defineConfig({ outDir: 'dist', format: ['esm', 'cjs'], dts: true, + bundle: false, + publicDir: 'public', + deps: { onlyAllowBundle: false }, }); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown_json_config/snapshots/migration_from_tsdown_json_config.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown_json_config/snapshots/migration_from_tsdown_json_config.md index 89494ff3d6..95b6526596 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown_json_config/snapshots/migration_from_tsdown_json_config.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown_json_config/snapshots/migration_from_tsdown_json_config.md @@ -9,7 +9,7 @@ VITE+ - The Unified Toolchain for the Web ◇ Migrated . to Vite+ • Node pnpm -• 2 config updates applied +• 2 config updates applied, 1 file had imports rewritten ``` ## `vpt stat-file tsdown.config.json --assert-not file` @@ -35,7 +35,11 @@ export default defineConfig({ "entry": "src/index.ts", "outDir": "dist", "format": ["esm", "cjs"], - "dts": true, + "dts": { generator: 'oxc', }, + unbundle: true, + copy: "public", + css: { inject: true }, + "deps": { resolveDepSubpath: true, onlyBundle: false }, "inputOptions": { "cwd": "./src" } @@ -117,7 +121,11 @@ export default defineConfig({ "entry": "src/index.ts", "outDir": "dist", "format": ["esm", "cjs"], - "dts": true, + "dts": { generator: 'oxc', }, + unbundle: true, + copy: "public", + css: { inject: true }, + "deps": { resolveDepSubpath: true, onlyBundle: false }, "inputOptions": { "cwd": "./src" } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown_json_config/tsdown.config.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown_json_config/tsdown.config.json index ff9c868a87..0c25ce1461 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown_json_config/tsdown.config.json +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown_json_config/tsdown.config.json @@ -2,7 +2,11 @@ "entry": "src/index.ts", "outDir": "dist", "format": ["esm", "cjs"], - "dts": true, + "dts": { "oxc": true, "cjsReexport": false }, + "bundle": false, + "publicDir": "public", + "injectStyle": true, + "deps": { "onlyAllowBundle": false }, "inputOptions": { "cwd": "./src" } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsup/snapshots/migration_from_tsup_success.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsup/snapshots/migration_from_tsup_success.md index 72f95eddaf..6c4af9a5ad 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsup/snapshots/migration_from_tsup_success.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsup/snapshots/migration_from_tsup_success.md @@ -40,7 +40,7 @@ the converted config uses vite-plus pack ``` import { defineConfig } from 'vite-plus/pack'; -export default defineConfig({ +export default defineConfig({ deps: { resolveDepSubpath: true }, entry: ['src/index.ts'], dts: true, format: ['esm', 'cjs'], diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsup_monorepo/snapshots/migration_from_tsup_monorepo_success.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsup_monorepo/snapshots/migration_from_tsup_monorepo_success.md index 5efd70577a..5c40a7ca05 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsup_monorepo/snapshots/migration_from_tsup_monorepo_success.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsup_monorepo/snapshots/migration_from_tsup_monorepo_success.md @@ -32,7 +32,7 @@ package a gets a converted config ``` import { defineConfig } from 'vite-plus/pack'; -export default defineConfig({ +export default defineConfig({ deps: { resolveDepSubpath: true }, entry: ['src/index.ts'], dts: true, format: 'cjs', @@ -74,7 +74,7 @@ package b gets a converted config ``` import { defineConfig } from 'vite-plus/pack'; -export default defineConfig({ +export default defineConfig({ deps: { resolveDepSubpath: true }, entry: ['src/index.ts'], dts: true, format: 'cjs', diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/assets/asset.txt b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/assets/asset.txt new file mode 100644 index 0000000000..73395d2293 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/assets/asset.txt @@ -0,0 +1 @@ +method copy asset diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/concise-entry.txt b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/concise-entry.txt new file mode 100644 index 0000000000..40ac43ceb9 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/concise-entry.txt @@ -0,0 +1 @@ +export { helper } from './helper.js'; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/concise.config.txt b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/concise.config.txt new file mode 100644 index 0000000000..20576b7b9f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/concise.config.txt @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig(() => ({ + entry: 'src/index.ts', + bundle: false, + dts: { oxc: true }, + outExtension() { return { js: '.custom.js' }; }, + publicDir() { return ['assets']; }, +})); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/external-conflicts.config.txt b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/external-conflicts.config.txt new file mode 100644 index 0000000000..b84cb43f00 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/external-conflicts.config.txt @@ -0,0 +1,18 @@ +const externalOptions = ['foo']; + +export default { + pack: [ + { + external: externalOptions, + skipNodeModulesBundle: true, + inputOptions(options) { return { external: options.external }; }, + bundle: false, + dts: { tsgo: true }, + }, + { + external: externalOptions, + deps: { skipNodeModulesBundle: true, dts: { neverBundle: ['types'] } }, + bundle: false, + }, + ], +}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/external-constants.config.txt b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/external-constants.config.txt new file mode 100644 index 0000000000..4dae6220dd --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/external-constants.config.txt @@ -0,0 +1,8 @@ +const externalOptions = ['foo']; + +export default { + pack: [ + { external: externalOptions, skipNodeModulesBundle: true, bundle: false, dts: { tsgo: true } }, + { external: externalOptions, deps: { skipNodeModulesBundle: true }, bundle: false }, + ], +}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/external-entry.txt b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/external-entry.txt new file mode 100644 index 0000000000..848a6ed7ab --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/external-entry.txt @@ -0,0 +1,2 @@ +export { foo } from 'foo'; +export { externalValue } from './external.js'; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/external.config.txt b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/external.config.txt new file mode 100644 index 0000000000..701e4cea58 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/external.config.txt @@ -0,0 +1,35 @@ +const externalOptions = ['foo', './external.js']; +const externalAlias = externalOptions; + +export default { + pack: [ + { + entry: 'src/index.ts', + outDir: 'dist/top', + dts: false, + external: ['foo', './external.js'], + skipNodeModulesBundle: true, + }, + { + entry: 'src/index.ts', + outDir: 'dist/nested', + dts: false, + external: ['foo', './external.js'], + deps: { skipNodeModulesBundle: true }, + }, + { + entry: 'src/index.ts', + outDir: 'dist/constant', + dts: false, + external: externalOptions, + skipNodeModulesBundle: true, + }, + { + entry: 'src/index.ts', + outDir: 'dist/alias', + dts: false, + external: externalAlias, + deps: { skipNodeModulesBundle: true }, + }, + ], +}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/helper.txt b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/helper.txt new file mode 100644 index 0000000000..6812cd906f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/helper.txt @@ -0,0 +1 @@ +export const helper: number = 42; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/no-external-entry.txt b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/no-external-entry.txt new file mode 100644 index 0000000000..c54745e96c --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/no-external-entry.txt @@ -0,0 +1 @@ +export { bundledValue } from '@fixture/pack-bundled'; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/no-external.config.txt b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/no-external.config.txt new file mode 100644 index 0000000000..e614018f4d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/no-external.config.txt @@ -0,0 +1,28 @@ +import { defineConfig } from 'vite-plus'; + +const bundlePatterns = ['@fixture/pack-bundled']; + +export default defineConfig({ + pack: [ + { + entry: 'src/index.ts', outDir: 'dist/literal', dts: false, + noExternal: ['@fixture/pack-bundled'], + }, + { + entry: 'src/index.ts', outDir: 'dist/reference', dts: false, + noExternal: bundlePatterns, + }, + { + entry: 'src/index.ts', outDir: 'dist/callback', dts: false, + noExternal: (id) => bundlePatterns.includes(id), + }, + { + entry: 'src/index.ts', outDir: 'dist/method', dts: false, + noExternal(id) { return bundlePatterns.includes(id); }, + deps: { onlyBundle: bundlePatterns }, + }, + { + entry: 'src/index.ts', outDir: 'dist/control', dts: false, + }, + ], +}); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/pack-dependency/index.js b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/pack-dependency/index.js new file mode 100644 index 0000000000..b965a8a799 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/pack-dependency/index.js @@ -0,0 +1 @@ +export const bundledValue = 42; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/pack-dependency/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/pack-dependency/package.json new file mode 100644 index 0000000000..0c2c6966a7 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/pack-dependency/package.json @@ -0,0 +1,6 @@ +{ + "name": "@fixture/pack-bundled", + "version": "1.0.0", + "type": "module", + "exports": "./index.js" +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/package.json new file mode 100644 index 0000000000..45223447d3 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/package.json @@ -0,0 +1,18 @@ +{ + "name": "migration-pack-tsdown-023", + "type": "module", + "private": true, + "scripts": { + "build": "vp pack --public-dir public" + }, + "devDependencies": { + "vite-plus": "0.2.0" + }, + "devEngines": { + "packageManager": { + "name": "npm", + "version": "11.16.0", + "onFail": "download" + } + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/public/asset.txt b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/public/asset.txt new file mode 100644 index 0000000000..2fd4c6ebcc --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/public/asset.txt @@ -0,0 +1 @@ +copied asset diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots.toml new file mode 100644 index 0000000000..938000fb01 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots.toml @@ -0,0 +1,104 @@ +[[case]] +name = "migration_pack_tsdown_023" +vp = "global" +comment = "Upgrade an existing Vite+ pack config without --full and preserve it on a second migration." +steps = [ + { argv = ["vp", "migrate", "--no-interactive"] }, + { argv = ["vpt", "print-file", "vite.config.ts"] }, + { argv = ["vpt", "print-file", "package.json"] }, + { argv = ["vp", "migrate", "--no-interactive"] }, + { argv = ["vpt", "print-file", "vite.config.ts"] }, +] + +[[case]] +name = "migration_pack_tsdown_023_build" +vp = "global" +comment = "Build a migrated library and check that static assets and declarations are emitted." +steps = [ + { argv = ["vpt", "write-file", "vite.config.ts", "export default { pack: { entry: 'src/index.ts', bundle: false, publicDir: 'public', removeNodeProtocol: true, dts: { oxc: true, cjsReexport: false } } };"], snapshot = false }, + { argv = ["vp", "migrate", "--no-interactive"] }, + { argv = ["vp", "run", "build"] }, + { argv = ["vpt", "stat-file", "dist/index.mjs", "--assert", "file"] }, + { argv = ["vpt", "stat-file", "dist/index.d.mts", "--assert", "file"] }, + { argv = ["vpt", "print-file", "dist/public/asset.txt"] }, +] + +[[case]] +name = "migration_pack_tsdown_023_external" +vp = "global" +comment = "Preserve external matchers when either skipNodeModulesBundle form becomes deps.neverBundle." +steps = [ + { argv = ["vpt", "cp", "external.config.txt", "vite.config.ts"], snapshot = false }, + { argv = ["vpt", "cp", "external-entry.txt", "src/index.ts"], snapshot = false }, + { argv = ["vp", "migrate", "--no-interactive"] }, + { argv = ["vpt", "print-file", "vite.config.ts"] }, + { argv = ["vp", "pack"], snapshot = false }, + { argv = ["vpt", "print-file", "dist/top/index.mjs"] }, + { argv = ["vpt", "print-file", "dist/nested/index.mjs"] }, + { argv = ["vpt", "print-file", "dist/constant/index.mjs"] }, + { argv = ["vpt", "print-file", "dist/alias/index.mjs"] }, + { argv = ["vp", "migrate", "--no-interactive"] }, +] + +[[case]] +name = "migration_pack_tsdown_023_concise_methods" +vp = "global" +comment = "Migrate a standalone concise arrow and method options, then check unbundled files and copied assets." +steps = [ + { argv = ["vpt", "cp", "concise.config.txt", "tsdown.config.ts"], snapshot = false }, + { argv = ["vpt", "write-file", "vite.config.ts", "import packConfig from './tsdown.config.js';\nexport default { pack: packConfig({}) };\n"], snapshot = false }, + { argv = ["vpt", "cp", "concise-entry.txt", "src/index.ts"], snapshot = false }, + { argv = ["vpt", "cp", "helper.txt", "src/helper.ts"], snapshot = false }, + { argv = ["vp", "migrate", "--no-interactive"] }, + { argv = ["vpt", "print-file", "tsdown.config.ts"] }, + { argv = ["vp", "pack"] }, + { argv = ["vpt", "stat-file", "dist/index.custom.js", "--assert", "file"] }, + { argv = ["vpt", "stat-file", "dist/helper.custom.js", "--assert", "file"] }, + { argv = ["vpt", "print-file", "dist/assets/asset.txt"] }, + { argv = ["vp", "migrate", "--no-interactive"] }, + { argv = ["vpt", "print-file", "tsdown.config.ts"] }, +] + +[[case]] +name = "migration_pack_tsdown_023_external_constants" +vp = "global" +comment = "Migrate constant external references with both skip forms without a manual-migration warning." +steps = [ + { argv = ["vpt", "cp", "external-constants.config.txt", "vite.config.ts"], snapshot = false }, + { argv = ["vp", "migrate", "--no-interactive"] }, + { argv = ["vpt", "print-file", "vite.config.ts"] }, + { argv = ["vp", "migrate", "--no-interactive"] }, + { argv = ["vpt", "print-file", "vite.config.ts"] }, +] + +[[case]] +name = "migration_pack_tsdown_023_external_conflicts" +vp = "global" +comment = "Report conflicting dependency rules while preserving the pack options." +steps = [ + { argv = ["vpt", "cp", "external-conflicts.config.txt", "vite.config.ts"], snapshot = false }, + { argv = ["vp", "migrate", "--no-interactive"] }, + { argv = ["vpt", "print-file", "vite.config.ts"] }, +] + +[[case]] +name = "migration_pack_tsdown_023_no_external" +vp = "global" +comment = "Migrate noExternal patterns, references, and callbacks and confirm the dependency remains bundled." +steps = [ + { argv = ["vpt", "cp", "no-external.config.txt", "vite.config.ts"], snapshot = false }, + { argv = ["vpt", "cp", "no-external-entry.txt", "src/index.ts"], snapshot = false }, + { argv = ["vpt", "json-edit", "package.json", "dependencies", '{"@fixture/pack-bundled":"file:./pack-dependency"}'], snapshot = false }, + { argv = ["vp", "migrate", "--no-interactive"] }, + { argv = ["vpt", "print-file", "vite.config.ts"] }, + { argv = ["vp", "migrate", "--no-interactive"] }, + { argv = ["vpt", "print-file", "vite.config.ts"] }, + { argv = ["vp", "install"], snapshot = false, timeout = 120000 }, + { argv = ["vpt", "stat-file", "node_modules/@fixture/pack-bundled/index.js", "--assert", "file"] }, + { argv = ["vp", "pack", "--fail-on-warn"], snapshot = false }, + { argv = ["vpt", "print-file", "dist/literal/index.mjs"] }, + { argv = ["vpt", "print-file", "dist/reference/index.mjs"] }, + { argv = ["vpt", "print-file", "dist/callback/index.mjs"] }, + { argv = ["vpt", "print-file", "dist/method/index.mjs"] }, + { argv = ["vpt", "print-file", "dist/control/index.mjs"] }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023.md new file mode 100644 index 0000000000..48d9dc17bc --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023.md @@ -0,0 +1,113 @@ +# migration_pack_tsdown_023 + +Upgrade an existing Vite+ pack config without --full and preserve it on a second migration. + +## `vp migrate --no-interactive` + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Updated . to Vite+ +• Node npm +• Dependencies: + vite-plus 0.2.0 → + vite → +• 1 file had imports rewritten +• Package manager settings configured +``` + +## `vpt print-file vite.config.ts` + +``` +import { defineConfig } from 'vite-plus'; + +export default defineConfig(() => ({ + // This is Vite's public directory, which must stay unchanged. + publicDir: 'vite-public', + pack: [ + { + entry: 'src/index.ts', + unbundle: true, + outExtensions: () => ({ js: '.mjs' }), + copy: 'public', + nodeProtocol: 'strip', + css: { inject: true }, + deps: { neverBundle: true, onlyBundle: [/^allowed/], resolveDepSubpath: true }, + + dts: { generator: 'oxc', }, + attw: { profile: 'strict' }, + }, + { + entry: 'src/index.ts', + deps: { onlyBundle: false, neverBundle: true, resolveDepSubpath: false }, + dts: { generator: 'tsgo', tsgo: { path: './tsgo' }, }, + attw: { profile: 'node16' }, + }, + ], +})); +``` + +## `vpt print-file package.json` + +``` +{ + "name": "migration-pack-tsdown-023", + "type": "module", + "private": true, + "scripts": { + "build": "vp pack --copy public" + }, + "devDependencies": { + "vite-plus": "" + }, + "devEngines": { + "packageManager": { + "name": "npm", + "version": "", + "onFail": "download" + } + }, + "overrides": { + "vite": "npm:@voidzero-dev/vite-plus-core@" + } +} +``` + +## `vp migrate --no-interactive` + +``` +VITE+ - The Unified Toolchain for the Web + +This project is already using Vite+! Happy coding! +``` + +## `vpt print-file vite.config.ts` + +``` +import { defineConfig } from 'vite-plus'; + +export default defineConfig(() => ({ + // This is Vite's public directory, which must stay unchanged. + publicDir: 'vite-public', + pack: [ + { + entry: 'src/index.ts', + unbundle: true, + outExtensions: () => ({ js: '.mjs' }), + copy: 'public', + nodeProtocol: 'strip', + css: { inject: true }, + deps: { neverBundle: true, onlyBundle: [/^allowed/], resolveDepSubpath: true }, + + dts: { generator: 'oxc', }, + attw: { profile: 'strict' }, + }, + { + entry: 'src/index.ts', + deps: { onlyBundle: false, neverBundle: true, resolveDepSubpath: false }, + dts: { generator: 'tsgo', tsgo: { path: './tsgo' }, }, + attw: { profile: 'node16' }, + }, + ], +})); +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_build.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_build.md new file mode 100644 index 0000000000..02675c1831 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_build.md @@ -0,0 +1,53 @@ +# migration_pack_tsdown_023_build + +Build a migrated library and check that static assets and declarations are emitted. + +## `vpt write-file vite.config.ts 'export default { pack: { entry: '\''src/index.ts'\'', bundle: false, publicDir: '\''public'\'', removeNodeProtocol: true, dts: { oxc: true, cjsReexport: false } } };'` + + +## `vp migrate --no-interactive` + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Updated . to Vite+ +• Node npm +• Dependencies: + vite-plus 0.2.0 → + vite → +• 1 file had imports rewritten +• Package manager settings configured +``` + +## `vp run build` + +``` +VITE+ - The Unified Toolchain for the Web + +$ vp pack --copy public ⊘ cache disabled +ℹ entry: src/index.ts +ℹ tsconfig: tsconfig.json +ℹ Build start +ℹ dist/index.mjs kB │ gzip: kB +ℹ dist/index.d.mts kB │ gzip: kB +ℹ 2 files, total: kB +✔ Build complete in +``` + +## `vpt stat-file dist/index.mjs --assert file` + +``` +dist/index.mjs: file +``` + +## `vpt stat-file dist/index.d.mts --assert file` + +``` +dist/index.d.mts: file +``` + +## `vpt print-file dist/public/asset.txt` + +``` +copied asset +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_concise_methods.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_concise_methods.md new file mode 100644 index 0000000000..cef0834214 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_concise_methods.md @@ -0,0 +1,101 @@ +# migration_pack_tsdown_023_concise_methods + +Migrate a standalone concise arrow and method options, then check unbundled files and copied assets. + +## `vpt cp concise.config.txt tsdown.config.ts` + + +## `vpt write-file vite.config.ts 'import packConfig from '\''./tsdown.config.js'\''; +export default { pack: packConfig({}) }; +'` + + +## `vpt cp concise-entry.txt src/index.ts` + + +## `vpt cp helper.txt src/helper.ts` + + +## `vp migrate --no-interactive` + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Updated . to Vite+ +• Node npm +• Dependencies: + vite-plus 0.2.0 → + vite → +• 1 file had imports rewritten +• Package manager settings configured +``` + +## `vpt print-file tsdown.config.ts` + +``` +import { defineConfig } from 'vite-plus/pack'; + +export default defineConfig(() => ({ deps: { resolveDepSubpath: true }, + entry: 'src/index.ts', + unbundle: true, + dts: { generator: 'oxc' }, + outExtensions() { return { js: '.custom.js' }; }, + copy() { return ['assets']; }, +})); +``` + +## `vp pack` + +``` +VITE+ - The Unified Toolchain for the Web + +ℹ entry: src/index.ts +ℹ tsconfig: tsconfig.json +ℹ Build start +ℹ dist/index.d.custom.ts kB │ gzip: kB +ℹ dist/index.custom.js kB │ gzip: kB +ℹ dist/helper.d.custom.ts kB │ gzip: kB +ℹ dist/helper.custom.js kB │ gzip: kB +ℹ 4 files, total: kB +✔ Build complete in +``` + +## `vpt stat-file dist/index.custom.js --assert file` + +``` +dist/index.custom.js: file +``` + +## `vpt stat-file dist/helper.custom.js --assert file` + +``` +dist/helper.custom.js: file +``` + +## `vpt print-file dist/assets/asset.txt` + +``` +method copy asset +``` + +## `vp migrate --no-interactive` + +``` +VITE+ - The Unified Toolchain for the Web + +This project is already using Vite+! Happy coding! +``` + +## `vpt print-file tsdown.config.ts` + +``` +import { defineConfig } from 'vite-plus/pack'; + +export default defineConfig(() => ({ deps: { resolveDepSubpath: true }, + entry: 'src/index.ts', + unbundle: true, + dts: { generator: 'oxc' }, + outExtensions() { return { js: '.custom.js' }; }, + copy() { return ['assets']; }, +})); +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_external.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_external.md new file mode 100644 index 0000000000..75e52492f5 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_external.md @@ -0,0 +1,106 @@ +# migration_pack_tsdown_023_external + +Preserve external matchers when either skipNodeModulesBundle form becomes deps.neverBundle. + +## `vpt cp external.config.txt vite.config.ts` + + +## `vpt cp external-entry.txt src/index.ts` + + +## `vp migrate --no-interactive` + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Updated . to Vite+ +• Node npm +• Dependencies: + vite-plus 0.2.0 → + vite → +• 1 file had imports rewritten +• Package manager settings configured +``` + +## `vpt print-file vite.config.ts` + +``` +const externalOptions = ['foo', './external.js']; +const externalAlias = externalOptions; + +export default { + pack: [ + { + entry: 'src/index.ts', + outDir: 'dist/top', + dts: false, + inputOptions: { external: ['foo', './external.js'] }, + deps: { neverBundle: true, resolveDepSubpath: true }, + }, + { + entry: 'src/index.ts', + outDir: 'dist/nested', + dts: false, + inputOptions: { external: ['foo', './external.js'] }, + deps: { resolveDepSubpath: true, neverBundle: true }, + }, + { + entry: 'src/index.ts', + outDir: 'dist/constant', + dts: false, + inputOptions: { external: externalOptions }, + deps: { neverBundle: true, resolveDepSubpath: true }, + }, + { + entry: 'src/index.ts', + outDir: 'dist/alias', + dts: false, + inputOptions: { external: externalAlias }, + deps: { resolveDepSubpath: true, neverBundle: true }, + }, + ], +}; +``` + +## `vp pack` + + +## `vpt print-file dist/top/index.mjs` + +``` +import { foo } from "foo"; +import { externalValue } from "./external.js"; +export { externalValue, foo }; +``` + +## `vpt print-file dist/nested/index.mjs` + +``` +import { foo } from "foo"; +import { externalValue } from "./external.js"; +export { externalValue, foo }; +``` + +## `vpt print-file dist/constant/index.mjs` + +``` +import { foo } from "foo"; +import { externalValue } from "./external.js"; +export { externalValue, foo }; +``` + +## `vpt print-file dist/alias/index.mjs` + +``` +import { foo } from "foo"; +import { externalValue } from "./external.js"; +export { externalValue, foo }; +``` + +## `vp migrate --no-interactive` + +``` +VITE+ - The Unified Toolchain for the Web + +This project is already using Vite+! Happy coding! +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_external_conflicts.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_external_conflicts.md new file mode 100644 index 0000000000..ede6b7f29d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_external_conflicts.md @@ -0,0 +1,44 @@ +# migration_pack_tsdown_023_external_conflicts + +Report conflicting dependency rules while preserving the pack options. + +## `vpt cp external-conflicts.config.txt vite.config.ts` + + +## `vp migrate --no-interactive` + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Updated . to Vite+ +• Node npm +• Dependencies: + vite-plus 0.2.0 → + vite → +• Package manager settings configured +! Warnings: + - vite.config.ts: Cannot safely combine external with skipNodeModulesBundle. Migrate this pack config manually; its options were left unchanged. See https://tsdown.dev/options/dependencies#migration-from-deprecated-options +``` + +## `vpt print-file vite.config.ts` + +``` +const externalOptions = ['foo']; + +export default { + pack: [ + { + external: externalOptions, + skipNodeModulesBundle: true, + inputOptions(options) { return { external: options.external }; }, + bundle: false, + dts: { tsgo: true }, + }, + { + external: externalOptions, + deps: { skipNodeModulesBundle: true, dts: { neverBundle: ['types'] } }, + bundle: false, + }, + ], +}; +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_external_constants.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_external_constants.md new file mode 100644 index 0000000000..2739fb9bff --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_external_constants.md @@ -0,0 +1,54 @@ +# migration_pack_tsdown_023_external_constants + +Migrate constant external references with both skip forms without a manual-migration warning. + +## `vpt cp external-constants.config.txt vite.config.ts` + + +## `vp migrate --no-interactive` + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Updated . to Vite+ +• Node npm +• Dependencies: + vite-plus 0.2.0 → + vite → +• 1 file had imports rewritten +• Package manager settings configured +``` + +## `vpt print-file vite.config.ts` + +``` +const externalOptions = ['foo']; + +export default { + pack: [ + { inputOptions: { external: externalOptions }, deps: { neverBundle: true, resolveDepSubpath: true }, unbundle: true, dts: { generator: 'tsgo' } }, + { inputOptions: { external: externalOptions }, deps: { resolveDepSubpath: true, neverBundle: true }, unbundle: true }, + ], +}; +``` + +## `vp migrate --no-interactive` + +``` +VITE+ - The Unified Toolchain for the Web + +This project is already using Vite+! Happy coding! +``` + +## `vpt print-file vite.config.ts` + +``` +const externalOptions = ['foo']; + +export default { + pack: [ + { inputOptions: { external: externalOptions }, deps: { neverBundle: true, resolveDepSubpath: true }, unbundle: true, dts: { generator: 'tsgo' } }, + { inputOptions: { external: externalOptions }, deps: { resolveDepSubpath: true, neverBundle: true }, unbundle: true }, + ], +}; +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_no_external.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_no_external.md new file mode 100644 index 0000000000..e6c74ae187 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/snapshots/migration_pack_tsdown_023_no_external.md @@ -0,0 +1,155 @@ +# migration_pack_tsdown_023_no_external + +Migrate noExternal patterns, references, and callbacks and confirm the dependency remains bundled. + +## `vpt cp no-external.config.txt vite.config.ts` + + +## `vpt cp no-external-entry.txt src/index.ts` + + +## `vpt json-edit package.json dependencies '{"@fixture/pack-bundled":"file:./pack-dependency"}'` + + +## `vp migrate --no-interactive` + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Updated . to Vite+ +• Node npm +• Dependencies: + vite-plus 0.2.0 → + vite → +• 1 file had imports rewritten +• Package manager settings configured +``` + +## `vpt print-file vite.config.ts` + +``` +import { defineConfig } from 'vite-plus'; + +const bundlePatterns = ['@fixture/pack-bundled']; + +export default defineConfig({ + pack: [ + { + entry: 'src/index.ts', outDir: 'dist/literal', dts: false, + deps: { alwaysBundle: ['@fixture/pack-bundled'], resolveDepSubpath: true }, + }, + { + entry: 'src/index.ts', outDir: 'dist/reference', dts: false, + deps: { alwaysBundle: bundlePatterns, resolveDepSubpath: true }, + }, + { + entry: 'src/index.ts', outDir: 'dist/callback', dts: false, + deps: { alwaysBundle: (id) => bundlePatterns.includes(id), resolveDepSubpath: true }, + }, + { + entry: 'src/index.ts', outDir: 'dist/method', dts: false, + + deps: { alwaysBundle(id) { return bundlePatterns.includes(id); }, resolveDepSubpath: true, onlyBundle: bundlePatterns }, + }, + { deps: { resolveDepSubpath: true }, + entry: 'src/index.ts', outDir: 'dist/control', dts: false, + }, + ], +}); +``` + +## `vp migrate --no-interactive` + +``` +VITE+ - The Unified Toolchain for the Web + +This project is already using Vite+! Happy coding! +``` + +## `vpt print-file vite.config.ts` + +``` +import { defineConfig } from 'vite-plus'; + +const bundlePatterns = ['@fixture/pack-bundled']; + +export default defineConfig({ + pack: [ + { + entry: 'src/index.ts', outDir: 'dist/literal', dts: false, + deps: { alwaysBundle: ['@fixture/pack-bundled'], resolveDepSubpath: true }, + }, + { + entry: 'src/index.ts', outDir: 'dist/reference', dts: false, + deps: { alwaysBundle: bundlePatterns, resolveDepSubpath: true }, + }, + { + entry: 'src/index.ts', outDir: 'dist/callback', dts: false, + deps: { alwaysBundle: (id) => bundlePatterns.includes(id), resolveDepSubpath: true }, + }, + { + entry: 'src/index.ts', outDir: 'dist/method', dts: false, + + deps: { alwaysBundle(id) { return bundlePatterns.includes(id); }, resolveDepSubpath: true, onlyBundle: bundlePatterns }, + }, + { deps: { resolveDepSubpath: true }, + entry: 'src/index.ts', outDir: 'dist/control', dts: false, + }, + ], +}); +``` + +## `vp install` + + +## `vpt stat-file node_modules/@fixture/pack-bundled/index.js --assert file` + +``` +node_modules/@fixture/pack-bundled/index.js: file +``` + +## `vp pack --fail-on-warn` + + +## `vpt print-file dist/literal/index.mjs` + +``` +//#region pack-dependency/index.js +const bundledValue = 42; +//#endregion +export { bundledValue }; +``` + +## `vpt print-file dist/reference/index.mjs` + +``` +//#region pack-dependency/index.js +const bundledValue = 42; +//#endregion +export { bundledValue }; +``` + +## `vpt print-file dist/callback/index.mjs` + +``` +//#region pack-dependency/index.js +const bundledValue = 42; +//#endregion +export { bundledValue }; +``` + +## `vpt print-file dist/method/index.mjs` + +``` +//#region pack-dependency/index.js +const bundledValue = 42; +//#endregion +export { bundledValue }; +``` + +## `vpt print-file dist/control/index.mjs` + +``` +import { bundledValue } from "@fixture/pack-bundled"; +export { bundledValue }; +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/src/index.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/src/index.ts new file mode 100644 index 0000000000..1b55ac618d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/src/index.ts @@ -0,0 +1 @@ +export const value: number = 42; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/tsconfig.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/tsconfig.json new file mode 100644 index 0000000000..6db6cf3ad8 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/tsconfig.json @@ -0,0 +1 @@ +{"compilerOptions":{"isolatedDeclarations":true}} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/vite.config.ts new file mode 100644 index 0000000000..5058b8c644 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_pack_tsdown_023/vite.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'vite-plus'; + +export default defineConfig(() => ({ + // This is Vite's public directory, which must stay unchanged. + publicDir: 'vite-public', + pack: [ + { + entry: 'src/index.ts', + bundle: false, + outExtension: () => ({ js: '.mjs' }), + publicDir: 'public', + removeNodeProtocol: true, + injectStyle: true, + inlineOnly: [/^allowed/], + skipNodeModulesBundle: true, + dts: { oxc: true, cjsReexport: false }, + attw: true, + }, + { + entry: 'src/index.ts', + deps: { onlyAllowBundle: false, skipNodeModulesBundle: true, resolveDepSubpath: false }, + dts: { tsgo: { path: './tsgo' }, cjsReexport: true }, + attw: { profile: 'node16' }, + }, + ], +})); diff --git a/crates/vp_migration/src/import_rewriter.rs b/crates/vp_migration/src/import_rewriter.rs index 812a637b8a..7b7493fb5e 100644 --- a/crates/vp_migration/src/import_rewriter.rs +++ b/crates/vp_migration/src/import_rewriter.rs @@ -2115,6 +2115,8 @@ struct RewriteResult { pub updated: bool, /// Whether an upstream `vitest` specifier was intentionally preserved. pub preserved_vitest: bool, + /// Pack configurations that need manual migration. + pub warnings: Vec, } /// Result of rewriting imports in multiple files @@ -2128,11 +2130,13 @@ pub struct BatchRewriteResult { pub preserved_vitest_files: Vec, /// Files that had errors (path, error message) pub errors: Vec<(PathBuf, String)>, + /// Pack configurations that need manual migration. + pub warnings: Vec<(PathBuf, String)>, } enum FileResult { - Modified, - Unchanged, + Modified(Vec), + Unchanged(Vec), Error(String), } @@ -2200,7 +2204,7 @@ pub fn rewrite_imports_in_directory_with_options( .map(|(file_path, package_context)| { let skip_packages = package_context.skip_packages; if skip_packages.all_skipped() { - return (file_path, FileResult::Unchanged, false); + return (file_path, FileResult::Unchanged(Vec::new()), false); } match rewrite_import( @@ -2213,10 +2217,18 @@ pub fn rewrite_imports_in_directory_with_options( if let Err(e) = std::fs::write(&file_path, &rewrite_result.content) { (file_path, FileResult::Error(e.to_string()), false) } else { - (file_path, FileResult::Modified, rewrite_result.preserved_vitest) + ( + file_path, + FileResult::Modified(rewrite_result.warnings), + rewrite_result.preserved_vitest, + ) } } else { - (file_path, FileResult::Unchanged, rewrite_result.preserved_vitest) + ( + file_path, + FileResult::Unchanged(rewrite_result.warnings), + rewrite_result.preserved_vitest, + ) } } Err(e) => (file_path, FileResult::Error(e.to_string()), false), @@ -2230,15 +2242,21 @@ pub fn rewrite_imports_in_directory_with_options( unchanged_files: Vec::new(), preserved_vitest_files: Vec::new(), errors: Vec::new(), + warnings: Vec::new(), }; for (file_path, file_result, preserved_vitest) in results { if preserved_vitest { batch_result.preserved_vitest_files.push(file_path.clone()); } + if let FileResult::Modified(warnings) | FileResult::Unchanged(warnings) = &file_result { + batch_result + .warnings + .extend(warnings.iter().map(|message| (file_path.clone(), message.clone()))); + } match file_result { - FileResult::Modified => batch_result.modified_files.push(file_path), - FileResult::Unchanged => batch_result.unchanged_files.push(file_path), + FileResult::Modified(_) => batch_result.modified_files.push(file_path), + FileResult::Unchanged(_) => batch_result.unchanged_files.push(file_path), FileResult::Error(msg) => batch_result.errors.push((file_path, msg)), } } @@ -2286,10 +2304,10 @@ fn rewrite_import( )?; let standalone = file_path.file_stem().is_some_and(|stem| stem == "tsdown.config"); if !skip_packages.skip_tsdown && (standalone || is_vite_config_file(file_path)) { - let rewritten = - crate::vite_config::rewrite_pack_dts_generators(&result.content, standalone); + let rewritten = crate::pack_config::rewrite_pack_config(&result.content, standalone); result.updated |= rewritten != result.content; result.content = rewritten; + result.warnings = crate::pack_config::pack_config_warnings(&result.content, standalone); } Ok(result) } @@ -2343,6 +2361,7 @@ fn rewrite_import_content_full( content: content.to_string(), updated: false, preserved_vitest: false, + warnings: Vec::new(), }); } @@ -2401,7 +2420,7 @@ fn rewrite_import_content_full( &mut preserved_vitest, ); - Ok(RewriteResult { content: new_content, updated, preserved_vitest }) + Ok(RewriteResult { content: new_content, updated, preserved_vitest, warnings: Vec::new() }) } #[cfg(test)] @@ -2412,6 +2431,52 @@ mod tests { use super::*; + #[test] + fn reports_unchanged_pack_configs_that_need_manual_migration() { + let temp = tempfile::tempdir().unwrap(); + let file = temp.path().join("vite.config.ts"); + let content = "export default { pack: { external: externalOptions, deps: { skipNodeModulesBundle: true }, bundle: false, dts: { tsgo: true } } };"; + std::fs::write(&file, content).unwrap(); + let result = rewrite_imports_in_directory(temp.path()).unwrap(); + assert!(result.modified_files.is_empty()); + assert!(result.errors.is_empty()); + assert_eq!(result.warnings.len(), 1); + assert_eq!(result.warnings[0].0, file); + assert!(result.warnings[0].1.contains("Migrate this pack config manually")); + assert_eq!(std::fs::read_to_string(file).unwrap(), content); + } + + #[test] + fn migrate_pack_configs_in_workspace_packages() { + let temp = tempfile::tempdir().unwrap(); + let package = temp.path().join("packages/library"); + std::fs::create_dir_all(&package).unwrap(); + std::fs::write( + temp.path().join("package.json"), + r#"{"devDependencies":{"vite-plus":"0.2.0"}}"#, + ) + .unwrap(); + std::fs::write( + package.join("package.json"), + r#"{"devDependencies":{"vite-plus":"0.2.0"}}"#, + ) + .unwrap(); + let vite_config = package.join("vite.config.ts"); + let tsdown_config = package.join("tsdown.config.mts"); + let source_file = package.join("example.ts"); + std::fs::write(&vite_config, "export default { pack: { bundle: false } };").unwrap(); + std::fs::write(&tsdown_config, "export default { publicDir: 'public' };").unwrap(); + let example = "export default { pack: { bundle: false } };"; + std::fs::write(&source_file, example).unwrap(); + let result = rewrite_imports_in_directory(temp.path()).unwrap(); + assert!(result.errors.is_empty()); + assert_eq!(result.modified_files.len(), 2); + assert!(std::fs::read_to_string(vite_config).unwrap().contains("unbundle: true")); + assert!(std::fs::read_to_string(tsdown_config).unwrap().contains("copy: 'public'")); + assert_eq!(std::fs::read_to_string(source_file).unwrap(), example); + assert!(rewrite_imports_in_directory(temp.path()).unwrap().modified_files.is_empty()); + } + #[test] fn test_rewrite_import_content_vite() { let vite_config = r#"import { defineConfig } from 'vite' diff --git a/crates/vp_migration/src/lib.rs b/crates/vp_migration/src/lib.rs index 855f23cd9b..427db5ea50 100644 --- a/crates/vp_migration/src/lib.rs +++ b/crates/vp_migration/src/lib.rs @@ -10,6 +10,7 @@ mod ast_grep; mod eslint; mod file_walker; mod import_rewriter; +mod pack_config; mod package; mod prettier; mod script_rewrite; diff --git a/crates/vp_migration/src/pack_config.rs b/crates/vp_migration/src/pack_config.rs new file mode 100644 index 0000000000..7caa44ceff --- /dev/null +++ b/crates/vp_migration/src/pack_config.rs @@ -0,0 +1,923 @@ +use std::ops::Range; + +use ast_grep_core::{Doc, Node, tree_sitter::StrDoc}; +use ast_grep_language::{LanguageExt, SupportLang}; + +use crate::vite_config::{ + is_direct_recognized_config_object, pair_key_matches, rewrite_pack_dts_generators, +}; + +type Edit = (Range, String); + +/// Upgrade the configuration options removed in tsdown 0.23 without evaluating +/// user code. Only direct pack objects and standalone tsdown configs qualify. +pub(crate) fn rewrite_pack_config(content: &str, standalone: bool) -> String { + let grep = SupportLang::TypeScript.ast_grep(content); + let mut edits = Vec::new(); + for object in grep.root().dfs().filter(|node| node.kind() == "object") { + if !is_pack_object(&object, standalone) { + continue; + } + if external_skip_needs_manual_migration(&object) { + continue; + } + let source = object.text(); + let rewritten = rewrite_options(&object); + if rewritten != source { + edits.push((object.range(), rewritten)); + } + } + // Select the declaration generator after the other option edits. + rewrite_pack_dts_generators(&apply_edits(content, edits, 0), standalone) +} + +pub(crate) fn is_pack_object(object: &Node<'_, D>, standalone: bool) -> bool { + let mut value = object.clone(); + loop { + // The recognizer needs the object *inside* a concise arrow's parentheses. + if standalone && is_top_config_value(&value) { + return true; + } + let Some(parent) = value.parent() else { break }; + match parent.kind().as_ref() { + "array" | "parenthesized_expression" | "satisfies_expression" | "as_expression" => { + value = parent; + } + _ => break, + } + } + value.parent().is_some_and(|pair| { + pair.kind() == "pair" + && pair.field("key").is_some_and(|key| pair_key_matches(&key, "pack")) + && pair.parent().is_some_and(|object| is_top_config_value(&object)) + }) +} + +fn is_top_config_value(node: &Node<'_, D>) -> bool { + is_direct_recognized_config_object(node) + && !node.ancestors().any(|ancestor| ancestor.kind() == "object") +} + +pub(crate) fn property_comments(node: &Node<'_, D>) -> String { + let mut comments = String::new(); + for child in node.children().filter(|child| child.kind() == "comment") { + comments.push_str(&child.text()); + comments.push('\n'); + } + comments +} + +fn apply_edits(content: &str, mut edits: Vec, offset: usize) -> String { + edits.sort_by_key(|(range, _)| std::cmp::Reverse(range.start)); + let mut result = content.to_owned(); + for (range, replacement) in edits { + result.replace_range(range.start - offset..range.end - offset, &replacement); + } + result +} + +fn find_property<'a, D: Doc>(node: &Node<'a, D>, name: &str) -> Option> { + node.children().find(|child| { + child + .field("key") + .or_else(|| child.field("name")) + .is_some_and(|key| pair_key_matches(&key, name)) + || child.kind() == "shorthand_property_identifier" && child.text() == name + }) +} + +fn property_value<'a, D: Doc>(node: &Node<'a, D>, name: &str) -> Option> { + let property = find_property(node, name)?; + property + .field("value") + .or_else(|| (property.kind() == "shorthand_property_identifier").then_some(property)) +} + +/// Edits only direct properties. Spreads, duplicate keys and computed keys make +/// property precedence unknown, so leave such objects for manual migration. +struct ObjectEditor<'a, D: Doc> { + node: Node<'a, D>, + edits: Vec, + additions: Vec, +} + +impl<'a, D: Doc> ObjectEditor<'a, D> { + fn property(&self, name: &str) -> Option> { + find_property(&self.node, name) + } + + fn value(&self, name: &str) -> Option> { + property_value(&self.node, name) + } + + fn remove(&mut self, name: &str) { + let Some(property) = self.property(name) else { return }; + self.edits.push((property.range(), property_comments(&property))); + if let Some(next) = property.next_all().find(|node| node.kind() != "comment") + && next.kind() == "," + { + self.edits.push((next.range(), String::new())); + } + } + + fn rename(&mut self, old: &str, new: &str) { + let Some(property) = self.property(old) else { return }; + if self.property(new).is_some() { + return; + } + if let Some(key) = property.field("key").or_else(|| property.field("name")) { + self.edits.push((key.range(), new.to_owned())); + } else if property.kind() == "shorthand_property_identifier" { + self.edits.push((property.range(), format!("{new}: {old}"))); + } + } + + fn set_default(&mut self, name: &str, value: &str) { + if self.property(name).is_none() { + self.additions.push(format!("{name}: {value}")); + } + } + + fn replace_value(&mut self, name: &str, replacement: String) { + let Some(value) = self.value(name) else { return }; + self.edits.push((value.range(), replacement)); + } + + fn finish(mut self) -> String { + if !self.additions.is_empty() { + let start = self.node.range().start + 1; + self.edits.push((start..start, format!(" {},", self.additions.join(", ")))); + } + apply_edits(&self.node.text(), self.edits, self.node.range().start) + } +} + +fn edit_object( + source: &str, + edit: impl FnOnce(&mut ObjectEditor<'_, StrDoc>), +) -> String { + let wrapped = format!("({source})"); + let grep = SupportLang::TypeScript.ast_grep(&wrapped); + let root = grep.root(); + let Some(node) = root.dfs().find(|node| node.kind() == "object") else { + return source.to_owned(); + }; + if !can_edit_object(&node) { + return source.to_owned(); + } + let mut editor = ObjectEditor { node, edits: Vec::new(), additions: Vec::new() }; + edit(&mut editor); + editor.finish() +} + +pub(crate) fn can_edit_object(node: &Node<'_, D>) -> bool { + let mut names = std::collections::HashSet::new(); + for child in node.children() { + if child.kind() == "spread_element" { + return false; + } + if let Some(key) = child.field("key").or_else(|| child.field("name")) { + if key.kind() == "computed_property_name" + || !names.insert(key.text().trim_matches(['\'', '"']).to_owned()) + { + return false; + } + } else if child.kind() == "shorthand_property_identifier" + && !names.insert(child.text().into_owned()) + { + return false; + } + } + true +} + +const EXTERNAL_SKIP_WARNING: &str = concat!( + "Cannot safely combine external with skipNodeModulesBundle. ", + "Migrate this pack config manually; its options were left unchanged. ", + "See https://tsdown.dev/options/dependencies#migration-from-deprecated-options", +); + +pub(crate) fn pack_config_warnings(content: &str, standalone: bool) -> Vec { + let grep = SupportLang::TypeScript.ast_grep(content); + if grep.root().dfs().any(|node| { + node.kind() == "object" + && is_pack_object(&node, standalone) + && external_skip_needs_manual_migration(&node) + }) { + vec![EXTERNAL_SKIP_WARNING.to_owned()] + } else { + Vec::new() + } +} + +fn has_external_skip(config: &Node<'_, D>) -> bool { + if find_property(config, "external").is_none() { + return false; + } + property_value(config, "skipNodeModulesBundle").is_some_and(|value| value.kind() == "true") + || property_value(config, "deps").is_some_and(|deps| { + deps.kind() == "object" + && property_value(&deps, "skipNodeModulesBundle") + .is_some_and(|value| value.kind() == "true") + }) +} + +pub(crate) fn external_skip_needs_manual_migration(config: &Node<'_, D>) -> bool { + if !has_external_skip(config) { + return false; + } + if !can_edit_object(config) + || property_value(config, "external").is_none_or(|value| !is_static_external(&value)) + { + return true; + } + for (namespace, conflicts) in + [("deps", &["neverBundle", "dts"][..]), ("inputOptions", &["external"][..])] + { + if find_property(config, namespace).is_some() { + let Some(value) = property_value(config, namespace) else { return true }; + if value.kind() != "object" + || !can_edit_object(&value) + || conflicts.iter().any(|name| find_property(&value, name).is_some()) + { + return true; + } + } + } + false +} + +fn is_static_external(value: &Node<'_, D>) -> bool { + match value.kind().as_ref() { + // tsdown interprets a top-level '/pattern/' string as a regular + // expression. Leave that form, and dynamic matchers, for manual review. + "string" => { + let text = value.text(); + let regex_string = text.as_bytes().get(1) == Some(&b'/') + && text.as_bytes().get(text.len() - 2) == Some(&b'/'); + !(text.contains('\\') || regex_string) + } + "regex" => true, + // Array entries pass through tsdown without string-to-regexp conversion. + "array" => value.children().all(|child| { + matches!(child.kind().as_ref(), "[" | "]" | "," | "comment" | "string" | "regex") + }), + "identifier" | "shorthand_property_identifier" => { + constant_initializer(value).is_some_and(|initializer| is_static_external(&initializer)) + } + "as_expression" | "satisfies_expression" | "parenthesized_expression" => value + .children() + .find(|child| !matches!(child.kind().as_ref(), "(" | "comment")) + .is_some_and(|inner| is_static_external(&inner)), + _ => false, + } +} + +/// Follow a local const binding without replacing the reference or evaluating +/// its initializer. Stop at shadowing bindings and unsupported lexical scopes. +fn constant_initializer<'a, D: Doc>(reference: &Node<'a, D>) -> Option> { + let name = reference.text(); + let mentions_name = |pattern: &Node<'_, D>| pattern.dfs().any(|node| node.text() == name); + for scope in reference.ancestors() { + match scope.kind().as_ref() { + "program" | "statement_block" => { + for statement in scope.children() { + let declaration = statement.field("declaration").unwrap_or(statement); + if matches!( + declaration.kind().as_ref(), + "lexical_declaration" | "variable_declaration" + ) { + for declarator in declaration + .children() + .filter(|node| node.kind() == "variable_declarator") + { + let Some(binding) = declarator.field("name") else { continue }; + if !mentions_name(&binding) { + continue; + } + // Only earlier bindings qualify. This also prevents + // cycles when following aliases between constants. + return (binding.kind() == "identifier" + && declaration + .field("kind") + .is_some_and(|kind| kind.text() == "const") + && declarator.range().end < reference.range().start) + .then(|| declarator.field("value")) + .flatten(); + } + } else if declaration + .field("name") + .is_some_and(|binding| mentions_name(&binding)) + { + return None; + } + } + } + "arrow_function" + | "function_expression" + | "function_declaration" + | "generator_function" + | "generator_function_declaration" + | "method_definition" => { + if ["parameter", "parameters", "name"] + .iter() + .any(|field| scope.field(field).is_some_and(|pattern| mentions_name(&pattern))) + { + return None; + } + // A var declaration can shadow an outer constant even when it + // appears in a nested block of the callback. + if scope.dfs().filter(|node| node.kind() == "variable_declaration").any( + |declaration| { + declaration.children().any(|declarator| { + declarator.field("name").is_some_and(|binding| mentions_name(&binding)) + }) + }, + ) { + return None; + } + } + "catch_clause" | "for_statement" | "for_in_statement" | "switch_body" + | "with_statement" | "class" | "class_declaration" | "internal_module" => return None, + _ => {} + } + } + None +} + +fn rewrite_options(object: &Node<'_, D>) -> String { + // Rolldown can retain the original static matcher while tsdown's deps + // plugin handles neverBundle: true. Avoid overriding DTS-specific matchers + // or user inputOptions; those combinations are reported for manual review. + let source = if has_external_skip(object) { + move_option(&object.text(), "external", "inputOptions", "external", false) + } else { + object.text().into_owned() + }; + // First update nested namespaces; subsequent moves see the new keys and + // cannot create duplicate deps/css objects or overwrite explicit settings. + let source = edit_object(&source, |config| { + for name in ["deps", "dts", "attw"] { + let Some(value) = config.value(name) else { continue }; + if value.kind() != "object" { + continue; + } + let updated = edit_object(&value.text(), |options| match name { + "deps" => { + options.rename("onlyAllowBundle", "onlyBundle"); + if let Some(skip) = options.value("skipNodeModulesBundle") { + if skip.kind() == "false" { + options.remove("skipNodeModulesBundle"); + } else if skip.kind() == "true" && options.property("neverBundle").is_none() + { + options.rename("skipNodeModulesBundle", "neverBundle"); + } + } + options.set_default("resolveDepSubpath", "true"); + } + "dts" => { + if options + .value("cjsReexport") + .is_some_and(|value| matches!(value.kind().as_ref(), "true" | "false")) + { + options.remove("cjsReexport"); + } + } + "attw" => { + if options.value("enabled").is_none_or(|value| value.kind() != "false") { + options.set_default("profile", "'strict'"); + } + } + _ => unreachable!(), + }); + config.replace_value(name, updated); + } + }); + let source = edit_object(&source, |config| { + config.rename("outExtension", "outExtensions"); + config.rename("publicDir", "copy"); + for (old, new, replacement) in + [("bundle", "unbundle", "true"), ("removeNodeProtocol", "nodeProtocol", "'strip'")] + { + let Some(value) = config.value(old) else { continue }; + let active = if old == "bundle" { "false" } else { "true" }; + if value.kind() == active && config.property(new).is_none() { + config.rename(old, new); + config.replace_value(old, replacement.to_owned()); + } else if matches!(value.kind().as_ref(), "true" | "false") + && (value.kind() != active || old == "bundle") + { + config.remove(old); + } + } + if config.value("attw").is_some_and(|value| value.kind() == "true") { + config.replace_value("attw", "{ profile: 'strict' }".to_owned()); + } + }); + let source = move_option(&source, "injectStyle", "css", "inject", false); + let source = move_option(&source, "inlineOnly", "deps", "onlyBundle", false); + let source = move_option(&source, "noExternal", "deps", "alwaysBundle", false); + let source = move_option(&source, "skipNodeModulesBundle", "deps", "neverBundle", true); + edit_object(&source, |config| { + config.set_default("deps", "{ resolveDepSubpath: true }"); + }) +} + +fn move_option(source: &str, old: &str, group: &str, new: &str, boolean: bool) -> String { + edit_object(source, |config| { + let Some(property) = config.property(old) else { return }; + let value = config.value(old); + if boolean { + let Some(value) = &value else { return }; + match value.kind().as_ref() { + "false" => { + config.remove(old); + return; + } + "true" => {} + _ => return, + } + } + let replacement = if let Some(value) = value { + format!("{new}: {}", value.text()) + } else if property.kind() == "method_definition" + && !property.children().any(|child| matches!(child.kind().as_ref(), "get" | "set")) + { + let Some(name) = property.field("name") else { return }; + apply_edits( + &property.text(), + vec![(name.range(), new.to_owned())], + property.range().start, + ) + } else { + return; + }; + if let Some(namespace) = config.value(group) { + if namespace.kind() != "object" { + return; + } + let mut moved = false; + let updated = edit_object(&namespace.text(), |options| { + if options.property(new).is_none() { + options.additions.push(replacement.clone()); + moved = true; + } + }); + if moved { + config.replace_value(group, updated); + config.remove(old); + } + } else if config.property(group).is_none() { + let defaults = if group == "deps" { ", resolveDepSubpath: true" } else { "" }; + // Replace in place so comments on the old option stay attached. + config + .edits + .push((property.range(), format!("{group}: {{ {replacement}{defaults} }}"))); + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn migrate(options: &str) -> String { + let input = format!("export default defineConfig({{ pack: {options} }});"); + let actual = rewrite_pack_config(&input, false); + assert_eq!(rewrite_pack_config(&actual, false), actual, "migration must be idempotent"); + let grep = SupportLang::TypeScript.ast_grep(&actual); + assert!(!grep.root().dfs().any(|node| node.kind() == "ERROR"), "{actual}"); + actual + } + + #[test] + fn migrates_static_external_with_both_skip_forms() { + for external in + ["['foo']", "['foo', /^virtual:/, './local.js']", "'foo'", "/^virtual:/", "[]"] + { + for skip in ["skipNodeModulesBundle: true", "deps: { skipNodeModulesBundle: true }"] { + let actual = migrate(&format!("{{ external: {external}, {skip} }}")); + assert!( + actual.contains(&format!("inputOptions: {{ external: {external} }}")), + "{actual}" + ); + assert!(actual.contains("neverBundle: true"), "{actual}"); + assert!(!actual.contains("skipNodeModulesBundle"), "{actual}"); + assert!(pack_config_warnings(&actual, false).is_empty()); + } + } + let actual = migrate( + "{ external: ['foo'], skipNodeModulesBundle: true, inputOptions: { treeshake: false } }", + ); + assert!(actual.contains("external: ['foo'], treeshake: false"), "{actual}"); + } + + #[test] + fn migrates_no_external_values_without_evaluation() { + for value in [ + "['foo', /^@vendor\\//]", + "'foo'", + "/^@vendor\\//", + "bundlePatterns", + "getBundlePatterns()", + "production ? ['foo'] : []", + "(id) => id === 'foo'", + "function (id) { return id === 'foo'; }", + ] { + for deps in ["", ", deps: { onlyBundle: ['foo'] }", ", deps: { neverBundle: true }"] { + let actual = migrate(&format!("{{ noExternal: {value}{deps} }}")); + assert!(actual.contains(&format!("alwaysBundle: {value}")), "{actual}"); + assert!(!actual.contains("noExternal"), "{actual}"); + assert!(actual.contains("resolveDepSubpath: true"), "{actual}"); + assert!(pack_config_warnings(&actual, false).is_empty()); + } + } + let actual = migrate("{ noExternal }"); + assert!(actual.contains("alwaysBundle: noExternal"), "{actual}"); + } + + #[test] + fn migrates_no_external_methods_in_standalone_callbacks() { + for deps in ["", ", deps: { onlyBundle: ['foo'] }"] { + let input = format!( + "export default defineConfig(() => ({{ noExternal(id) {{ /* match */ return id === 'foo'; }}{deps} }}));" + ); + let actual = rewrite_pack_config(&input, true); + assert!( + actual.contains("alwaysBundle(id) { /* match */ return id === 'foo'; }"), + "{actual}" + ); + assert!(!actual.contains("noExternal"), "{actual}"); + assert!(pack_config_warnings(&actual, true).is_empty()); + assert_eq!(rewrite_pack_config(&actual, true), actual); + } + } + + #[test] + fn preserves_no_external_conflicts_and_unknown_deps() { + for deps in [ + "customDeps", + "{ ...customDeps }", + "{ alwaysBundle: ['bar'] }", + "{ alwaysBundle(id) { return id === 'bar'; } }", + ] { + for option in ["noExternal: bundlePatterns", "noExternal(id) { return id === 'foo'; }"] + { + let actual = migrate(&format!("{{ {option}, deps: {deps} }}")); + assert!(actual.contains(option), "{actual}"); + assert!(!actual.contains("alwaysBundle: bundlePatterns"), "{actual}"); + assert!(!actual.contains("alwaysBundle(id) { return id === 'foo'; }"), "{actual}"); + } + } + } + + #[test] + fn migrates_constant_external_references_without_warnings() { + for declarations in [ + "const externalOptions = ['foo', './external.js'];", + "const externalOptions: string[] = (['foo']);", + "export const externalOptions = ['foo', './external.js'] as const;", + "const externalOptions = ['foo'] satisfies string[];", + "const patterns = ['foo']; const externalOptions = patterns;", + "const patterns = ['foo'], externalOptions = patterns;", + "const externalOptions = /^virtual:/;", + "const externalOptions = 'foo';", + ] { + for skip in ["skipNodeModulesBundle: true", "deps: { skipNodeModulesBundle: true }"] { + for (standalone, config) in [ + ( + false, + format!( + "export default {{ pack: {{ external: externalOptions, {skip} }} }};" + ), + ), + ( + false, + format!( + "export default defineConfig(() => ({{ pack: {{ external: externalOptions, {skip} }} }}));" + ), + ), + ( + true, + format!( + "export default defineConfig({{ external: externalOptions, {skip} }});" + ), + ), + ] { + let input = format!("{declarations}\n{config}"); + let actual = rewrite_pack_config(&input, standalone); + assert!(actual.starts_with(declarations), "{actual}"); + assert!( + actual.contains("inputOptions: { external: externalOptions }"), + "{actual}" + ); + assert!(actual.contains("neverBundle: true"), "{actual}"); + assert!(!actual.contains("skipNodeModulesBundle"), "{actual}"); + assert!(pack_config_warnings(&actual, standalone).is_empty()); + assert_eq!(rewrite_pack_config(&actual, standalone), actual); + } + } + } + let input = "export default defineConfig(() => { const external = ['foo']; return { pack: { external, skipNodeModulesBundle: true } }; });"; + let actual = rewrite_pack_config(input, false); + assert!(actual.contains("inputOptions: { external: external }"), "{actual}"); + assert!(!actual.contains("skipNodeModulesBundle"), "{actual}"); + assert!(pack_config_warnings(&actual, false).is_empty()); + } + + #[test] + fn does_not_confuse_unknown_or_shadowed_external_references_with_constants() { + for input in [ + "let externalOptions = ['foo']; export default { pack: { external: externalOptions, skipNodeModulesBundle: true } };", + "const externalOptions = getExternal(); export default { pack: { external: externalOptions, skipNodeModulesBundle: true } };", + "const externalOptions = '/foo/'; export default { pack: { external: externalOptions, skipNodeModulesBundle: true } };", + "const externalOptions = other; const other = externalOptions; export default { pack: { external: externalOptions, skipNodeModulesBundle: true } };", + "const externalOptions = ['foo']; export default defineConfig((externalOptions) => ({ pack: { external: externalOptions, skipNodeModulesBundle: true } }));", + "const externalOptions = ['foo']; export default defineConfig(({ externalOptions }) => ({ pack: { external: externalOptions, skipNodeModulesBundle: true } }));", + "const externalOptions = ['foo']; export default defineConfig(() => { let externalOptions = getExternal(); return { pack: { external: externalOptions, skipNodeModulesBundle: true } }; });", + "const externalOptions = ['foo']; export default defineConfig(() => { const externalOptions = '/foo/'; return { pack: { external: externalOptions, skipNodeModulesBundle: true } }; });", + "const externalOptions = ['foo']; export default defineConfig(() => { if (custom) { var externalOptions = getExternal(); } return { pack: { external: externalOptions, skipNodeModulesBundle: true } }; });", + ] { + assert_eq!(rewrite_pack_config(input, false), input); + assert_eq!(pack_config_warnings(input, false), [EXTERNAL_SKIP_WARNING]); + } + } + + #[test] + fn unsafe_external_combinations_stay_unchanged_and_warn() { + for options in [ + "external: dynamicExternal", + "external", + "external: (id) => id === 'foo'", + "external: '/foo/'", + "external: ['foo'], inputOptions: customOptions", + "external: ['foo'], inputOptions: { ...customOptions }", + "external: ['foo'], inputOptions: { external: ['bar'] }", + "external: ['foo'], inputOptions: { external() {} }", + "external: ['foo'], ...otherOptions", + ] { + for skip in ["skipNodeModulesBundle: true", "deps: { skipNodeModulesBundle: true }"] { + let input = format!( + "export default {{ pack: {{ {options}, {skip}, bundle: false, dts: {{ tsgo: true }} }} }};" + ); + assert_eq!(rewrite_pack_config(&input, false), input); + assert_eq!(pack_config_warnings(&input, false), [EXTERNAL_SKIP_WARNING]); + } + } + let input = "export default { pack: { external: ['foo'], deps: { skipNodeModulesBundle: true, dts: { neverBundle: ['types'] } } } };"; + assert_eq!(rewrite_pack_config(input, false), input); + assert_eq!(pack_config_warnings(input, false), [EXTERNAL_SKIP_WARNING]); + for input in [ + "export default { pack: { external: ['foo'], skipNodeModulesBundle: false } };", + "export default { plugins: [plugin({ external, skipNodeModulesBundle: true })] };", + ] { + assert!(pack_config_warnings(input, false).is_empty()); + } + } + + #[test] + fn standalone_concise_arrows_migrate_unbundle_and_generator() { + for input in [ + "export default defineConfig(() => ({ bundle: false, dts: { tsgo: true } }));", + "export default defineConfig(async () => ({ bundle: false, dts: { tsgo: true } }));", + "export default defineConfig(() => ([{ bundle: false, dts: { tsgo: true } }]));", + "export default defineConfig(() => (({ bundle: false, dts: { tsgo: true } }) satisfies UserConfig));", + "export default defineConfig(() => { return { bundle: false, dts: { tsgo: true } }; });", + ] { + let actual = rewrite_pack_config(input, true); + assert!(actual.contains("unbundle: true"), "{actual}"); + assert!(actual.contains("generator: 'tsgo'"), "{actual}"); + assert!(!actual.contains("tsgo: true"), "{actual}"); + assert_eq!(rewrite_pack_config(&actual, true), actual); + } + } + + #[test] + fn renames_method_options_without_changing_bodies() { + let input = "export default defineConfig({ outExtension() { return { js: '.custom.js' }; }, async 'publicDir'() { /* assets */ return ['assets']; } });"; + let actual = rewrite_pack_config(input, true); + assert!(actual.contains("outExtensions() { return { js: '.custom.js' }; }"), "{actual}"); + assert!(actual.contains("async copy() { /* assets */ return ['assets']; }"), "{actual}"); + assert_eq!(rewrite_pack_config(&actual, true), actual); + } + + #[test] + fn removed_options_and_previous_defaults() { + let actual = migrate( + r#"{ + bundle: false, + outExtension: ({ format }) => ({ js: `.${format}.js` }), + publicDir: ['public'], + removeNodeProtocol: true, + injectStyle: false, + inlineOnly: [/^allowed/], + skipNodeModulesBundle: true, + dts: { tsgo: true, cjsReexport: false, sourcemap: true }, + attw: true, + }"#, + ); + for expected in [ + "unbundle: true", + "outExtensions: ({ format })", + "copy: ['public']", + "nodeProtocol: 'strip'", + "inject: false", + "onlyBundle: [/^allowed/]", + "neverBundle: true", + "resolveDepSubpath: true", + "generator: 'tsgo'", + "sourcemap: true", + "profile: 'strict'", + ] { + assert!(actual.contains(expected), "missing {expected}: {actual}"); + } + for removed in [ + "bundle:", + "outExtension:", + "publicDir", + "removeNodeProtocol", + "injectStyle", + "inlineOnly", + "skipNodeModulesBundle", + "cjsReexport", + "tsgo:", + ] { + // unbundle contains bundle as a substring. + assert!(!actual.contains(&format!(" {removed}")), "{actual}"); + } + } + + #[test] + fn comments_inside_removed_properties_remain_valid() { + let actual = + migrate("{ dts: { cjsReexport: // removed option\ntrue, tsgo: /* compiler */ true } }"); + assert!(actual.contains("// removed option\n"), "{actual}"); + assert!(actual.contains("/* compiler */"), "{actual}"); + } + + #[test] + fn nested_define_config_calls_are_not_pack_configs() { + let input = "export default defineConfig({ plugins: [defineConfig({ bundle: false, dts: { tsgo: true } })] });"; + let actual = rewrite_pack_config(input, true); + assert!(actual.contains("plugins: [defineConfig({ bundle: false, dts: { tsgo: true } })]")); + assert_eq!(actual.matches("resolveDepSubpath").count(), 1); + assert_eq!(rewrite_pack_config(&actual, true), actual); + assert_eq!(rewrite_pack_config(input, false), input); + } + + #[test] + fn nested_options_and_explicit_defaults() { + let actual = migrate( + r#"{ + bundle: true, removeNodeProtocol: false, + deps: { onlyAllowBundle: false, skipNodeModulesBundle: true, resolveDepSubpath: false }, + css: { modules: true }, injectStyle: true, + dts: { oxc: true, cjsReexport: true }, + attw: { profile: 'node16', enabled: false }, + }"#, + ); + for expected in [ + "onlyBundle: false", + "neverBundle: true", + "resolveDepSubpath: false", + "modules: true", + "inject: true", + "generator: 'oxc'", + "profile: 'node16'", + "enabled: false", + ] { + assert!(actual.contains(expected), "{actual}"); + } + assert!(!actual.contains("skipNodeModulesBundle")); + assert!(!actual.contains("cjsReexport")); + assert!(!actual.contains("unbundle")); + assert!(!actual.contains("nodeProtocol")); + } + + #[test] + fn preserves_method_conflicts() { + let actual = migrate( + "{ outExtension: extensions, outExtensions() { return {}; }, publicDir: 'public', copy() { return []; }, deps: { onlyBundle() { return false; } }, inlineOnly: false }", + ); + for expected in [ + "outExtension: extensions", + "outExtensions()", + "publicDir: 'public'", + "copy()", + "onlyBundle()", + "inlineOnly: false", + ] { + assert!(actual.contains(expected), "{actual}"); + } + } + + #[test] + fn shorthand_and_comments() { + let actual = migrate( + "{ publicDir, outExtension, inlineOnly, deps: { /* deps */ }, dts: { cjsReexport: true /* keep */ }, /* tail */ }", + ); + for expected in [ + "copy: publicDir", + "outExtensions: outExtension", + "onlyBundle: inlineOnly", + "/* deps */", + "/* keep */", + "/* tail */", + ] { + assert!(actual.contains(expected), "{actual}"); + } + } + + #[test] + fn skips_ambiguous_objects_and_conflicts() { + for options in [ + "{ ...shared, bundle: false }", + "{ [key]: value, bundle: false }", + "{ bundle: false, 'bundle': true }", + "{ ...shared, dts: { tsgo: true } }", + "{ dts: { tsgo: true, tsgo: false }, deps: { resolveDepSubpath: true } }", + ] { + let input = format!("export default {{ pack: {options} }};"); + assert_eq!(rewrite_pack_config(&input, false), input); + } + let actual = migrate( + "{ publicDir: 'old', copy: 'new', injectStyle: true, css: cssOptions, inlineOnly: ['x'], deps: { onlyBundle: ['y'], resolveDepSubpath: false }, dts: { ...dtsOptions, cjsReexport: true }, attw: attwOptions }", + ); + for expected in [ + "publicDir: 'old'", + "copy: 'new'", + "injectStyle: true", + "css: cssOptions", + "inlineOnly: ['x']", + "onlyBundle: ['y']", + "cjsReexport: true", + "attw: attwOptions", + ] { + assert!(actual.contains(expected), "{actual}"); + } + } + + #[test] + fn scope_arrays_callbacks_and_json() { + for (input, standalone) in [ + ("export default defineConfig([{ bundle: false }, { publicDir: 'public' }]);", true), + ("export default defineConfig(() => ({ pack: [{ bundle: false }] }));", false), + ( + "export default defineConfig(async () => { return { pack: { bundle: false } }; });", + false, + ), + ("export default { pack: ({ bundle: false } satisfies PackConfig) };", false), + ("export default { pack: { \"bundle\": false, \"dts\": { \"tsgo\": true } } };", false), + ] { + let actual = rewrite_pack_config(input, standalone); + assert!(!actual.contains("bundle: false"), "{actual}"); + assert!(actual.contains("resolveDepSubpath: true"), "{actual}"); + assert_eq!(rewrite_pack_config(&actual, standalone), actual); + } + for input in [ + "export default { publicDir: 'vite-public', plugins: [plugin({ bundle: false })] };", + "export default { test: { pack: { bundle: false } } };", + "export default defineConfig({ plugins: [{ config() { return { pack: { bundle: false } }; } }] });", + "const config = { bundle: false }; export default config;", + ] { + assert_eq!(rewrite_pack_config(input, false), input); + } + } + + #[test] + fn generator_objects_keep_their_options() { + for (options, generator, expected) in [ + ( + "{ dts: { tsgo: { path: './tsgo' }, oxc: true } }", + "tsgo", + "tsgo: { path: './tsgo' }", + ), + ( + "{ dts: { oxc: { stripInternal: true }, tsgo: false } }", + "oxc", + "oxc: { stripInternal: true }", + ), + ( + "{ dts: { generator: 'tsc', tsgo: { path: './tsgo' }, oxc: true } }", + "tsc", + "tsgo: { path: './tsgo' }", + ), + ] { + let actual = migrate(options); + assert!(actual.contains(&format!("generator: '{generator}'")), "{actual}"); + assert!(actual.contains(expected), "{actual}"); + assert!(!actual.contains("oxc: true"), "{actual}"); + assert!(!actual.contains("tsgo: false"), "{actual}"); + } + } + + #[test] + fn preserve_old_defaults_without_removed_options() { + let actual = migrate("{ entry: 'src/index.ts', attw: { enabled: true } }"); + assert!(actual.contains("resolveDepSubpath: true")); + assert!(actual.contains("profile: 'strict'")); + let actual = migrate("{ deps: { resolveDepSubpath: false }, attw: { enabled: false } }"); + assert!(!actual.contains("'strict'")); + assert!(actual.contains("resolveDepSubpath: false")); + } +} diff --git a/crates/vp_migration/src/package.rs b/crates/vp_migration/src/package.rs index 0039008fa8..59a3ce07ca 100644 --- a/crates/vp_migration/src/package.rs +++ b/crates/vp_migration/src/package.rs @@ -4,8 +4,10 @@ use serde_json::{Map, Value}; use vp_error::Error; use crate::{ - ast_grep, eslint::rewrite_eslint_script, prettier::rewrite_prettier_script, - script_rewrite::rewrite_bunx_commands, + ast_grep, + eslint::rewrite_eslint_script, + prettier::rewrite_prettier_script, + script_rewrite::{rewrite_bunx_commands, rewrite_pack_flags}, }; // Marker to replace "cross-env " before ast-grep processing @@ -29,7 +31,7 @@ fn rewrite_script(script: &str, rules: &[RuleConfig]) -> String { // matches an active rule, then process ordinary commands. let rewritten_bunx = rewrite_bunx_commands(&preprocessed, |inner| ast_grep::apply_loaded_rules(inner, rules)); - let result = ast_grep::apply_loaded_rules(&rewritten_bunx, rules); + let result = rewrite_pack_flags(&ast_grep::apply_loaded_rules(&rewritten_bunx, rules)); // Step 3: Replace cross-env marker back with "cross-env " (only if we replaced it) @@ -108,6 +110,37 @@ pub fn rewrite_scripts(scripts_json: &str, rules_yaml: &str) -> Result vp --version / vp -v (global flags, not dev-specific) --- diff --git a/crates/vp_migration/src/script_rewrite.rs b/crates/vp_migration/src/script_rewrite.rs index 753ed60f4c..6436fcf1bb 100644 --- a/crates/vp_migration/src/script_rewrite.rs +++ b/crates/vp_migration/src/script_rewrite.rs @@ -37,6 +37,67 @@ pub fn rewrite_script(script: &str, config: &ScriptRewriteConfig) -> String { rewrite_direct_script(&rewritten_bunx, config) } +/// Rename the removed tsdown copy flag only in `vp pack` invocations. The +/// command rewrite runs first, so this also covers scripts that used tsdown. +pub(crate) fn rewrite_pack_flags(script: &str) -> String { + let script = rewrite_bunx_commands(script, rewrite_direct_pack_flags); + rewrite_direct_pack_flags(&script) +} + +fn rewrite_direct_pack_flags(script: &str) -> String { + let mut parser = brush_parser::Parser::new( + script.as_bytes(), + &brush_parser::ParserOptions::default(), + &brush_parser::SourceInfo::default(), + ); + let Ok(mut program) = parser.parse_program() else { return script.to_owned() }; + let changed = visit_simple_commands(&mut program, &mut |cmd| { + let words = collect_command_words(cmd); + let start = match words.first().map(|word| word.value.as_str()) { + Some("vp") => 0, + Some("cross-env" | "cross-env-shell") => { + let Some(index) = words.iter().skip(1).position(|word| !word.value.contains('=')) + else { + return false; + }; + index + 1 + } + _ => return false, + }; + if words.get(start).is_none_or(|word| word.value != "vp") + || words.get(start + 1).is_none_or(|word| word.value != "pack") + { + return false; + } + let mut changed = false; + for word in words.iter().skip(start + 2) { + if word.value == "--" { + break; + } + let replacement = if word.value == "--public-dir" { + "--copy".to_owned() + } else if let Some(value) = word.value.strip_prefix("--public-dir=") { + format!("--copy={value}") + } else { + continue; + }; + if let CommandWordPosition::Suffix(index) = word.position + && let Some(suffix) = &mut cmd.suffix + && let ast::CommandPrefixOrSuffixItem::Word(value) = &mut suffix.0[index] + { + value.value = replacement; + changed = true; + } + } + changed + }); + if changed { + collapse_newlines(&normalize_pipe_spacing(&program.to_string())) + } else { + script.to_owned() + } +} + fn rewrite_direct_script(script: &str, config: &ScriptRewriteConfig) -> String { let mut parser = brush_parser::Parser::new( script.as_bytes(), diff --git a/crates/vp_migration/src/vite_config.rs b/crates/vp_migration/src/vite_config.rs index f87f975b05..9953f7458f 100644 --- a/crates/vp_migration/src/vite_config.rs +++ b/crates/vp_migration/src/vite_config.rs @@ -405,7 +405,7 @@ fn wrap_lazy_plugins_content( Ok(MergeResult { content, updated: true, uses_function_callback }) } -fn pair_key_matches(key_node: &Node<'_, D>, config_key: &str) -> bool { +pub(crate) fn pair_key_matches(key_node: &Node<'_, D>, config_key: &str) -> bool { let text = key_node.text(); match key_node.kind().as_ref() { "property_identifier" => text == config_key, @@ -414,7 +414,7 @@ fn pair_key_matches(key_node: &Node<'_, D>, config_key: &str) -> bool { } } -/// Convert the boolean declaration-generator options removed in tsdown 0.23. +/// Make declaration-generator selection explicit for tsdown 0.23. /// Only inspect config objects, leaving plugin options and computed values alone. pub(crate) fn rewrite_pack_dts_generators(content: &str, standalone: bool) -> String { let grep = SupportLang::TypeScript.ast_grep(content); @@ -427,33 +427,31 @@ pub(crate) fn rewrite_pack_dts_generators(content: &str, standalone: bool) -> St continue; } let Some(config) = node.parent() else { continue }; - let config = match config.parent() { - Some(parent) if parent.kind() == "array" => parent, - _ => config, - }; - let is_pack = config.parent().is_some_and(|pair| { - pair.kind() == "pair" - && pair.field("key").is_some_and(|key| pair_key_matches(&key, "pack")) - && pair.parent().is_some_and(|object| is_direct_recognized_config_object(&object)) - }); - if !(is_pack || standalone && is_direct_recognized_config_object(&config)) { + if !crate::pack_config::is_pack_object(&config, standalone) + || !crate::pack_config::can_edit_object(&config) + || crate::pack_config::external_skip_needs_manual_migration(&config) + { continue; } let Some(options) = node.field("value").filter(|value| value.kind() == "object") else { continue; }; + if !crate::pack_config::can_edit_object(&options) { + continue; + } let children: Vec<_> = options.children().collect(); - // A spread, computed key, or non-boolean selector can affect precedence. + // A spread, computed key, or dynamic selector can affect precedence. if children.iter().any(|child| { child.kind() == "spread_element" + || child.kind() == "method_definition" || child.kind() == "shorthand_property_identifier" && matches!(child.text().as_ref(), "tsgo" | "oxc") || child.field("key").is_some_and(|key| key.kind() == "computed_property_name") || (child.field("key").is_some_and(|key| { pair_key_matches(&key, "tsgo") || pair_key_matches(&key, "oxc") - }) && child - .field("value") - .is_some_and(|value| !matches!(value.kind().as_ref(), "true" | "false"))) + }) && child.field("value").is_some_and(|value| { + !matches!(value.kind().as_ref(), "true" | "false" | "object") + })) }) { continue; } @@ -473,13 +471,22 @@ pub(crate) fn rewrite_pack_dts_generators(content: &str, standalone: bool) -> St continue; }; let Some(value) = child.field("value") else { continue }; - if !matches!(value.kind().as_ref(), "true" | "false") { + if !matches!(value.kind().as_ref(), "true" | "false" | "object") { continue; } - if value.kind() == "true" && (generator.is_none() || name == "tsgo") { + if value.kind() != "false" && (generator.is_none() || name == "tsgo") { generator = Some((index, name)); } - boolean_options.push(index); + if value.kind() != "object" { + boolean_options.push(index); + } + } + if !has_generator + && let Some((index, name)) = generator + && children[index].field("value").is_some_and(|value| value.kind() == "object") + { + let start = options.range().start + 1; + edits.push((start..start, format!(" generator: '{name}',"))); } for index in boolean_options { let child = &children[index]; @@ -487,10 +494,13 @@ pub(crate) fn rewrite_pack_dts_generators(content: &str, standalone: bool) -> St && let Some((selected, name)) = generator && selected == index { - edits.push((child.range(), format!("generator: '{name}'"))); + if let (Some(key), Some(value)) = (child.field("key"), child.field("value")) { + edits.push((key.range(), "generator".to_owned())); + edits.push((value.range(), format!("'{name}'"))); + } continue; } - edits.push((child.range(), String::new())); + edits.push((child.range(), crate::pack_config::property_comments(child))); if let Some(comma) = children[index + 1..].iter().find(|n| n.kind() != "comment") && comma.kind() == "," { @@ -587,7 +597,7 @@ static RE_NAMESPACE_LAZY_PLUGINS_IMPORT: LazyLock = /// returns inside nested functions (e.g. an inline plugin's `config()` hook) /// do NOT match, so destructive edits never touch them. Used by transforms /// that rewrite in place (`wrap_lazy_plugins`, `upsert_json_config`). -fn is_direct_recognized_config_object(object_node: &Node<'_, D>) -> bool { +pub(crate) fn is_direct_recognized_config_object(object_node: &Node<'_, D>) -> bool { let Some(parent) = object_node.parent() else { return false }; match parent.kind().as_ref() { "export_statement" => true, @@ -1028,12 +1038,10 @@ mod tests { for input in [ "export default { plugins: [plugin({ dts: { tsgo: true } })] };", "export default { test: { pack: { dts: { tsgo: true } } } };", - "export default { pack: { dts: { tsgo: { path: './tsgo' } } } };", "export default { pack: { dts: { tsgo: enabled } } };", "export default { pack: { dts: { tsgo: enabled, oxc: true } } };", "export default { pack: { dts: { tsgo, oxc: true } } };", "export default { pack: { dts: { tsgo: true, oxc } } };", - "export default { pack: { dts: { tsgo: { path: './tsgo' }, oxc: true } } };", "export default { pack: { dts: { ...options, tsgo: true } } };", "export default { pack: { dts: { [key]: value, tsgo: true } } };", ] { diff --git a/docs/guide/migrate-rules.md b/docs/guide/migrate-rules.md index a7eb4e2689..48efaca562 100644 --- a/docs/guide/migrate-rules.md +++ b/docs/guide/migrate-rules.md @@ -39,6 +39,52 @@ When a default upgrade skips setup actions that would apply, it prints a hint to run `vp migrate --full`. Fresh (non Vite+) projects always run the full migration. +## Pack Configuration + +`vp migrate` updates static `pack` objects in `vite.config.*` and exported +objects in `tsdown.config.*` for [tsdown 0.23](https://github.com/rolldown/tsdown/releases/tag/v0.23.0). +This also runs on existing Vite+ projects without `--full`, including workspace +packages. Arrays and direct objects returned by `defineConfig` callbacks are +supported. JSON tsdown configs receive the same updates after they merge into +`vite.config.ts`. + +| Previous option | Updated option | +| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------- | +| `bundle: false` | `unbundle: true` | +| `bundle: true` | Removed; bundling remains the default | +| `outExtension` | `outExtensions` | +| `publicDir` | `copy` | +| `removeNodeProtocol: true` | `nodeProtocol: 'strip'` | +| `injectStyle` | `css.inject` | +| `inlineOnly` / `deps.onlyAllowBundle` | `deps.onlyBundle` | +| `noExternal` | `deps.alwaysBundle` | +| `skipNodeModulesBundle: true` / `deps.skipNodeModulesBundle: true` | `deps.neverBundle: true` | +| `dts.tsgo` / `dts.oxc` | Select with `dts.generator`; retain generator option objects and remove boolean flags | +| `dts.cjsReexport` | Removed; tsdown generates CJS declarations separately | +| `--public-dir` in `tsdown` or `vp pack` scripts | `--copy` | + +Migration preserves the previous defaults by setting `deps.resolveDepSubpath` +to `true` when absent. Enabled ATTW checks receive `profile: 'strict'` when +no profile is set. Explicit values, including `false`, remain unchanged. + +`noExternal` moves to `deps.alwaysBundle`, preserving matcher expressions, +references, and callback methods. Existing `deps.alwaysBundle` values remain +unchanged. + +When `external` accompanies either `skipNodeModulesBundle` form, static matchers +and references to local constants move to `inputOptions.external` before +`deps.neverBundle` is set. Constant declarations and references stay intact. +This preserves the original matching rules, including external file paths. Unsupported matchers, +conflicting `inputOptions`, and declaration-specific dependency rules leave the +pack object unchanged and produce a manual-migration warning. + +The transform does not evaluate configuration code. Objects with spreads, +computed keys, or duplicate keys, and conflicting old and new options require +manual review. Dynamic boolean selectors remain unchanged. Unrelated Vite and +plugin options remain unchanged. Run `vp pack` after migration to check the +result. Node.js requirements, TypeScript module resolution, and programmatic +`build()` return values require separate review. + ## Dependency Rules What happens to each toolchain dependency, at a glance: diff --git a/ecosystem-ci/patch-project.ts b/ecosystem-ci/patch-project.ts index ab08264943..df86e933fd 100644 --- a/ecosystem-ci/patch-project.ts +++ b/ecosystem-ci/patch-project.ts @@ -105,18 +105,6 @@ if (project === 'vinext') { } await writeFile(workspacePath, patched, 'utf-8'); - // tsdown 0.23 replaces skipNodeModulesBundle with neverBundle. - const cloudflareConfigPath = join(repoRoot, 'packages/cloudflare/vite.config.ts'); - const cloudflareConfig = await readFile(cloudflareConfigPath, 'utf-8'); - const patchedCloudflareConfig = cloudflareConfig.replace( - 'skipNodeModulesBundle: true', - 'neverBundle: true', - ); - if (patchedCloudflareConfig === cloudflareConfig) { - throw new Error(`vinext patch: skipNodeModulesBundle not found in ${cloudflareConfigPath}`); - } - await writeFile(cloudflareConfigPath, patchedCloudflareConfig, 'utf-8'); - // The single in-process `integration` project runs serially and its ISR // revalidation test sits right at the 30s ceiling under CI load (observed // 26.8s on green main runs, 30.0s here) — a borderline timeout, not a real diff --git a/packages/cli/binding/index.d.cts b/packages/cli/binding/index.d.cts index 72642a9d46..8cd94c5e75 100644 --- a/packages/cli/binding/index.d.cts +++ b/packages/cli/binding/index.d.cts @@ -3450,6 +3450,8 @@ export interface BatchRewriteResult { preservedVitestFiles: Array; /** Files that had errors */ errors: Array; + /** Pack configurations that need manual migration */ + warnings: Array; } /** Configuration options passed from JavaScript to Rust. */ diff --git a/packages/cli/binding/src/migration.rs b/packages/cli/binding/src/migration.rs index 4d19833e8c..81b7b14899 100644 --- a/packages/cli/binding/src/migration.rs +++ b/packages/cli/binding/src/migration.rs @@ -201,6 +201,8 @@ pub struct BatchRewriteResult { pub preserved_vitest_files: Vec, /// Files that had errors pub errors: Vec, + /// Pack configurations that need manual migration + pub warnings: Vec, } /// Merge tsdown config into vite config by importing it @@ -310,6 +312,14 @@ pub fn rewrite_imports_in_directory( .iter() .map(|p| p.to_string_lossy().to_string()) .collect(), + warnings: result + .warnings + .iter() + .map(|(p, m)| BatchRewriteError { + path: p.to_string_lossy().to_string(), + message: m.clone(), + }) + .collect(), errors: result .errors .iter() diff --git a/packages/cli/src/migration/migrator/vite-config.ts b/packages/cli/src/migration/migrator/vite-config.ts index 86b8e29c49..7846ad2b87 100644 --- a/packages/cli/src/migration/migrator/vite-config.ts +++ b/packages/cli/src/migration/migrator/vite-config.ts @@ -528,6 +528,10 @@ export function rewriteAllImports( const preserved = result.preservedVitestFiles.length; const errors = result.errors.length; + for (const warning of result.warnings) { + warnMigration(`${displayRelative(warning.path)}: ${warning.message}`, report); + } + if (report) { report.rewrittenImportFileCount += modified; report.preservedUpstreamVitestImportFileCount += preserved; diff --git a/packages/core/build.ts b/packages/core/build.ts index 94b4a2fbef..5b806bf105 100644 --- a/packages/core/build.ts +++ b/packages/core/build.ts @@ -498,81 +498,6 @@ async function bundleTsdown() { await copyFile(join(tsdownSourceDir, 'client.d.ts'), join(projectDir, 'dist/tsdown/client.d.ts')); } -// Ensure a bundled chunk has the given ansis color helpers (e.g. `bold`, `red`). -// Rolldown can inline ansis into the logger chunk or keep it in a shared chunk. -// For the latter layout, add imports for any missing helpers by resolving their -// minified aliases from the shared chunk's own `export { ... }` map. -async function ensureAnsisImports( - content: string, - names: string[], - distDir: string, -): Promise { - // Scan every relative chunk import in the branded logger chunk. Which shared - // chunk holds the ansis colors depends on rolldown's chunking and has moved - // between versions (e.g. `main-*.js` → `ansis-*.js`), so we don't assume a - // fixed chunk name: instead we append each missing color to whichever imported - // chunk actually re-exports it. - const importRe = /import \{([^}]*)\} from "(\.\/[^"]+\.js)";/g; - const imports = [...content.matchAll(importRe)]; - // Every binding already in scope across all imports (its local name). - const localNames = new Set(); - for (const [, bindings] of imports) { - for (const binding of bindings.split(',')) { - const trimmed = binding.trim(); - if (!trimmed) { - continue; - } - const aliased = trimmed.match(/\bas\s+([A-Za-z0-9_$]+)$/); - localNames.add(aliased ? aliased[1] : trimmed); - } - } - // Rolldown can also inline ansis into the logger chunk. Detect its destructured - // declarations so we do not try to import a binding that is already local. - const isLocallyDeclared = (name: string) => - new RegExp(`\\b(?:const|let|var)\\s+(?:${name}\\b|\\{[^}]*\\b${name}\\b)`).test(content); - const missing = names.filter((name) => !localNames.has(name) && !isLocallyDeclared(name)); - if (missing.length === 0) { - return content; - } - if (imports.length === 0) { - throw new Error('ensureAnsisImports: no relative chunk import found in branded logger chunk'); - } - - // Group missing colors by the imported chunk that re-exports them. Chunks - // re-export colors as ` as ` (e.g. `bold as i`); the consumer - // side imports ` as `, so capture the alias here. - const additionsBySpecifier = new Map(); - for (const name of missing) { - let resolved = false; - for (const [, , specifier] of imports) { - const chunkContent = await readFile(join(distDir, specifier.slice(2)), 'utf-8'); - const exportAlias = chunkContent.match(new RegExp(`\\b${name} as ([A-Za-z0-9_$]+)`)); - if (!exportAlias) { - continue; - } - const additions = additionsBySpecifier.get(specifier) ?? []; - additions.push(`${exportAlias[1]} as ${name}`); - additionsBySpecifier.set(specifier, additions); - resolved = true; - break; - } - if (!resolved) { - throw new Error(`ensureAnsisImports: \`${name}\` is not re-exported from any imported chunk`); - } - } - - let result = content; - for (const [fullImport, bindings, specifier] of imports) { - const additions = additionsBySpecifier.get(specifier); - if (!additions) { - continue; - } - const newImport = `import { ${bindings.trim().replace(/,$/, '')}, ${additions.join(', ')} } from "${specifier}";`; - result = result.replace(fullImport, newImport); - } - return result; -} - async function brandTsdown() { const tsdownDistDir = join(projectDir, 'dist/tsdown'); const buildFiles = await glob(toPosixPath(join(tsdownDistDir, 'build-*.js')), { absolute: true }); @@ -636,47 +561,9 @@ async function brandTsdown() { throw new Error('brandTsdown: build error message patterns not found in any build chunk'); } - // `ansisColors` lists the bare ansis identifiers a replacement relies on, so we - // only try to import colors for the patches that actually applied. tsdown 0.23 - // dropped ansis for a `node:util` styleText proxy, and those patches need no - // imports at all (the proxy is declared in the same chunk). - const loggerPatches: { - search: string | RegExp; - replacement: string; - ansisColors?: string[]; - }[] = [ - { - search: 'output("warn", `\\n${bgYellow` WARN `} ${message}\\n`);', - replacement: 'output("warn", `${bold(yellow`warn:`)} ${message}`);', - ansisColors: ['bold', 'red'], - }, - { - search: 'output("warn", `${bgYellow` WARN `} ${message}\\n`);', - replacement: 'output("warn", `${bold(yellow`warn:`)} ${message}`);', - ansisColors: ['bold', 'red'], - }, - { - search: 'output("error", `\\n${bgRed` ERROR `} ${format(msgs)}\\n`);', - replacement: - 'output("error", `${bold(red`error:`)} ${format(msgs).replace(/^([A-Za-z]*Error):\\s*/, "")}`);', - ansisColors: ['bold', 'red'], - }, - { - search: 'output("error", `${bgRed` ERROR `} ${format(msgs)}\\n`);', - replacement: - 'output("error", `${bold(red`error:`)} ${format(msgs).replace(/^([A-Za-z]*Error):\\s*/, "")}`);', - ansisColors: ['bold', 'red'], - }, - { - search: 'output("error", `${bold(red`error:`)} ${format(msgs)}`);', - replacement: - 'output("error", `${bold(red`error:`)} ${format(msgs).replace(/^([A-Za-z]*Error):\\s*/, "")}`);', - ansisColors: ['bold', 'red'], - }, - // tsdown >= 0.23 formats with a `node:util` styleText proxy instead of ansis. - // The proxy binding is local to the logger chunk but rolldown may rename it - // (`styleText` → `styleText$1`) to avoid clashing with the `node:util` - // import, so capture whatever name it ended up with and reuse it. + // The styleText proxy is local to the logger chunk, but Rolldown may rename + // it to avoid a binding conflict. Capture and reuse its emitted name. + const loggerPatches = [ { search: /output\("warn", `\\n\$\{(styleText\$?\w*)\.bgYellow\(" WARN "\)\} \$\{message\}\\n`\);/g, @@ -694,31 +581,16 @@ async function brandTsdown() { for (const candidateFile of loggerCandidateFiles) { let content = await readFile(candidateFile, 'utf-8'); let changed = false; - const neededColors = new Set(); - for (const { search, replacement, ansisColors } of loggerPatches) { - const matched = typeof search === 'string' ? content.includes(search) : search.test(content); - if (!matched) { + for (const { search, replacement } of loggerPatches) { + if (!search.test(content)) { continue; } content = content.replaceAll(search, replacement); - for (const color of ansisColors ?? []) { - neededColors.add(color); - } changed = true; } if (!changed) { continue; } - // The branded ansis logger output uses `bold(...)` and `red` (see - // loggerPatches), but tsdown's logger module only imported the other ansis - // colors it needed (`bgRed`, `bgYellow`, `yellow`, ...). Those identifiers - // only happened to be in scope when rolldown co-located them in this chunk; - // newer chunking splits them out, leaving `bold`/`red` undefined at runtime. - // Ensure the branded chunk imports them from the same shared chunk it - // already pulls colors from. - if (neededColors.size > 0) { - content = await ensureAnsisImports(content, [...neededColors], tsdownDistDir); - } await writeFile(candidateFile, content, 'utf-8'); console.log(`Branded tsdown logger prefixes in ${candidateFile}`); loggerPatched = true;