Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 40 additions & 15 deletions .github/workflows/upgrade-deps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,21 +190,23 @@ jobs:
incorrect source-level fix, fix the source and regenerate until the diff
is either gone or intentionally kept.

### Final validation (this step is complete ONLY when all pass)
1. `just build` exits 0.
2. `pnpm bootstrap-cli:ci && pnpm test:unit` exits 0.
3. `just snapshot-test` exits 0. Snapshot mismatches fail the run with a
unified diff. Re-record with `UPDATE_SNAPSHOTS=1` only for cosmetic
drift from the upgrade (e.g. a bumped version string in help output);
unexpected stack traces, missing output, or diverging CLI behavior are
regressions to fix.
4. `git status --short` and `git diff --stat` have been inspected, and
every changed file is intentional for the upgrade PR. Pay special
attention to generated NAPI binding files, because later CI runs fail
if a build produces uncommitted binding output changes.

If any of the four above fails, diagnose the root cause, fix it, and re-run
the final validation. Do not exit with the task marked complete otherwise.
### Validation during fixups
Rebuild the affected packages and run focused tests for each fix. For
tsdown or declaration-generator changes, run `just snapshot-test
command_pack_dts_compat`, `just snapshot-test command_pack_monorepo`,
and `just snapshot-test migration_from_tsup_monorepo_success`.
A successful internal build does
not prove that `vp pack` works with consumers' TypeScript versions.

Re-record with `UPDATE_SNAPSHOTS=1` only for cosmetic drift from the
upgrade (e.g. a bumped version string in help output). Unexpected stack
traces, missing output, or diverging CLI behavior are regressions to fix.
Inspect `git status --short` and `git diff --stat` before finishing;
every changed file, including generated NAPI bindings, must be intentional.

The workflow's separate Final validation step runs the full build, unit
tests, and snapshot suite after this action. Do not launch the full
snapshot suite here or leave any validation command running in the background.

### Running long commands (IMPORTANT)
Run every long-running command (`just build`, `pnpm bootstrap-cli:ci`,
Expand Down Expand Up @@ -245,6 +247,21 @@ jobs:
continue-on-error: true
run: pnpm fmt

- name: Final validation
id: final-validation
timeout-minutes: 90
# Keep broken upgrades available for review, but only as draft PRs.
continue-on-error: true
shell: bash
env:
RELEASE_BUILD: 'true'
run: |
set -euo pipefail
pnpm bootstrap-cli
export PATH="$HOME/.vite-plus/bin:$PATH"
pnpm test:unit
just snapshot-test

- name: Enhance PR description with Kimi K3
id: enhance-pr-description
continue-on-error: true
Expand Down Expand Up @@ -400,6 +417,7 @@ jobs:
base: main
branch: deps/upstream-update
title: 'feat(deps): upgrade upstream dependencies'
draft: ${{ steps.final-validation.outcome != 'success' }}
sign-commits: true
token: ${{ steps.app-token.outputs.token }}
body: ${{ steps.pr-content.outputs.body }}
Expand All @@ -411,3 +429,10 @@ jobs:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ steps.create-pr.outputs.pull-request-number }}
run: gh pr comment "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body-file "${UPGRADE_DEPS_META_DIR}/cli-help-report.md"

- name: Report validation failure
if: always() && steps.final-validation.outcome != 'success'
shell: bash
run: |
echo '::error::The upgraded dependencies did not pass final validation.'
exit 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "command-pack-dts-compat",
"version": "1.0.0",
"private": true,
"type": "module",
"types": "./dist/index.d.mts",
"devDependencies": {
"typescript": "7.0.2",
"vite-plus": "latest"
},
"packageManager": "pnpm@11.24.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
hoist: false
minimumReleaseAge: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[[case]]
name = "command_pack_dts_typescript_7"
vp = "global"
local-registry = true
comment = "The packed CLI generates inferred declarations with stable TypeScript 7.0.2 and automatic generator selection."
steps = [
{ argv = ["vp", "install", "--ignore-scripts"], snapshot = false },
["vp", "pack"],
["vpt", "print-file", "dist/index.d.mts"],
]

