From 4e9104220ad1f3ce880a1b2bc49a28d941ea024c Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 23 Oct 2026 15:27:32 +0900 Subject: [PATCH 1/7] feat(cli): add public TypeScript preload --- .../action.yml | 1 + .../command_script_import/binding-guard.cjs | 9 + .../command_script_import/package.json | 9 + .../command_script_import/snapshots.toml | 50 ++ .../snapshots/script_import_native_watch.md | 9 + .../script_import_node22_async.global.md | 24 + .../script_import_node22_async.local.md | 24 + .../snapshots/script_import_node22_sync.md | 22 + .../snapshots/script_import_node24_async.md | 22 + .../snapshots/script_import_node24_sync.md | 22 + .../snapshots/script_import_node26_sync.md | 22 + .../script_import_unsupported_node.md | 9 + .../command_script_import/src/argv.ts | 8 + .../command_script_import/src/child.ts | 3 + .../command_script_import/src/children.mjs | 18 + .../command_script_import/src/cluster.mjs | 12 + .../command_script_import/src/common.cts | 5 + .../src/config-fields.cts | 9 + .../src/config-parent-check.mts | 1 + .../src/config-worker.mts | 14 + .../src/config-workers.mjs | 64 +++ .../command_script_import/src/config.ts | 8 + .../command_script_import/src/dedup.mjs | 16 + .../src/dependency-error.ts | 2 + .../command_script_import/src/error.ts | 7 + .../src/internal-host.mjs | 13 + .../src/internal-loader.mjs | 13 + .../src/javascript-view.jsx | 4 + .../command_script_import/src/main.cts | 16 + .../command_script_import/src/main.ts | 30 ++ .../command_script_import/src/module.mts | 2 + .../command_script_import/src/nested.cts | 2 + .../command_script_import/src/remapped.ts | 2 + .../command_script_import/src/setup.ts | 2 + .../command_script_import/src/spawn.mjs | 12 + .../command_script_import/src/value.ts | 2 + .../command_script_import/src/view.tsx | 4 + .../command_script_import/src/worker-host.mjs | 17 + .../command_script_import/src/worker.cts | 4 + .../command_script_import/src/worker.mts | 5 + .../command_script_import/tests/enum.test.ts | 6 + .../command_script_import/tsconfig.json | 15 + .../verify-unsupported.mjs | 15 + .../command_script_import/verify-watch.mjs | 50 ++ .../fixtures/command_script_import/verify.mjs | 170 +++++++ .../migration_tsx_already_vite_plus.md | 6 +- .../snapshots/migration_tsx_kept.md | 6 +- .../snapshots/migration_tsx_scripts.md | 6 +- .../.fixture/tsx/package.json | 4 + .../migration_tsx_preload/package.json | 17 + .../migration_tsx_preload/scripts/seed.ts | 2 + .../migration_tsx_preload/snapshots.toml | 12 + .../snapshots/migration_tsx_preload.md | 94 ++++ .../migration_tsx_preload/tsconfig.json | 5 + docs/guide/migrate-rules.md | 26 +- docs/guide/vpx.md | 38 +- packages/cli/BUNDLING.md | 5 +- packages/cli/binding/index.d.cts | 5 + packages/cli/binding/src/oxc_node.rs | 7 + packages/cli/package.json | 8 +- .../cli/src/migration/__tests__/tsx.spec.ts | 251 +++++++++- packages/cli/src/migration/bin.ts | 2 +- packages/cli/src/migration/migrator/tsx.ts | 200 ++++++-- packages/cli/src/register.ts | 17 + packages/cli/src/script-esm-hooks.ts | 8 +- packages/cli/src/script-hooks.ts | 18 + packages/cli/src/script-preload.ts | 8 +- packages/cli/src/script-register.ts | 10 +- packages/cli/tsdown.config.ts | 2 + .../patches/oxc-node-loader-context.patch | 457 ++++++++++++++++++ .../src/__tests__/patch-oxc-node.spec.ts | 60 ++- packages/tools/src/patch-oxc-node.ts | 34 +- rfcs/script-import-loader.md | 170 +++++++ rfcs/vpx-script-execution.md | 22 +- 74 files changed, 2179 insertions(+), 95 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/binding-guard.cjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_native_watch.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node22_async.global.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node22_async.local.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node22_sync.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node24_async.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node24_sync.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node26_sync.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_unsupported_node.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/argv.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/child.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/children.mjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/cluster.mjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/common.cts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config-fields.cts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config-parent-check.mts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config-worker.mts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config-workers.mjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/dedup.mjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/dependency-error.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/error.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/internal-host.mjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/internal-loader.mjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/javascript-view.jsx create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/main.cts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/main.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/module.mts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/nested.cts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/remapped.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/setup.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/spawn.mjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/value.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/view.tsx create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/worker-host.mjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/worker.cts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/worker.mts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/tests/enum.test.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/tsconfig.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/verify-unsupported.mjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/verify-watch.mjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/verify.mjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/.fixture/tsx/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/scripts/seed.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/snapshots/migration_tsx_preload.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/tsconfig.json create mode 100644 packages/cli/src/register.ts create mode 100644 packages/tools/patches/oxc-node-loader-context.patch create mode 100644 rfcs/script-import-loader.md diff --git a/.github/actions/compute-native-cache-input-hash/action.yml b/.github/actions/compute-native-cache-input-hash/action.yml index 445bb83df5..3769d07e7b 100644 --- a/.github/actions/compute-native-cache-input-hash/action.yml +++ b/.github/actions/compute-native-cache-input-hash/action.yml @@ -31,6 +31,7 @@ runs: hashFiles( 'packages/tools/.upstream-versions.json', 'packages/tools/src/patch-oxc-node.ts', + 'packages/tools/patches/oxc-node-loader-context.patch', 'packages/tools/src/cargo-toml.ts', 'rust-toolchain.toml', 'Cargo.lock', diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/binding-guard.cjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/binding-guard.cjs new file mode 100644 index 0000000000..d954506ca3 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/binding-guard.cjs @@ -0,0 +1,9 @@ +// An unsupported runtime must stop before loading a native binding. +const Module = require('node:module'); +const original = Module._load; +Module._load = function (request, ...args) { + if (/binding[\\/]index\.(?:cjs|js)$/.test(request) || request.endsWith('.node')) { + throw new Error('NATIVE_BINDING_LOADED_BEFORE_VERSION_CHECK'); + } + return original.call(this, request, ...args); +}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/package.json new file mode 100644 index 0000000000..e455992a16 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/package.json @@ -0,0 +1,9 @@ +{ + "name": "command-script-import", + "version": "1.0.0", + "private": true, + "type": "module", + "engines": { + "node": "22.18.0" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots.toml new file mode 100644 index 0000000000..cf0dfb02cf --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots.toml @@ -0,0 +1,50 @@ +[[case]] +name = "script_import_node22_async" +vp = ["local", "global"] +env = { EXPECTED_NODE_VERSION = "22.18.0" } +comment = "The public installed package preload works without tsx on the asynchronous hook path." +steps = [ + ["node", "verify.mjs"], + { argv = ["vpx", "--import=vite-plus/register", "./src/dedup.mjs"], comment = "The CLI's existing flags and the public entry share one registration" }, +] + +[[case]] +name = "script_import_node22_sync" +vp = "global" +env = { VP_NODE_VERSION = "22.23.2", EXPECTED_NODE_VERSION = "22.23.2" } +comment = "The complete preload contract also runs on Node.js 22 synchronous hooks." +steps = [["node", "verify.mjs"], ["vpx", "--import=vite-plus/register", "./src/dedup.mjs"]] + +[[case]] +name = "script_import_node24_async" +vp = "global" +env = { VP_NODE_VERSION = "24.11.0", EXPECTED_NODE_VERSION = "24.11.0" } +comment = "The earliest supported Node.js 24 also uses asynchronous hooks, including workers with independent configs." +steps = [["node", "verify.mjs"], ["vpx", "--import=vite-plus/register", "./src/dedup.mjs"]] + +[[case]] +name = "script_import_node24_sync" +vp = "global" +env = { VP_NODE_VERSION = "24.12.0", EXPECTED_NODE_VERSION = "24.12.0" } +comment = "The complete preload contract runs on Node.js 24 synchronous hooks." +steps = [["node", "verify.mjs"], ["vpx", "--import=vite-plus/register", "./src/dedup.mjs"]] + +[[case]] +name = "script_import_node26_sync" +vp = "global" +env = { VP_NODE_VERSION = "26.5.0", EXPECTED_NODE_VERSION = "26.5.0" } +comment = "The complete preload contract runs on Node.js 26 synchronous hooks." +steps = [["node", "verify.mjs"], ["vpx", "--import=vite-plus/register", "./src/dedup.mjs"]] + +[[case]] +name = "script_import_native_watch" +vp = "global" +comment = "A public preload preserves Node.js native watch mode." +steps = [["node", "verify-watch.mjs"]] + +[[case]] +name = "script_import_unsupported_node" +vp = "global" +env = { VP_NODE_VERSION = "20.19.0" } +comment = "Unsupported Node.js reaches the version diagnostic before loading native code." +steps = [["node", "verify-unsupported.mjs"]] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_native_watch.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_native_watch.md new file mode 100644 index 0000000000..c5692e29a9 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_native_watch.md @@ -0,0 +1,9 @@ +# script_import_native_watch + +A public preload preserves Node.js native watch mode. + +## `node verify-watch.mjs` + +``` +native node --watch reloads an imported TypeScript dependency +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node22_async.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node22_async.global.md new file mode 100644 index 0000000000..4be5269269 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node22_async.global.md @@ -0,0 +1,24 @@ +# script_import_node22_async + +The public installed package preload works without tsx on the asynchronous hook path. + +## `node verify.mjs` + +``` +public package export: both flag forms, resolved URL, NODE_OPTIONS, ESM/CJS +subsequent TypeScript preload, argv, execArgv, PID, runtime, and exit status +eval/stdin imports and requires, dynamic registration, node:test and discovery +fork, ESM/CJS workers, spawned Node.js, and inherited custom tsconfig +worker-specific configs, explicit worker preload, cluster, and internal loader thread +repeated flags, repeated dynamic imports, and NODE_OPTIONS/argv overlap +config paths with spaces, explicit/compat/upstream precedence, unchanged environment +source maps identify TypeScript lines in application and dependency errors +``` + +## `vpx --import=vite-plus/register ./src/dedup.mjs` + +The CLI's existing flags and the public entry share one registration + +``` +registration stays single across repeated and mixed preloads +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node22_async.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node22_async.local.md new file mode 100644 index 0000000000..4be5269269 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node22_async.local.md @@ -0,0 +1,24 @@ +# script_import_node22_async + +The public installed package preload works without tsx on the asynchronous hook path. + +## `node verify.mjs` + +``` +public package export: both flag forms, resolved URL, NODE_OPTIONS, ESM/CJS +subsequent TypeScript preload, argv, execArgv, PID, runtime, and exit status +eval/stdin imports and requires, dynamic registration, node:test and discovery +fork, ESM/CJS workers, spawned Node.js, and inherited custom tsconfig +worker-specific configs, explicit worker preload, cluster, and internal loader thread +repeated flags, repeated dynamic imports, and NODE_OPTIONS/argv overlap +config paths with spaces, explicit/compat/upstream precedence, unchanged environment +source maps identify TypeScript lines in application and dependency errors +``` + +## `vpx --import=vite-plus/register ./src/dedup.mjs` + +The CLI's existing flags and the public entry share one registration + +``` +registration stays single across repeated and mixed preloads +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node22_sync.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node22_sync.md new file mode 100644 index 0000000000..3376185b39 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node22_sync.md @@ -0,0 +1,22 @@ +# script_import_node22_sync + +The complete preload contract also runs on Node.js 22 synchronous hooks. + +## `node verify.mjs` + +``` +public package export: both flag forms, resolved URL, NODE_OPTIONS, ESM/CJS +subsequent TypeScript preload, argv, execArgv, PID, runtime, and exit status +eval/stdin imports and requires, dynamic registration, node:test and discovery +fork, ESM/CJS workers, spawned Node.js, and inherited custom tsconfig +worker-specific configs, explicit worker preload, cluster, and internal loader thread +repeated flags, repeated dynamic imports, and NODE_OPTIONS/argv overlap +config paths with spaces, explicit/compat/upstream precedence, unchanged environment +source maps identify TypeScript lines in application and dependency errors +``` + +## `vpx --import=vite-plus/register ./src/dedup.mjs` + +``` +registration stays single across repeated and mixed preloads +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node24_async.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node24_async.md new file mode 100644 index 0000000000..85b15cdb56 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node24_async.md @@ -0,0 +1,22 @@ +# script_import_node24_async + +The earliest supported Node.js 24 also uses asynchronous hooks, including workers with independent configs. + +## `node verify.mjs` + +``` +public package export: both flag forms, resolved URL, NODE_OPTIONS, ESM/CJS +subsequent TypeScript preload, argv, execArgv, PID, runtime, and exit status +eval/stdin imports and requires, dynamic registration, node:test and discovery +fork, ESM/CJS workers, spawned Node.js, and inherited custom tsconfig +worker-specific configs, explicit worker preload, cluster, and internal loader thread +repeated flags, repeated dynamic imports, and NODE_OPTIONS/argv overlap +config paths with spaces, explicit/compat/upstream precedence, unchanged environment +source maps identify TypeScript lines in application and dependency errors +``` + +## `vpx --import=vite-plus/register ./src/dedup.mjs` + +``` +registration stays single across repeated and mixed preloads +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node24_sync.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node24_sync.md new file mode 100644 index 0000000000..0040e0535a --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node24_sync.md @@ -0,0 +1,22 @@ +# script_import_node24_sync + +The complete preload contract runs on Node.js 24 synchronous hooks. + +## `node verify.mjs` + +``` +public package export: both flag forms, resolved URL, NODE_OPTIONS, ESM/CJS +subsequent TypeScript preload, argv, execArgv, PID, runtime, and exit status +eval/stdin imports and requires, dynamic registration, node:test and discovery +fork, ESM/CJS workers, spawned Node.js, and inherited custom tsconfig +worker-specific configs, explicit worker preload, cluster, and internal loader thread +repeated flags, repeated dynamic imports, and NODE_OPTIONS/argv overlap +config paths with spaces, explicit/compat/upstream precedence, unchanged environment +source maps identify TypeScript lines in application and dependency errors +``` + +## `vpx --import=vite-plus/register ./src/dedup.mjs` + +``` +registration stays single across repeated and mixed preloads +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node26_sync.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node26_sync.md new file mode 100644 index 0000000000..63a305cf68 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_node26_sync.md @@ -0,0 +1,22 @@ +# script_import_node26_sync + +The complete preload contract runs on Node.js 26 synchronous hooks. + +## `node verify.mjs` + +``` +public package export: both flag forms, resolved URL, NODE_OPTIONS, ESM/CJS +subsequent TypeScript preload, argv, execArgv, PID, runtime, and exit status +eval/stdin imports and requires, dynamic registration, node:test and discovery +fork, ESM/CJS workers, spawned Node.js, and inherited custom tsconfig +worker-specific configs, explicit worker preload, cluster, and internal loader thread +repeated flags, repeated dynamic imports, and NODE_OPTIONS/argv overlap +config paths with spaces, explicit/compat/upstream precedence, unchanged environment +source maps identify TypeScript lines in application and dependency errors +``` + +## `vpx --import=vite-plus/register ./src/dedup.mjs` + +``` +registration stays single across repeated and mixed preloads +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_unsupported_node.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_unsupported_node.md new file mode 100644 index 0000000000..d12c274a93 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/snapshots/script_import_unsupported_node.md @@ -0,0 +1,9 @@ +# script_import_unsupported_node + +Unsupported Node.js reaches the version diagnostic before loading native code. + +## `node verify-unsupported.mjs` + +``` +unsupported Node.js fails before binding and application loading +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/argv.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/argv.ts new file mode 100644 index 0000000000..1730258b90 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/argv.ts @@ -0,0 +1,8 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +assert.equal(process.execPath, process.env.EXPECTED_EXEC_PATH); +assert.equal(process.ppid, Number(process.env.EXPECTED_PARENT_PID)); +assert.equal(process.argv[1], path.resolve('src/argv.ts')); +assert.equal((globalThis as any).setup, 'loaded'); +console.log(JSON.stringify({ args: process.argv.slice(2), execArgv: process.execArgv })); +process.exitCode = 7; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/child.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/child.ts new file mode 100644 index 0000000000..cd3b494a81 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/child.ts @@ -0,0 +1,3 @@ +import { value } from '@value'; +enum Result { Value = 'fork' } +process.send!(`${Result.Value}: ${value}`, () => process.disconnect()); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/children.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/children.mjs new file mode 100644 index 0000000000..98d9902850 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/children.mjs @@ -0,0 +1,18 @@ +import assert from 'node:assert/strict'; +import { fork } from 'node:child_process'; +import { once } from 'node:events'; +import { Worker } from 'node:worker_threads'; + +async function collect(child, expected) { + const signal = AbortSignal.timeout(15000); + const exited = once(child, 'exit', { signal }); + const [message] = await once(child, 'message', { signal }); + assert.equal(message, expected); + const [code] = await exited; + assert.equal(code, 0); +} +const expected = process.env.EXPECTED_CONFIG_VALUE ?? 'default'; +await collect(fork(new URL('./child.ts', import.meta.url)), `fork: ${expected}`); +await collect(new Worker(new URL('./worker.mts', import.meta.url)), `esm worker: cts: ${expected}`); +await collect(new Worker(new URL('./worker.cts', import.meta.url)), 'cjs worker: cts'); +console.log('fork and ESM/CJS workers inherit registration'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/cluster.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/cluster.mjs new file mode 100644 index 0000000000..49c179d408 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/cluster.mjs @@ -0,0 +1,12 @@ +import assert from 'node:assert/strict'; +import cluster from 'node:cluster'; +import { once } from 'node:events'; +import { fileURLToPath } from 'node:url'; + +cluster.setupPrimary({ exec: fileURLToPath(new URL('./child.ts', import.meta.url)) }); +const child = cluster.fork(); +const exited = once(child, 'exit'); +const [message] = await once(child, 'message'); +assert.equal(message, 'fork: compat'); +assert.equal((await exited)[0], 0); +console.log('cluster inherits registration and custom tsconfig'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/common.cts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/common.cts new file mode 100644 index 0000000000..16d3520789 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/common.cts @@ -0,0 +1,5 @@ +const assert = require('node:assert/strict'); +enum Format { Value = 'cts' } +const nested: string = require('./nested.cts'); +assert.equal(nested, 'nested'); +module.exports = { format: Format.Value }; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config-fields.cts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config-fields.cts new file mode 100644 index 0000000000..17ef0793bd --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config-fields.cts @@ -0,0 +1,9 @@ +let writes: number = 0; +class Base { + set field(_value: number) { writes += 1; } +} +class Derived extends Base { + field = 1; +} +new Derived(); +module.exports = { writes }; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config-parent-check.mts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config-parent-check.mts new file mode 100644 index 0000000000..e1ac29af16 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config-parent-check.mts @@ -0,0 +1 @@ +export { value } from '@value'; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config-worker.mts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config-worker.mts new file mode 100644 index 0000000000..9ed1135325 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config-worker.mts @@ -0,0 +1,14 @@ +import { once } from 'node:events'; +import { createRequire } from 'node:module'; +import { parentPort } from 'node:worker_threads'; +import { value as initial } from '@value'; + +enum Stage { Ready = 'ready', Result = 'result' } +const run = once(parentPort!, 'message'); +parentPort!.postMessage({ stage: Stage.Ready, initial }); +await run; +// The other worker has registered before either worker resolves or transforms these. +const { value } = await import('./config-parent-check.mts'); +const { writes } = createRequire(import.meta.url)('./config-fields.cts'); +parentPort!.postMessage({ stage: Stage.Result, value, writes }); +parentPort!.close(); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config-workers.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config-workers.mjs new file mode 100644 index 0000000000..d0fbf9f53f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config-workers.mjs @@ -0,0 +1,64 @@ +import assert from 'node:assert/strict'; +import { once } from 'node:events'; +import { createRequire } from 'node:module'; +import { Worker } from 'node:worker_threads'; + +const require = createRequire(import.meta.url); +const selectors = ['VP_SCRIPT_TSCONFIG', 'TSX_TSCONFIG_PATH', 'TS_NODE_PROJECT', 'OXC_TSCONFIG_PATH']; +const original = selectors.map(key => process.env[key]); +const one = 'configs with spaces/worker-one.json'; +const two = 'configs with spaces/worker-two.json'; +let checks = 0; +async function checkParent() { + const { value } = await import(`./config-parent-check.mts?check=${checks++}`); + delete require.cache[require.resolve('./config-fields.cts')]; + assert.equal(value, 'parent'); + assert.equal(require('./config-fields.cts').writes, 0); + assert.deepEqual(selectors.map(key => process.env[key]), original); +} + +async function pair(configurations) { + let readyCount = 0; + let release; + const gate = new Promise(resolve => { release = resolve; }); + const tasks = configurations.map(async ({ env: overrides, value, writes }) => { + const env = { ...process.env }; + for (const key of selectors) delete env[key]; + Object.assign(env, overrides); + const worker = new Worker(new URL('./config-worker.mts', import.meta.url), { env }); + const signal = AbortSignal.timeout(15000); + try { + const [ready] = await once(worker, 'message', { signal }); + assert.deepEqual(ready, { stage: 'ready', initial: value }); + if (++readyCount === configurations.length) release(); + await gate; + const result = once(worker, 'message', { signal }); + worker.postMessage('run'); + assert.deepEqual((await result)[0], { stage: 'result', value, writes }); + } finally { + await worker.terminate(); + } + }); + // Attach rejection handlers immediately, including when a worker fails before the gate. + const completed = Promise.all(tasks); + await Promise.race([gate, completed]); + await Promise.all([completed, checkParent()]); + await checkParent(); +} + +await checkParent(); +await pair([ + { env: { TSX_TSCONFIG_PATH: one }, value: 'worker-one', writes: 1 }, + { env: { TSX_TSCONFIG_PATH: two }, value: 'worker-two', writes: 0 }, +]); +// Worker environments differ from the main thread's OS environment. In particular, +// no missing variable in a worker may fall back to the parent's native getenv(). +await pair([ + { env: { TS_NODE_PROJECT: one, OXC_TSCONFIG_PATH: two }, value: 'worker-one', writes: 1 }, + { env: { OXC_TSCONFIG_PATH: two }, value: 'worker-two', writes: 0 }, +]); +await pair([ + { env: { VP_SCRIPT_TSCONFIG: one, TSX_TSCONFIG_PATH: two }, value: 'worker-one', writes: 1 }, + { env: {}, value: 'default', writes: 0 }, +]); +console.log('concurrent worker configs isolate ESM aliases and CJS transforms from the parent'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config.ts new file mode 100644 index 0000000000..f3911873e4 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/config.ts @@ -0,0 +1,8 @@ +import { value } from '@value'; +console.log(JSON.stringify({ + value, + explicit: process.env.VP_SCRIPT_TSCONFIG ?? null, + compat: process.env.TSX_TSCONFIG_PATH ?? null, + upstream: process.env.OXC_TSCONFIG_PATH ?? null, + tsnode: process.env.TS_NODE_PROJECT ?? null, +})); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/dedup.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/dedup.mjs new file mode 100644 index 0000000000..b60549a2a5 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/dedup.mjs @@ -0,0 +1,16 @@ +import assert from 'node:assert/strict'; +import Module from 'node:module'; + +const transform = Module._extensions['.ts']; +const resolve = Module._resolveFilename; +assert.equal(typeof transform, 'function'); +await import('vite-plus/register'); +// Re-evaluate the public wrapper, while retaining the emitted hook identities. +const again = new URL(import.meta.resolve('vite-plus/register')); +again.searchParams.set('again', '1'); +await import(again.href); +assert.equal(Module._extensions['.ts'], transform); +assert.equal(Module._resolveFilename, resolve); +const { format } = await import('./module.mts'); +assert.equal(format, 'mts'); +console.log('registration stays single across repeated and mixed preloads'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/dependency-error.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/dependency-error.ts new file mode 100644 index 0000000000..0399633fc2 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/dependency-error.ts @@ -0,0 +1,2 @@ +import { fail } from 'fixture-ts-import/error'; +fail(); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/error.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/error.ts new file mode 100644 index 0000000000..fda19bce35 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/error.ts @@ -0,0 +1,7 @@ +enum Failure { Message = 'mapped failure' } +class Broken { + constructor(public message: string) { + throw new Error(message); + } +} +new Broken(Failure.Message); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/internal-host.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/internal-host.mjs new file mode 100644 index 0000000000..3ef8696157 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/internal-host.mjs @@ -0,0 +1,13 @@ +import assert from 'node:assert/strict'; +import { once } from 'node:events'; +import { register } from 'node:module'; +import { MessageChannel } from 'node:worker_threads'; + +const { port1, port2 } = new MessageChannel(); +const message = once(port1, 'message', { signal: AbortSignal.timeout(15000) }); +register(new URL('./internal-loader.mjs', import.meta.url), { + parentURL: import.meta.url, data: { port: port2 }, transferList: [port2], +}); +assert.deepEqual((await message)[0], { internal: true, unchanged: true }); +port1.close(); +console.log('the internal loader thread skips public registration'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/internal-loader.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/internal-loader.mjs new file mode 100644 index 0000000000..4a89bdff16 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/internal-loader.mjs @@ -0,0 +1,13 @@ +import Module from 'node:module'; +import { isInternalThread } from 'node:worker_threads'; + +export async function initialize({ port }) { + const transform = Module._extensions['.ts']; + const resolve = Module._resolveFilename; + await import('vite-plus/register'); + port.postMessage({ + internal: isInternalThread, + unchanged: Module._extensions['.ts'] === transform && Module._resolveFilename === resolve, + }); + port.close(); +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/javascript-view.jsx b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/javascript-view.jsx new file mode 100644 index 0000000000..661fc2ee73 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/javascript-view.jsx @@ -0,0 +1,4 @@ +function h(tag, props, ...children) { + return { tag, children }; +} +export const view =
jsx
; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/main.cts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/main.cts new file mode 100644 index 0000000000..c6da42ddb0 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/main.cts @@ -0,0 +1,16 @@ +const assert = require('node:assert/strict'); +const common = require('./common.cts'); +const dependency: string = require('fixture-ts-require'); +const decorated: string[] = []; +function record(_target: object, key: string) { decorated.push(key); } +class Counter { + field = 2; + constructor(public initial: number) {} + @record + total() { return this.initial + this.field; } +} +assert.deepEqual(common, { format: 'cts' }); +assert.equal(dependency, 'cjs dependency'); +assert.equal(new Counter(3).total(), 5); +assert.deepEqual(decorated, ['total']); +console.log('cjs syntax, nested require, helpers, and dependencies'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/main.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/main.ts new file mode 100644 index 0000000000..5151d9c92a --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/main.ts @@ -0,0 +1,30 @@ +import assert from 'node:assert/strict'; +import { value } from '@value'; +import { value as remapped } from './remapped.js'; +import { format } from './module.mts'; +import common from './common.cts'; +import { view } from './view.tsx'; +import { view as jsx } from './javascript-view.jsx'; +import { kind } from 'fixture-ts-import'; +import dependency from 'fixture-ts-require'; + +namespace Options { export const count = 3; } +const decorated: string[] = []; +function record(_target: object, key: string) { decorated.push(key); } +class Counter { + field = 2; + constructor(public initial: number) {} + @record + total() { return this.initial + this.field; } +} +assert.equal(value, 'default'); +assert.equal(remapped, 'remapped'); +assert.equal(format, 'mts'); +assert.deepEqual(common, { format: 'cts' }); +assert.deepEqual(view, { tag: 'section', children: ['tsx'] }); +assert.deepEqual(jsx, { tag: 'section', children: ['jsx'] }); +assert.equal(kind, 'esm dependency'); +assert.equal(dependency, 'cjs dependency'); +assert.equal(new Counter(Options.count).total(), 5); +assert.deepEqual(decorated, ['total']); +console.log('esm syntax, resolution, JSX, helpers, and dependencies'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/module.mts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/module.mts new file mode 100644 index 0000000000..b619caf0f5 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/module.mts @@ -0,0 +1,2 @@ +enum Format { Value = 'mts' } +export const format: Format = await Promise.resolve(Format.Value); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/nested.cts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/nested.cts new file mode 100644 index 0000000000..f6dc2278ca --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/nested.cts @@ -0,0 +1,2 @@ +enum Value { Current = 'nested' } +module.exports = Value.Current; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/remapped.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/remapped.ts new file mode 100644 index 0000000000..bc2cfa2634 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/remapped.ts @@ -0,0 +1,2 @@ +export enum Value { Current = 'remapped' } +export const value: Value = Value.Current; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/setup.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/setup.ts new file mode 100644 index 0000000000..a16c228f87 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/setup.ts @@ -0,0 +1,2 @@ +enum Setup { Value = 'loaded' } +(globalThis as any).setup = Setup.Value; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/spawn.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/spawn.mjs new file mode 100644 index 0000000000..7e8a3514f3 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/spawn.mjs @@ -0,0 +1,12 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; + +assert.deepEqual(process.execArgv, []); +const child = spawnSync(process.execPath, ['./src/main.ts'], { + encoding: 'utf8', + timeout: 15000, +}); +assert.equal(child.error, undefined); +assert.equal(child.status, 0, child.stderr); +assert.equal(child.stdout.trim(), 'esm syntax, resolution, JSX, helpers, and dependencies'); +console.log('spawn inherits NODE_OPTIONS registration'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/value.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/value.ts new file mode 100644 index 0000000000..97214aaa78 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/value.ts @@ -0,0 +1,2 @@ +export enum Value { Current = 'default' } +export const value: Value = Value.Current; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/view.tsx b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/view.tsx new file mode 100644 index 0000000000..ee2d34fdee --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/view.tsx @@ -0,0 +1,4 @@ +function h(tag: string, props: object | null, ...children: unknown[]) { + return { tag, children }; +} +export const view =
tsx
; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/worker-host.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/worker-host.mjs new file mode 100644 index 0000000000..813469d9bb --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/worker-host.mjs @@ -0,0 +1,17 @@ +import assert from 'node:assert/strict'; +import { once } from 'node:events'; +import { createRequire } from 'node:module'; +import { pathToFileURL } from 'node:url'; +import { Worker } from 'node:worker_threads'; + +const register = pathToFileURL(createRequire(import.meta.url).resolve('vite-plus/register')).href; +const worker = new Worker(new URL('./worker.mts', import.meta.url), { + execArgv: ['--import', register], +}); +try { + const [message] = await once(worker, 'message', { signal: AbortSignal.timeout(15000) }); + assert.equal(message, 'esm worker: cts: default'); + console.log('a plain JavaScript host registers its TypeScript worker'); +} finally { + await worker.terminate(); +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/worker.cts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/worker.cts new file mode 100644 index 0000000000..061206ad74 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/worker.cts @@ -0,0 +1,4 @@ +const { parentPort } = require('node:worker_threads'); +const common = require('./common.cts'); +enum Result { Value = 'cjs worker' } +parentPort.postMessage(`${Result.Value}: ${common.format}`); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/worker.mts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/worker.mts new file mode 100644 index 0000000000..7471baca9b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/src/worker.mts @@ -0,0 +1,5 @@ +import { parentPort } from 'node:worker_threads'; +import common from './common.cts'; +import { value } from '@value'; +enum Result { Value = 'esm worker' } +parentPort!.postMessage(`${Result.Value}: ${common.format}: ${value}`); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/tests/enum.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/tests/enum.test.ts new file mode 100644 index 0000000000..43190dc102 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/tests/enum.test.ts @@ -0,0 +1,6 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +enum Result { Value = 'node:test' } +test('preload reaches the test process', () => { + assert.equal(Result.Value, 'node:test'); +}); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/tsconfig.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/tsconfig.json new file mode 100644 index 0000000000..4805b9707d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "experimentalDecorators": true, + "allowJs": true, + "jsx": "react", + "jsxFactory": "h", + "baseUrl": ".", + "paths": { + "@value": ["./src/value.ts"] + } + }, + "include": ["src", "tests"] +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/verify-unsupported.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/verify-unsupported.mjs new file mode 100644 index 0000000000..a8a32df2ca --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/verify-unsupported.mjs @@ -0,0 +1,15 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; + +assert.equal(Number(process.versions.node.split('.')[0]), 20); +const child = spawnSync(process.execPath, [ + '--require', './binding-guard.cjs', + '--import=vite-plus/register', + '--eval', 'throw new Error("APPLICATION_RAN");', +], { encoding: 'utf8', timeout: 15000 }); +assert.equal(child.error, undefined); +assert.equal(child.status, 1, child.stderr); +assert.equal(child.stdout, ''); +assert.match(child.stderr, /requires Node\.js/); +assert.doesNotMatch(child.stderr, /NATIVE_BINDING_LOADED|APPLICATION_RAN|SyntaxError/); +console.log('unsupported Node.js fails before binding and application loading'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/verify-watch.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/verify-watch.mjs new file mode 100644 index 0000000000..2459b38bc3 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/verify-watch.mjs @@ -0,0 +1,50 @@ +import assert from 'node:assert/strict'; +import { spawn } from 'node:child_process'; +import { once } from 'node:events'; +import { writeFileSync } from 'node:fs'; +import { writeFile, unlink } from 'node:fs/promises'; + +const entry = 'src/watch-entry.ts'; +const dependency = 'src/watch-dependency.ts'; +await writeFile(entry, "import { value } from './watch-dependency.ts'; console.log('watch-value:' + value);\n"); +await writeFile(dependency, "export enum Value { Current = 'first' }\nexport const value = Value.Current;\n"); +const child = spawn(process.execPath, [ + '--watch', '--watch-preserve-output', '--import=vite-plus/register', entry, +], { stdio: ['ignore', 'pipe', 'pipe'] }); +let output = ''; +let edited = false; +const exited = once(child, 'exit'); +let timer; +let rewrite; +try { + await new Promise((resolve, reject) => { + timer = setTimeout(() => reject(new Error(`native watch timed out:\n${output}`)), 20000); + child.once('error', reject); + child.once('exit', (code, signal) => reject(new Error(`native watch exited ${code}/${signal}:\n${output}`))); + child.stderr.on('data', chunk => { output += chunk; }); + child.stdout.on('data', chunk => { + output += chunk; + if (!edited && output.includes('watch-value:first')) { + edited = true; + // The child can print before the parent receives its dependency report. + // Repeat the edit until the real watcher observes it, as the vpx fixture does. + const edit = () => { + try { + writeFileSync(dependency, "export enum Value { Current = 'second' }\nexport const value = Value.Current;\n"); + } catch (error) { reject(error); } + }; + edit(); + rewrite = setInterval(edit, 500); + } + if (output.includes('watch-value:second')) resolve(); + }); + }); + assert.equal(edited, true); + console.log('native node --watch reloads an imported TypeScript dependency'); +} finally { + clearTimeout(timer); + clearInterval(rewrite); + child.kill(); + await exited; + await Promise.all([unlink(entry), unlink(dependency)]); +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/verify.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/verify.mjs new file mode 100644 index 0000000000..84a1036164 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_script_import/verify.mjs @@ -0,0 +1,170 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { mkdirSync, writeFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +if (process.env.EXPECTED_NODE_VERSION) { + assert.equal(process.versions.node, process.env.EXPECTED_NODE_VERSION); +} +const require = createRequire(import.meta.url); +assert.throws(() => require.resolve('tsx'), { code: 'MODULE_NOT_FOUND' }); +const registerPath = require.resolve('vite-plus/register'); +assert.match(registerPath.replaceAll('\\', '/'), /\/dist\/register\.js$/); +assert.equal(import.meta.resolve('vite-plus/register'), pathToFileURL(registerPath).href); + +function write(file, contents) { + mkdirSync(path.dirname(file), { recursive: true }); + writeFileSync(file, contents); +} + +// Local packages exercise installed TypeScript without a network dependency. +write('node_modules/fixture-ts-import/package.json', JSON.stringify({ + name: 'fixture-ts-import', type: 'module', + exports: { '.': './index.ts', './error': './error.ts' }, +})); +write('node_modules/fixture-ts-import/index.ts', + "export enum Kind { Value = 'esm dependency' }\nexport const kind: Kind = Kind.Value;\n"); +write('node_modules/fixture-ts-import/error.ts', + "enum Failure { Value = 'dependency failure' }\nexport function fail() {\n throw new Error(Failure.Value);\n}\n"); +write('node_modules/fixture-ts-require/package.json', JSON.stringify({ + name: 'fixture-ts-require', main: './index.cts', +})); +write('node_modules/fixture-ts-require/index.cts', + "enum Kind { Value = 'cjs dependency' }\nmodule.exports = Kind.Value;\n"); + +const configs = {}; +for (const name of ['explicit', 'compat', 'upstream', 'parent', 'worker-one', 'worker-two']) { + configs[name] = `configs with spaces/${name}.json`; + write(`src/config-${name}.ts`, + `export enum Value { Current = '${name}' }\nexport const value = Value.Current;\n`); + write(configs[name], JSON.stringify({ + extends: '../tsconfig.json', + compilerOptions: { + baseUrl: '..', paths: { '@value': [`./src/config-${name}.ts`] }, + useDefineForClassFields: name !== 'worker-one', + }, + })); +} +write('src/name #%.ts', "enum Value { Current = 'encoded filename' }\nconsole.log(Value.Current);\n"); + +const baseEnv = { ...process.env }; +for (const key of ['NODE_OPTIONS', 'VP_SCRIPT_TSCONFIG', 'TSX_TSCONFIG_PATH', 'OXC_TSCONFIG_PATH', 'TS_NODE_PROJECT']) { + delete baseEnv[key]; +} +function run(args, { env = {}, input, cwd, status = 0 } = {}) { + const child = spawnSync(process.execPath, args, { + env: { ...baseEnv, ...env }, input, cwd, encoding: 'utf8', timeout: 30000, + }); + assert.equal(child.error, undefined, `node ${JSON.stringify(args)}: ${child.error}`); + assert.equal(child.status, status, + `node ${JSON.stringify(args)}\nstdout: ${child.stdout}\nstderr: ${child.stderr}`); + return { out: child.stdout.trim(), err: child.stderr.replaceAll('\\', '/') }; +} + +const esm = 'esm syntax, resolution, JSX, helpers, and dependencies'; +const cjs = 'cjs syntax, nested require, helpers, and dependencies'; +assert.equal(run(['--import=vite-plus/register', './src/main.ts']).out, esm); +assert.equal(run(['--import', 'vite-plus/register', './src/main.cts']).out, cjs); +assert.equal(run(['--import', pathToFileURL(registerPath).href, './src/name #%.ts']).out, 'encoded filename'); +// A file URL is portable; Node also accepts an absolute POSIX path. +if (process.platform !== 'win32') { + assert.equal(run(['--import', registerPath, './src/name #%.ts']).out, 'encoded filename'); +} +assert.equal(run(['./src/main.ts'], { env: { NODE_OPTIONS: '--import=vite-plus/register' } }).out, esm); +assert.equal(run(['./src/main.cts'], { env: { NODE_OPTIONS: '--import vite-plus/register' } }).out, cjs); +console.log('public package export: both flag forms, resolved URL, NODE_OPTIONS, ESM/CJS'); + +const preloads = ['--import=vite-plus/register', '--import', './src/setup.ts']; +const args = ['one', '--', '--flag', 'space argument']; +const argv = run([...preloads, './src/argv.ts', ...args], { + status: 7, + env: { EXPECTED_EXEC_PATH: process.execPath, EXPECTED_PARENT_PID: String(process.pid) }, +}); +assert.deepEqual(JSON.parse(argv.out), { args, execArgv: preloads }); +assert.deepEqual(JSON.parse(run(['--import', './src/setup.ts', './src/argv.ts', ...args], { + status: 7, + env: { + NODE_OPTIONS: '--import=vite-plus/register', + EXPECTED_EXEC_PATH: process.execPath, EXPECTED_PARENT_PID: String(process.pid), + }, +}).out), { args, execArgv: ['--import', './src/setup.ts'] }); +console.log('subsequent TypeScript preload, argv, execArgv, PID, runtime, and exit status'); + +const esmEval = "const { format } = await import('./src/module.mts'); console.log(format);"; +const cjsEval = "console.log(require('./src/common.cts').format);"; +assert.equal(run(['--import=vite-plus/register', '--input-type=module', '--eval', esmEval]).out, 'mts'); +assert.equal(run(['--import=vite-plus/register', '--eval', cjsEval]).out, 'cts'); +assert.equal(run(['--import=vite-plus/register', '--input-type=module', '-'], { input: esmEval }).out, 'mts'); +assert.equal(run(['--import=vite-plus/register', '-'], { input: cjsEval }).out, 'cts'); +assert.equal(run(['--input-type=module', '--eval', + "await import('vite-plus/register'); const { format } = await import('./src/module.mts'); console.log(format);", +]).out, 'mts'); +for (const options of [ + { args: ['./tests/enum.test.ts'] }, + { args: [], cwd: path.resolve('tests') }, +]) { + const test = run(['--import=vite-plus/register', '--test', '--test-reporter=tap', ...options.args], options); + assert.match(test.out, /# tests 1\b/); + assert.match(test.out, /# fail 0\b/); +} +console.log('eval/stdin imports and requires, dynamic registration, node:test and discovery'); + +const children = 'fork and ESM/CJS workers inherit registration'; +assert.equal(run(['--import=vite-plus/register', './src/children.mjs']).out, children); +assert.equal(run(['./src/children.mjs'], { env: { NODE_OPTIONS: '--import=vite-plus/register' } }).out, children); +assert.equal(run(['./src/spawn.mjs'], { env: { NODE_OPTIONS: '--import=vite-plus/register' } }).out, + 'spawn inherits NODE_OPTIONS registration'); +assert.equal(run(['--import=vite-plus/register', './src/children.mjs'], { + env: { TSX_TSCONFIG_PATH: configs.compat, EXPECTED_CONFIG_VALUE: 'compat' }, +}).out, children); +console.log('fork, ESM/CJS workers, spawned Node.js, and inherited custom tsconfig'); + +assert.equal(run(['--import=vite-plus/register', './src/config-workers.mjs'], { + env: { + TSX_TSCONFIG_PATH: configs.parent, + TS_NODE_PROJECT: configs.parent, + OXC_TSCONFIG_PATH: configs.parent, + }, +}).out, 'concurrent worker configs isolate ESM aliases and CJS transforms from the parent'); +assert.equal(run(['./src/worker-host.mjs']).out, 'a plain JavaScript host registers its TypeScript worker'); +assert.equal(run(['--import=vite-plus/register', './src/cluster.mjs'], { + env: { TSX_TSCONFIG_PATH: configs.compat }, +}).out, 'cluster inherits registration and custom tsconfig'); +assert.equal(run(['./src/internal-host.mjs']).out, 'the internal loader thread skips public registration'); +console.log('worker-specific configs, explicit worker preload, cluster, and internal loader thread'); + +const dedup = 'registration stays single across repeated and mixed preloads'; +assert.equal(run(['--import=vite-plus/register', '--import', 'vite-plus/register', './src/dedup.mjs']).out, dedup); +assert.equal(run(['--import=vite-plus/register', './src/dedup.mjs'], { + env: { NODE_OPTIONS: '--import=vite-plus/register' }, +}).out, dedup); +console.log('repeated flags, repeated dynamic imports, and NODE_OPTIONS/argv overlap'); + +for (const [value, env] of [ + ['default', {}], + ['compat', { TSX_TSCONFIG_PATH: configs.compat }], + ['upstream', { OXC_TSCONFIG_PATH: configs.upstream }], + ['upstream', { TS_NODE_PROJECT: configs.upstream }], + ['compat', { TS_NODE_PROJECT: configs.compat, OXC_TSCONFIG_PATH: configs.upstream }], + ['compat', { TSX_TSCONFIG_PATH: configs.compat, OXC_TSCONFIG_PATH: configs.upstream }], + ['compat', { VP_SCRIPT_TSCONFIG: '', TSX_TSCONFIG_PATH: configs.compat }], + ['explicit', { VP_SCRIPT_TSCONFIG: configs.explicit, TSX_TSCONFIG_PATH: configs.compat, + OXC_TSCONFIG_PATH: configs.upstream, TS_NODE_PROJECT: configs.upstream }], +]) { + const actual = JSON.parse(run(['--import=vite-plus/register', './src/config.ts'], { env }).out); + assert.deepEqual(actual, { value, + explicit: env.VP_SCRIPT_TSCONFIG ?? null, compat: env.TSX_TSCONFIG_PATH ?? null, + upstream: env.OXC_TSCONFIG_PATH ?? null, tsnode: env.TS_NODE_PROJECT ?? null, + }); +} +console.log('config paths with spaces, explicit/compat/upstream precedence, unchanged environment'); + +const mapped = run(['--import=vite-plus/register', './src/error.ts'], { status: 1 }); +assert.match(mapped.err, /Error: mapped failure/); +assert.match(mapped.err, /at new Broken \([^\n]*\/src\/error\.ts:4:\d+\)/); +const dependencyMapped = run(['--import=vite-plus/register', './src/dependency-error.ts'], { status: 1 }); +assert.match(dependencyMapped.err, /Error: dependency failure/); +assert.match(dependencyMapped.err, /at fail \([^\n]*\/node_modules\/fixture-ts-import\/error\.ts:3:\d+\)/); +console.log('source maps identify TypeScript lines in application and dependency errors'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx/snapshots/migration_tsx_already_vite_plus.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx/snapshots/migration_tsx_already_vite_plus.md index 924cf81287..c2fa29fee6 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx/snapshots/migration_tsx_already_vite_plus.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx/snapshots/migration_tsx_already_vite_plus.md @@ -29,12 +29,12 @@ VITE+ - The Unified Toolchain for the Web VITE+ - The Unified Toolchain for the Web ▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. -● tsx scripts detected. Auto-migrating to vpx... +● tsx scripts detected. Auto-migrating to Vite+... ◇ Updated . to Vite+ • Node pnpm • Dependencies: vite → -• tsx scripts migrated to `vpx` +• tsx scripts migrated to Vite+ ``` ## `vpt print-file package.json` @@ -49,7 +49,7 @@ scripts run vpx, and the tsx devDependency is gone }, "name": "migration-tsx", "scripts": { - "bench": "vpx ./scripts/bench.ts", + "bench": "node --import vite-plus/register ./scripts/bench.ts", "dev": "vpx --watch --clear-screen=false src/server.ts", "seed": "cross-env NODE_ENV=development vpx scripts/seed.ts", "start": "concurrently \"vpx --watch src/server.ts\" \"vp dev\"" diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx/snapshots/migration_tsx_kept.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx/snapshots/migration_tsx_kept.md index 83ca0a570a..3a1e8331d9 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx/snapshots/migration_tsx_kept.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx/snapshots/migration_tsx_kept.md @@ -15,11 +15,11 @@ scripts vpx cannot run keep tsx, with a warning for each ``` VITE+ - The Unified Toolchain for the Web -● tsx scripts detected. Auto-migrating to vpx... +● tsx scripts detected. Auto-migrating to Vite+... ◇ Migrated . to Vite+ • Node pnpm • 1 config update applied -• tsx scripts migrated to `vpx` +• tsx scripts migrated to Vite+ ! Warnings: - Kept tsx in package.json scripts "calc": `vpx` does not run TypeScript passed to --eval or --print. - Kept tsx in package.json scripts "repl": `vpx` does not start a TypeScript REPL. @@ -39,7 +39,7 @@ the other scripts run vpx, and the tsx devDependency stays }, "name": "migration-tsx", "scripts": { - "bench": "vpx ./scripts/bench.ts", + "bench": "node --import vite-plus/register ./scripts/bench.ts", "calc": "tsx -e 'console.log(1 + 1)'", "dev": "vpx --watch --clear-screen=false src/server.ts", "repl": "tsx", diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx/snapshots/migration_tsx_scripts.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx/snapshots/migration_tsx_scripts.md index 6bbb17a93e..fdcc90f780 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx/snapshots/migration_tsx_scripts.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx/snapshots/migration_tsx_scripts.md @@ -9,11 +9,11 @@ tsx scripts should migrate to vpx automatically ``` VITE+ - The Unified Toolchain for the Web -● tsx scripts detected. Auto-migrating to vpx... +● tsx scripts detected. Auto-migrating to Vite+... ◇ Migrated . to Vite+ • Node pnpm • 1 config update applied -• tsx scripts migrated to `vpx` +• tsx scripts migrated to Vite+ ``` ## `vpt print-file package.json` @@ -27,7 +27,7 @@ scripts run vpx, and the tsx devDependency is gone "scripts": { "dev": "vpx --watch --clear-screen=false src/server.ts", "seed": "cross-env NODE_ENV=development vpx scripts/seed.ts", - "bench": "vpx ./scripts/bench.ts", + "bench": "node --import vite-plus/register ./scripts/bench.ts", "start": "concurrently \"vpx --watch src/server.ts\" \"vp dev\"" }, "devDependencies": { diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/.fixture/tsx/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/.fixture/tsx/package.json new file mode 100644 index 0000000000..e900a90163 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/.fixture/tsx/package.json @@ -0,0 +1,4 @@ +{ + "name": "tsx", + "version": "4.20.0" +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/package.json new file mode 100644 index 0000000000..27eea0adc6 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/package.json @@ -0,0 +1,17 @@ +{ + "name": "migration-tsx-preload", + "type": "module", + "scripts": { + "seed": "node --import=tsx ./scripts/seed.ts", + "eval": "node --import tsx --eval \"import('./scripts/seed.ts')\"", + "test": "node --import tsx --test", + "watch": "node --import=tsx --watch --test ./scripts/seed.ts", + "tool": "cross-env NODE_OPTIONS=\"--trace-warnings --import tsx\" mocha", + "config": "TSX_TSCONFIG_PATH=tsconfig.json node --import tsx ./scripts/seed.ts", + "launcher": "NODE_OPTIONS=--import=tsx concurrently \"node ./scripts/seed.ts\" \"node --test\"" + }, + "devDependencies": { + "tsx": "file:.fixture/tsx", + "vite": "^7.0.0" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/scripts/seed.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/scripts/seed.ts new file mode 100644 index 0000000000..81130916e2 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/scripts/seed.ts @@ -0,0 +1,2 @@ +const message: string = 'TypeScript preload works'; +console.log(message); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/snapshots.toml new file mode 100644 index 0000000000..87daf044f5 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/snapshots.toml @@ -0,0 +1,12 @@ +[[case]] +name = "migration_tsx_preload" +vp = "local" +comment = "Plain tsx preloads keep Node.js and its flags. The local tsx stub keeps installation offline." +steps = [ + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "Migrate preload flags and literal NODE_OPTIONS assignments.", continue-on-failure = true }, + { argv = ["vpt", "print-file", "package.json"], comment = "All commands keep Node.js or the tool launcher, and tsx is removed.", continue-on-failure = true }, + { argv = ["vp", "run", "seed"], comment = "The migrated file command resolves the installed preload.", continue-on-failure = true }, + { argv = ["vp", "run", "eval"], comment = "The migrated eval command keeps native Node.js behavior.", continue-on-failure = true }, + { argv = ["vp", "migrate", "--full", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "A second migration finds no remaining tsx use.", continue-on-failure = true }, + { argv = ["vpt", "print-file", "package.json"], comment = "The scripts are unchanged on the second run.", continue-on-failure = true }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/snapshots/migration_tsx_preload.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/snapshots/migration_tsx_preload.md new file mode 100644 index 0000000000..282df84862 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/snapshots/migration_tsx_preload.md @@ -0,0 +1,94 @@ +# migration_tsx_preload + +Plain tsx preloads keep Node.js and its flags. The local tsx stub keeps installation offline. + +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` + +Migrate preload flags and literal NODE_OPTIONS assignments. + +``` +VITE+ - The Unified Toolchain for the Web + +● tsx scripts detected. Auto-migrating to Vite+... +◇ Migrated . to Vite+ +• Node pnpm +• 1 config update applied +• tsx scripts migrated to Vite+ +``` + +## `vpt print-file package.json` + +All commands keep Node.js or the tool launcher, and tsx is removed. + +``` +{ + "name": "migration-tsx-preload", + "type": "module", + "scripts": { + "seed": "node --import=vite-plus/register ./scripts/seed.ts", + "eval": "node --import vite-plus/register --eval \"import('./scripts/seed.ts')\"", + "test": "node --import vite-plus/register --test", + "watch": "node --import=vite-plus/register --watch --test ./scripts/seed.ts", + "tool": "cross-env NODE_OPTIONS=\"--trace-warnings --import vite-plus/register\" mocha", + "config": "TSX_TSCONFIG_PATH=tsconfig.json node --import vite-plus/register ./scripts/seed.ts", + "launcher": "NODE_OPTIONS=--import=vite-plus/register concurrently \"node ./scripts/seed.ts\" \"node --test\"" + }, + "devDependencies": { + "vite": "catalog:", + "vite-plus": "catalog:" + } +} +``` + +## `vp run seed` + +The migrated file command resolves the installed preload. + +``` +$ node --import=vite-plus/register ./scripts/seed.ts ⊘ cache disabled +TypeScript preload works +``` + +## `vp run eval` + +The migrated eval command keeps native Node.js behavior. + +``` +$ node --import vite-plus/register --eval "import('./scripts/seed.ts')" ⊘ cache disabled +TypeScript preload works +``` + +## `vp migrate --full --no-interactive --no-hooks --no-agent --no-editor` + +A second migration finds no remaining tsx use. + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +This project is already using Vite+! Happy coding! +``` + +## `vpt print-file package.json` + +The scripts are unchanged on the second run. + +``` +{ + "name": "migration-tsx-preload", + "type": "module", + "scripts": { + "seed": "node --import=vite-plus/register ./scripts/seed.ts", + "eval": "node --import vite-plus/register --eval \"import('./scripts/seed.ts')\"", + "test": "node --import vite-plus/register --test", + "watch": "node --import=vite-plus/register --watch --test ./scripts/seed.ts", + "tool": "cross-env NODE_OPTIONS=\"--trace-warnings --import vite-plus/register\" mocha", + "config": "TSX_TSCONFIG_PATH=tsconfig.json node --import vite-plus/register ./scripts/seed.ts", + "launcher": "NODE_OPTIONS=--import=vite-plus/register concurrently \"node ./scripts/seed.ts\" \"node --test\"" + }, + "devDependencies": { + "vite": "catalog:", + "vite-plus": "catalog:" + } +} +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/tsconfig.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/tsconfig.json new file mode 100644 index 0000000000..678ff85dad --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsx_preload/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "target": "ESNext" + } +} diff --git a/docs/guide/migrate-rules.md b/docs/guide/migrate-rules.md index 503c2f401e..bc8f63147c 100644 --- a/docs/guide/migrate-rules.md +++ b/docs/guide/migrate-rules.md @@ -278,21 +278,29 @@ Unrelated `bunx` commands and other package-executor forms remain unchanged. When a `package.json` script, a lint-staged task, or a `#!/usr/bin/env tsx` shebang runs [tsx](https://tsx.is), migration offers to move it to [`vpx`](/guide/vpx), Vite+'s TypeScript script runner. It rewrites scripts and lint-staged tasks in `package.json` and JSON lint-staged files, and `#!/usr/bin/env tsx` shebangs in script files: -| Before | After | -| -------------------------------------------------------------------------- | ------------------------------------------ | -| `tsx src/seed.ts --rows 10` | `vpx src/seed.ts --rows 10` | -| `tsx watch --include "*.json" src/a.ts` | `vpx --watch --include "*.json" src/a.ts` | -| `npx tsx a.ts`, `pnpm exec tsx a.ts`, `pnpm dlx tsx a.ts`, `bunx tsx a.ts` | `vpx a.ts` | -| `node --import tsx ./a.ts` | `vpx ./a.ts` | -| `cross-env NODE_ENV=dev tsx a.ts` | `cross-env NODE_ENV=dev vpx a.ts` | -| `concurrently "tsx watch a.ts" "vp dev"` | `concurrently "vpx --watch a.ts" "vp dev"` | +| Before | After | +| -------------------------------------------------------------------------- | -------------------------------------------------- | +| `tsx src/seed.ts --rows 10` | `vpx src/seed.ts --rows 10` | +| `tsx watch --include "*.json" src/a.ts` | `vpx --watch --include "*.json" src/a.ts` | +| `npx tsx a.ts`, `pnpm exec tsx a.ts`, `pnpm dlx tsx a.ts`, `bunx tsx a.ts` | `vpx a.ts` | +| `node --import tsx ./a.ts` | `node --import vite-plus/register ./a.ts` | +| `node --import=tsx --test` | `node --import=vite-plus/register --test` | +| `NODE_OPTIONS='--import tsx' mocha` | `NODE_OPTIONS='--import vite-plus/register' mocha` | +| `cross-env NODE_ENV=dev tsx a.ts` | `cross-env NODE_ENV=dev vpx a.ts` | +| `concurrently "tsx watch a.ts" "vp dev"` | `concurrently "vpx --watch a.ts" "vp dev"` | `tsx watch` becomes `vpx --watch`, never `vpx watch`, which runs the npm package `watch`. Node.js options and tsx watch options keep their order, and `--no-cache` is dropped. A quoted command is rewritten only for programs that run it, such as `concurrently`, `nodemon --exec`, `sh -c`, and `cross-env-shell`; a quoted argument of other programs, as in `echo "tsx a.ts"`, is data. `vpx` needs the same Node.js versions as Vite+ itself, and migration already upgrades incompatible runtime pins to them (see [Vitest 5](/guide/vitest-v5)). -Migration keeps tsx, with a warning, for a command `vpx` cannot run: `--eval`, `--print`, or `--check`, the REPL, `--test` without files or in watch mode, an entry without a file extension, commands that set `TSX_TSCONFIG_PATH` (pass `--tsconfig` to `vpx` instead) or load tsx through `NODE_OPTIONS`, and commands with pipes, variables, or subshells. It also reads the scripts a command runs, including local `--require` and `--import` modules, and keeps tsx when one does not exist yet, as when an earlier command generates it, and when `vpx` would fail on one: a `require()` of `./a.js` that only tsx resolves to `a.ts`, and, because tsx compiles ES module syntax to CommonJS, a `.cts` file with `import` or `export`, and a file that mixes `import` with `__dirname`, `__filename`, `require()`, or `module.exports` in a package without `"type": "module"`. This check skips globs and scripts after a `cd` or another shell state change, and reads only the files a command names, not the modules they import. It does not look for what else `vpx` handles differently, such as standard decorators, tsconfig `paths` in `require()`, or a tsconfig `module` of `node16` or `nodenext`; `vpx` reports those when the script runs. A package that lists tsx in `dependencies`, `optionalDependencies`, or `peerDependencies` keeps it too, since `vpx` comes from the `vite-plus` devDependency. +The plain `node --import tsx` and `node --import=tsx` forms keep Node.js, its options, wrappers, and arguments. They use the [`vite-plus/register` preload](/guide/vpx#nodejs-preload), so Node.js retains its eval, test discovery, and native watch behavior. Literal `NODE_OPTIONS` assignments also migrate, including assignments through `cross-env` and known command launchers. The preload accepts `TSX_TSCONFIG_PATH` as a fallback. Assignments that also load a legacy tsx hook, or reach a command that still needs tsx, stay unchanged. + +For the tsx executable and legacy loader forms such as `--loader tsx/esm` and `-r tsx/cjs`, migration keeps the existing `vpx` rules. It keeps tsx, with a warning, for `--eval`, `--print`, or `--check`, the REPL, `--test` without files or in watch mode, an entry without a file extension, or commands that load tsx through `NODE_OPTIONS`. Commands with pipes, variables, or subshells also stay unchanged. `vpx` accepts `TSX_TSCONFIG_PATH` as a fallback too; an explicit `--tsconfig` takes priority. + +Both migration paths read the scripts a command names, including local `--require` and `--import` modules. They keep tsx when a script does not exist yet, as when an earlier command generates it, or when the Vite+ loader would fail on it: a `require()` of `./a.js` that only tsx resolves to `a.ts`, a `.cts` file with `import` or `export`, or a file that mixes `import` with `__dirname`, `__filename`, `require()`, or `module.exports` in a package without `"type": "module"`. This check skips globs and scripts after a `cd` or another shell state change, and reads only the files a command names, not the modules they import. It does not look for other compiler differences, such as standard decorators, tsconfig `paths` in `require()`, or a tsconfig `module` of `node16` or `nodenext`; the loader reports those when the script runs. A package that lists tsx in `dependencies`, `optionalDependencies`, or `peerDependencies` keeps it too, since the replacement comes from the `vite-plus` devDependency. Migration removes the `tsx` devDependency only when nothing uses tsx anymore: no kept script, no import of `tsx` or its subpaths such as `tsx/esm/api` (in source, build output, or a `package.json` `imports` alias), no shebang, no tool setting such as `--import=tsx` in `package.json` or a JSON or JavaScript config, and no tsx command in CI workflows, git hooks, or shell scripts. The root's dependency stays while any workspace package still uses tsx, and a package's dependency stays while a nested package under it does. When tsx stays, the warning names what still uses it. +Tool configurations and source files need manual edits. For example, change `nodeArguments: ['--import=tsx']` to `nodeArguments: ['--import=vite-plus/register']`, or Mocha's `"import": "tsx"` setting to `"import": "vite-plus/register"`. Keep tsx if the project uses its programmatic APIs. + See [Coming from tsx](/guide/vpx#coming-from-tsx) for the other differences. ## Continuous Integration Rules diff --git a/docs/guide/vpx.md b/docs/guide/vpx.md index 5f3fdcf4a4..e94a531033 100644 --- a/docs/guide/vpx.md +++ b/docs/guide/vpx.md @@ -103,6 +103,38 @@ Scripts can use `vpx` as their interpreter. Make the file executable and keep it The `vpx` bin of the `vite-plus` package runs scripts on its own, so `"seed": "vpx ./scripts/seed.ts"` works in `package.json` scripts without the global CLI. It runs them on the Node.js that started it, which inside `vp run` and package manager scripts is the project's version. +### Node.js Preload + +If a tool starts Node.js itself, replace `--import=tsx` with `--import=vite-plus/register`: + +```bash +node --import=vite-plus/register ./scripts/seed.ts +node --import vite-plus/register --test ./test/example.test.ts +NODE_OPTIONS='--import=vite-plus/register' node ./scripts/seed.ts +``` + +Install `vite-plus` in the project first. The preload resolves from the project and runs on the Node.js that started the command; it requires the same Node.js versions as Vite+. It provides the same TypeScript transforms, tsconfig resolution, and source maps as `vpx`. + +Tools with a Node.js argument setting can replace `["--import", "tsx"]` with `["--import", "vite-plus/register"]`. For example, an AVA configuration can use: + +```json +{ + "ava": { + "nodeArguments": ["--import=vite-plus/register"] + } +} +``` + +Keep the tool's existing TypeScript file patterns and other settings. A JavaScript launcher can also run `await import('vite-plus/register')` before dynamically importing a TypeScript file. A static TypeScript import in that launcher is resolved before registration, so use `--import` for static imports. + +`fork()` and workers inherit the preload through Node.js arguments. `NODE_OPTIONS` also passes it to spawned Node.js processes. Replacing a child's arguments or environment follows Node.js's normal inheritance rules. + +To select a tsconfig, set `VP_SCRIPT_TSCONFIG=tsconfig.scripts.json`. The existing `TSX_TSCONFIG_PATH` variable also works, with `VP_SCRIPT_TSCONFIG` taking precedence. Otherwise the [usual tsconfig discovery](#tsconfig) applies. Each worker selects its config from its own environment, so workers can use different configs without changing their parent. + +Node.js owns its flags, test discovery, and native watch mode. Eval and stdin can import TypeScript files, but their input text is not transformed. All `--require` preloads run before `--import`: put a TypeScript setup file after the loader as `--import ./setup.ts`. Repeated imports of `vite-plus/register` are harmless, including when a tool already runs under `vpx`. + +Remove the old tsx preload when you add this one. The [compiler differences from tsx](#coming-from-tsx) also apply to the public preload. + ### Watch Mode ```bash @@ -132,7 +164,7 @@ It watches every file the script imports or requires (in worker threads too, on `vpx` reads compiler options from a tsconfig but does not type-check with it. Each file uses the nearest `tsconfig.json` above it that includes it through `files`, `include`, `exclude`, or project `references`, as `tsc` does, and `extends` is followed. Files in `node_modules` use no tsconfig, and neither does a file that no tsconfig includes, so it gets the defaults below. Such a file still resolves `paths` aliases through the nearest `tsconfig.json` above it, as tsx does: with a root tsconfig whose `include` is `["src"]`, a script in `scripts/` can import the same aliases as `src/`. Because the config depends on the file's location, not on the working directory, `vpx` compiles a file the same way wherever it runs from. -`--tsconfig ` applies one config to every file instead, including files that config does not include. Without `--tsconfig`, an `OXC_TSCONFIG_PATH` or `TS_NODE_PROJECT` environment variable does the same. If it names a file that does not exist, or a tsconfig fails to parse, `vpx` prints a warning and runs without that config. +`--tsconfig ` applies one config to every file instead, including files that config does not include. Without `--tsconfig`, `TSX_TSCONFIG_PATH`, `TS_NODE_PROJECT`, or `OXC_TSCONFIG_PATH` can select a config, in that order. If it names a file that does not exist, or a tsconfig fails to parse, `vpx` prints a warning and runs without that config. A file that no tsconfig applies to runs as if its `compilerOptions` were empty: @@ -193,8 +225,8 @@ JSX that depends on a compiler plugin is not supported. `vpx` compiles JSX only - **Node.js version**: `vpx` needs Node.js `^22.18.0 || ^24.11.0 || >=26.0.0`, while tsx also runs on older releases. - **Watch mode**: `tsx watch` becomes `vpx --watch`, which takes tsx watch's options (see [Watch Mode](#watch-mode)). It also reruns the script on tsconfig edits and when a file a failed import looked for is created, which tsx watch misses. - **Eval, REPL, and `--check`**: `vpx --eval`, the REPL, and `vpx --check` do not transform TypeScript. -- **`node --import tsx`**: `vpx` has no equivalent for tools that must start `node` themselves. -- **Other loaders**: remove `--import tsx` from `NODE_OPTIONS`. Two loaders registered together compete for the same files. +- **`node --import tsx`**: replace it with [`node --import vite-plus/register`](#nodejs-preload), including in tool settings and `NODE_OPTIONS`. +- **Other loaders**: replace the tsx preload instead of adding a second loader. Two loaders registered together compete for the same files. ## `vp exec` diff --git a/packages/cli/BUNDLING.md b/packages/cli/BUNDLING.md index 58e854fb7a..d531789801 100644 --- a/packages/cli/BUNDLING.md +++ b/packages/cli/BUNDLING.md @@ -312,8 +312,9 @@ A binding built by `pnpm build` contains: `vpx