[[case]]
name = "command_pack_dts_native_preview"
vp = "global"
local-registry = true
env = { PACK_DTS_GENERATOR = "tsgo" }
comment = "An explicit tsgo generator uses native-preview with TypeScript 5.9.3. pnpm hoist:false also verifies core declares the optional native-preview peer."
steps = [
{ argv = ["vpt", "json-edit", "package.json", "devDependencies.typescript", "5.9.3"], snapshot = false },
{ argv = ["vpt", "json-edit", "package.json", "devDependencies.@typescript/native-preview", "7.0.0-dev.20260707.2"], snapshot = false },
{ argv = ["vp", "install", "--ignore-scripts"], snapshot = false },
["vp", "pack"],
["vpt", "print-file", "dist/index.d.mts"],
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# command_pack_dts_native_preview

An explicit tsgo generator uses native-preview with TypeScript 5.9.3. pnpm hoist:false also verifies core declares the optional native-preview peer.

## `vpt json-edit package.json devDependencies.typescript 5.9.3`


## `vpt json-edit package.json devDependencies.@typescript/native-preview 7.0.0-dev.20260707.2`


## `vp install --ignore-scripts`


## `vp pack`

```
VITE+ - The Unified Toolchain for the Web

ℹ entry: src/index.ts
ℹ tsconfig: tsconfig.json
ℹ Build start
warn: TypeScript 7.0 does not yet have a stable API and is experimental. Some options will be unavailable.
ℹ Emit types with @typescript/native-preview@7.0.0-dev.20260707.2
ℹ dist/index.mjs <size> kB │ gzip: <size> kB
ℹ dist/index.d.mts <size> kB │ gzip: <size> kB
ℹ 2 files, total: <size> kB
✔ Build complete in <duration>
```

## `vpt print-file dist/index.d.mts`

```
export declare function greet(name: string): {
message: string;
length: number;
};
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# command_pack_dts_typescript_7

The packed CLI generates inferred declarations with stable TypeScript 7.0.2 and automatic generator selection.

## `vp install --ignore-scripts`


## `vp pack`

```
VITE+ - The Unified Toolchain for the Web

ℹ entry: src/index.ts
ℹ tsconfig: tsconfig.json
ℹ Build start
warn: TypeScript 7.0 does not yet have a stable API and is experimental. Some options will be unavailable.
ℹ Emit types with typescript@7.0.2
ℹ dist/index.mjs <size> kB │ gzip: <size> kB
ℹ dist/index.d.mts <size> kB │ gzip: <size> kB
ℹ 2 files, total: <size> kB
✔ Build complete in <duration>
```

## `vpt print-file dist/index.d.mts`

```
export declare function greet(name: string): {
message: string;
length: number;
};
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function format(name: string) {
return `Hello, ${name}!`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { format } from './format.js';

// The declaration generator must infer exported types across module boundaries.
export function greet(name: string) {
return { message: format(name), length: name.length };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"strict": true,
"declaration": true
},
"include": ["src"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'vite-plus';

export default defineConfig({
pack: {
entry: ['src/index.ts'],
dts: process.env.PACK_DTS_GENERATOR === 'tsgo' ? { generator: 'tsgo' } : true,
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,20 @@ steps = [
{ argv = ["vpx", "./src/require-ts-dependency.cts"], comment = "A CommonJS TypeScript dependency loaded with require()" },
]

[[case]]
name = "vpx_script_async_loader_settings"
vp = ["local", "global"]
env = { VP_NODE_VERSION = "22.18.0" }
comment = "Off-thread ESM hooks receive the CommonJS preload's tsconfig selection and TypeScript dependency policy."
steps = [
{ argv = ["vpx", "--tsconfig", "tsconfig.alt.json", "src/resolution.ts"], comment = "The loader thread honors --tsconfig instead of discovering the default config" },
{ argv = ["vpx", "./src/print-setup.ts"], envs = [["TS_NODE_PROJECT", "missing.json"]], comment = "A missing environment-selected config keeps its TS_NODE_PROJECT diagnostic label" },
{ argv = ["vpt", "mkdir", "-p", "node_modules/ts-dep"], snapshot = false },
{ argv = ["vpt", "write-file", "node_modules/ts-dep/package.json", "{ \"name\": \"ts-dep\", \"type\": \"module\", \"exports\": \"./index.ts\" }\n"], snapshot = false },
{ argv = ["vpt", "write-file", "node_modules/ts-dep/index.ts", "export enum Level { High = 'high' }\nexport const level: Level = Level.High;\n"], snapshot = false },
{ argv = ["vpx", "./src/ts-dependency.ts"], comment = "The loader thread transforms published TypeScript dependencies" },
]

[[case]]
name = "vpx_script_tsconfig_transforms"
vp = ["local", "global"]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# vpx_script_async_loader_settings

Off-thread ESM hooks receive the CommonJS preload's tsconfig selection and TypeScript dependency policy.

## `vpx --tsconfig tsconfig.alt.json src/resolution.ts`

The loader thread honors --tsconfig instead of discovering the default config

```
hello vpx from alt
4 6 42 cts counter=1
```

## `TS_NODE_PROJECT=missing.json vpx ./src/print-setup.ts`

A missing environment-selected config keeps its TS_NODE_PROJECT diagnostic label

```
oxc-node: warning: the tsconfig TS_NODE_PROJECT names, <workspace>/missing.json, does not exist; running without a tsconfig
no preload
```

## `vpt mkdir -p node_modules/ts-dep`


## `vpt write-file node_modules/ts-dep/package.json '{ "name": "ts-dep", "type": "module", "exports": "./index.ts" }
'`


## `vpt write-file node_modules/ts-dep/index.ts 'export enum Level { High = '\''high'\'' }
export const level: Level = Level.High;
'`


## `vpx ./src/ts-dependency.ts`

The loader thread transforms published TypeScript dependencies

```
ts-dep: high
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# vpx_script_async_loader_settings

Off-thread ESM hooks receive the CommonJS preload's tsconfig selection and TypeScript dependency policy.

## `vpx --tsconfig tsconfig.alt.json src/resolution.ts`

The loader thread honors --tsconfig instead of discovering the default config

```
hello vpx from alt
4 6 42 cts counter=1
```

## `TS_NODE_PROJECT=missing.json vpx ./src/print-setup.ts`

A missing environment-selected config keeps its TS_NODE_PROJECT diagnostic label

```
oxc-node: warning: the tsconfig TS_NODE_PROJECT names, <workspace>/missing.json, does not exist; running without a tsconfig
no preload
```

## `vpt mkdir -p node_modules/ts-dep`


## `vpt write-file node_modules/ts-dep/package.json '{ "name": "ts-dep", "type": "module", "exports": "./index.ts" }
'`


## `vpt write-file node_modules/ts-dep/index.ts 'export enum Level { High = '\''high'\'' }
export const level: Level = Level.High;
'`


## `vpx ./src/ts-dependency.ts`

The loader thread transforms published TypeScript dependencies

```
ts-dep: high
```
4 changes: 2 additions & 2 deletions packages/cli/BUNDLING.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,8 @@ A binding built by `pnpm build` contains:
`vpx <script>` runs TypeScript through [oxc-node](https://github.com/oxc-project/oxc-node), vendored like Rolldown (see [the RFC](../../rfcs/vpx-script-execution.md)):

- `sync-remote` clones it into `oxc-node/` at the hash in `packages/tools/.upstream-versions.json`; CI checks it out in `.github/actions/clone`.
- The source stays unpatched. The root `Cargo.toml` links it with `default-features = false`, so it registers no NAPI exports and declares no global allocator, and excludes `oxc-node/` from the workspace, so upstream code is not held to workspace lints. `binding/src/oxc_node.rs` forwards the functions the loader uses under an `oxcNode` namespace, so nothing collides with Rolldown's `transform`. `packages/tools/src/patch-oxc-node.ts` only sets oxc-node's `oxc` dependency to the workspace version.
- `src/script-preload.ts` (the `--require` entry), `src/script-register.ts` (the `--import` entry), and `src/script-esm-hooks.ts` are adapted from oxc-node's `register.mjs` and `esm.mjs`. They load the hooks from this binding, pass `vpx --tsconfig` through `setTsconfigPath()`, and resolve oxc-node's runtime helpers from `@oxc-project/runtime`, a `vite-plus` dependency.
- The root `Cargo.toml` links it with `default-features = false`, so it registers no NAPI exports and declares no global allocator, and excludes `oxc-node/` from the workspace, so upstream code is not held to workspace lints. `binding/src/oxc_node.rs` forwards the functions the loader uses under an `oxcNode` namespace, so nothing collides with Rolldown's `transform`. `packages/tools/src/patch-oxc-node.ts` sets oxc-node's `oxc` dependency to the workspace version and exposes its frozen tsconfig APIs to embedders while keeping their NAPI registration disabled.
- `src/script-preload.ts` (the `--require` entry), `src/script-register.ts` (the `--import` entry), and `src/script-esm-hooks.ts` are adapted from oxc-node's `register.mjs` and `esm.mjs`. They load the hooks from this binding, pass `vpx --tsconfig` through `setTsconfigPath()`, and resolve oxc-node's runtime helpers from `@oxc-project/runtime`, a `vite-plus` dependency. Off-thread ESM hooks receive the preload's frozen tsconfig and dependency policy because each thread has a separate loader context.
- oxc-node's `packages/core/watch-report.mjs` and `signal-ack.mjs` (`@oxc-node/core/watch-report` and `/signal-ack`) are imported from the vendored source and bundled into the loader, so watch mode gets the reports in the format the binding records them.
- `src/script-watch.ts` (`dist/script-watch.js`, `vpx --watch`) bundles oxc-node's watcher, `packages/cli/src/watch` (`@oxc-node/cli/watch`), from the vendored source the same way, and runs it with the loader entries next to it. It imports upstream's `watch.d.ts` for types, and a `tsdown.config.ts` plugin swaps in the sources, so type checking never reaches code written against upstream's `@types/node`.
- `pirates`, the CommonJS hook, is bundled into `dist`.
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/binding/index.d.cts
Original file line number Diff line number Diff line change
Expand Up @@ -1669,6 +1669,15 @@ export interface TypeScriptOptions {
rewriteImportExtensions?: 'rewrite' | 'remove' | boolean;
}

/**
* The selected tsconfig and its original source, sent to an asynchronous loader.
* `None` selects discovery explicitly, without consulting the receiving thread's env.
*/
export interface FrozenTsconfig {
path?: string;
source?: string;
}

export interface LoadContext {
/** Export conditions of the relevant `package.json` */
conditions?: Array<string>;
Expand Down Expand Up @@ -4238,6 +4247,14 @@ export declare namespace oxcNode {
) => ResolveFnOutput | Promise<ResolveFnOutput>,
): ResolveFnOutput | Promise<ResolveFnOutput>;
export function enableWatchReport(): void;
export function freezeTransformDeps(): string;
/** Freeze the effective config before registering the off-thread ESM hooks. */
export function freezeTsconfig(cwd: string): FrozenTsconfig;
/** Apply a frozen selection, including discovery without the receiving thread's env. */
export function initializeLoader(
path?: string | undefined | null,
source?: string | undefined | null,
): void;
export function load(
url: string,
context: LoadContext,
Expand Down
Loading
Loading