From 72b0e7e0d1bdb5548d9ba518b353c61196fdccb8 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 16 Oct 2026 00:29:51 +0800 Subject: [PATCH 1/8] test: stabilize CLI snapshots and registry requests --- .../snapshots.toml | 2 + .../new_vite_monorepo/degit-fixture/LICENSE | 21 ++++++ .../new_vite_monorepo/degit-fixture/README.md | 18 +++++ .../new_vite_monorepo/degit-fixture/cli.mjs | 12 +++ .../new_vite_monorepo/degit-fixture/pack.py | 41 ++++++++++ .../degit-fixture/package.json | 7 ++ .../degit-fixture/template/.gitignore | 4 + .../degit-fixture/template/README.md | 23 ++++++ .../degit-fixture/template/package.json | 39 ++++++++++ .../degit-fixture/template/src/index.ts | 3 + .../template/tests/index.test.ts | 6 ++ .../degit-fixture/template/tsconfig.json | 20 +++++ .../degit-fixture/template/vite.config.ts | 17 +++++ .../new_vite_monorepo/mock-manifest.json | 23 ++++++ .../fixtures/new_vite_monorepo/snapshots.toml | 2 + .../tarballs/degit-0.0.0.tgz | Bin 0 -> 2309 bytes .../new_vite_monorepo_bun/mock-manifest.json | 23 ++++++ .../new_vite_monorepo_bun/snapshots.toml | 2 + .../tarballs/degit-0.0.0.tgz | Bin 0 -> 2309 bytes .../test_v5_optional_packages/snapshots.toml | 2 + .../src/__tests__/local-npm-registry.spec.ts | 51 ++++++++++++- .../src/__tests__/registry-request.spec.ts | 71 ++++++++++++++++++ packages/tools/src/local-npm-registry.ts | 17 +++-- packages/tools/src/registry-request.ts | 18 +++++ 24 files changed, 415 insertions(+), 7 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/LICENSE create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/README.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/cli.mjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.py create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/.gitignore create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/README.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/src/index.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/tests/index.test.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/tsconfig.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/vite.config.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/mock-manifest.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/tarballs/degit-0.0.0.tgz create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/mock-manifest.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/tarballs/degit-0.0.0.tgz create mode 100644 packages/tools/src/__tests__/registry-request.spec.ts create mode 100644 packages/tools/src/registry-request.ts diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_existing_vite_plus/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_existing_vite_plus/snapshots.toml index b8bf12cafe..0c4ad95f34 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_existing_vite_plus/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_existing_vite_plus/snapshots.toml @@ -3,6 +3,8 @@ name = "migration_oxlint_existing_vite_plus" vp = "global" skip-platforms = ["windows"] local-registry = true +# Keep npm network retries inside the 120-second step budget. +env = { npm_config_fetch_timeout = "15000", npm_config_fetch_retry_mintimeout = "1000", npm_config_fetch_retry_maxtimeout = "5000" } unset-env = ["VP_SKIP_INSTALL"] steps = [ { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], snapshot = false }, diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/LICENSE b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/LICENSE new file mode 100644 index 0000000000..8701f9fe69 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2025-PRESENT Kevin Deng (https://github.com/sxzz) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/README.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/README.md new file mode 100644 index 0000000000..397ca8eedf --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/README.md @@ -0,0 +1,18 @@ +# Fixed library template + +The `new_vite_monorepo` and `new_vite_monorepo_bun` cases check project creation +and catalog migration. Their registry overlays replace `degit` with a local +template copy. The wrapper checks the requested repository and arguments. +The real package managers and Vite+ commands still run. Other cases retain +the real `degit` package. + +`template/` contains the `vite-plus/` template from +[`sxzz/tsdown-templates` at `c1d30b48`](https://github.com/sxzz/tsdown-templates/tree/c1d30b48c445cc73d2da92be1db6f7c27cc0110c/vite-plus). +`LICENSE` preserves its MIT license. Test runs never fetch or refresh this copy. + +After an intentional fixture change, regenerate both case tarballs and their +integrity hashes from the repository root: + +```sh +python3 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.py +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/cli.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/cli.mjs new file mode 100644 index 0000000000..7e615cc664 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/cli.mjs @@ -0,0 +1,12 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import { cpSync, existsSync } from 'node:fs'; + +// These cases check monorepo creation and catalog migration. Keep the +// downloaded template fixed while running the real package manager and CLI. +const [source, destination, ...extra] = process.argv.slice(2); +assert.equal(source, 'sxzz/tsdown-templates/vite-plus'); +assert.ok(destination, 'Missing template destination'); +assert.deepEqual(extra, [], 'Unexpected template arguments'); +assert.equal(existsSync(destination), false, 'Template destination already exists'); +cpSync(new URL('./template/', import.meta.url), destination, { recursive: true }); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.py b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.py new file mode 100644 index 0000000000..2fdbbbe195 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.py @@ -0,0 +1,41 @@ +"""Pack the checked-in template without fetching external data.""" + +import base64 +import gzip +import hashlib +import io +import json +from pathlib import Path +import tarfile + +source = Path(__file__).resolve().parent +buffer = io.BytesIO() +with tarfile.open(fileobj=buffer, mode="w") as archive: + files = [source / name for name in ["package.json", "cli.mjs", "LICENSE"]] + files += sorted(path for path in (source / "template").rglob("*") if path.is_file()) + for path in files: + data = path.read_bytes() + info = tarfile.TarInfo("package/" + path.relative_to(source).as_posix()) + info.size = len(data) + info.mode = 0o755 if path.name == "cli.mjs" else 0o644 + archive.addfile(info, io.BytesIO(data)) + +tarball = gzip.compress(buffer.getvalue(), mtime=0) +integrity = "sha512-" + base64.b64encode(hashlib.sha512(tarball).digest()).decode() +package = json.loads((source / "package.json").read_text()) +package["dist"] = { + "tarball": "{REGISTRY}/degit/-/degit-0.0.0.tgz", + "integrity": integrity, +} +manifest = { + "degit": { + "name": "degit", + "dist-tags": {"latest": "0.0.0"}, + "versions": {"0.0.0": package}, + } +} +for name in ["new_vite_monorepo", "new_vite_monorepo_bun"]: + case = source.parent.parent / name + (case / "tarballs").mkdir(exist_ok=True) + (case / "tarballs" / "degit-0.0.0.tgz").write_bytes(tarball) + (case / "mock-manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/package.json new file mode 100644 index 0000000000..528c4eb721 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/package.json @@ -0,0 +1,7 @@ +{ + "name": "degit", + "version": "0.0.0", + "private": true, + "type": "module", + "bin": { "degit": "cli.mjs" } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/.gitignore b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/.gitignore new file mode 100644 index 0000000000..7535211682 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +*.log +.DS_Store diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/README.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/README.md new file mode 100644 index 0000000000..6ae949b2c7 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/README.md @@ -0,0 +1,23 @@ +# vite-plus-starter + +A starter for creating a Vite Plus project. + +## Development + +- Install dependencies: + +```bash +vp install +``` + +- Run the unit tests: + +```bash +vp test +``` + +- Build the library: + +```bash +vp pack +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/package.json new file mode 100644 index 0000000000..cca257edb9 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/package.json @@ -0,0 +1,39 @@ +{ + "name": "vite-plus-starter", + "version": "0.0.0", + "description": "A starter for creating a TypeScript package.", + "homepage": "https://github.com/author/library#readme", + "bugs": { + "url": "https://github.com/author/library/issues" + }, + "license": "MIT", + "author": "Author Name ", + "repository": { + "type": "git", + "url": "git+https://github.com/author/library.git" + }, + "files": [ + "dist" + ], + "type": "module", + "exports": { + ".": "./dist/index.mjs", + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "vp pack", + "dev": "vp pack --watch", + "test": "vp test", + "check": "vp check", + "prepublishOnly": "vp run build" + }, + "devDependencies": { + "@types/node": "^26.1.1", + "bumpp": "^11.1.0", + "typescript": "^7.0.2", + "vite-plus": "^0.2.4" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/src/index.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/src/index.ts new file mode 100644 index 0000000000..476d2c7edd --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/src/index.ts @@ -0,0 +1,3 @@ +export function fn() { + return "Hello, tsdown!"; +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/tests/index.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/tests/index.test.ts new file mode 100644 index 0000000000..29a522ee7e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/tests/index.test.ts @@ -0,0 +1,6 @@ +import { expect, test } from "vite-plus/test"; +import { fn } from "../src/index.ts"; + +test("fn", () => { + expect(fn()).toBe("Hello, tsdown!"); +}); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/tsconfig.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/tsconfig.json new file mode 100644 index 0000000000..ff4adab564 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["es2023"], + "moduleDetection": "force", + "module": "nodenext", + "moduleResolution": "nodenext", + "resolveJsonModule": true, + "types": ["node"], + "strict": true, + "noUnusedLocals": true, + "declaration": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "esModuleInterop": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/vite.config.ts new file mode 100644 index 0000000000..bffbbc1e6a --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + pack: { + dts: { + tsgo: true, + }, + exports: true, + }, + lint: { + options: { + typeAware: true, + typeCheck: true, + }, + }, + fmt: {}, +}); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/mock-manifest.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/mock-manifest.json new file mode 100644 index 0000000000..c682476ed6 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/mock-manifest.json @@ -0,0 +1,23 @@ +{ + "degit": { + "name": "degit", + "dist-tags": { + "latest": "0.0.0" + }, + "versions": { + "0.0.0": { + "name": "degit", + "version": "0.0.0", + "private": true, + "type": "module", + "bin": { + "degit": "cli.mjs" + }, + "dist": { + "tarball": "{REGISTRY}/degit/-/degit-0.0.0.tgz", + "integrity": "sha512-LjOTGOarcX+1htyKXXfwmIzY7NjCICaVUv6bFvvOOknqSQoovSKzj0Kd14kcev38sErykYrhEar4VhrW5NjvTQ==" + } + } + } + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/snapshots.toml index 84a08f56da..690eefa972 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/snapshots.toml @@ -2,6 +2,8 @@ name = "new_vite_monorepo" vp = "global" skip-platforms = ["windows"] +local-registry = true +# mock-manifest.json fixes the remote library input; see degit-fixture/README.md. steps = [ { argv = ["vp", "create", "vite:monorepo", "--no-interactive", "--git", "--editor", "vscode"], comment = "create monorepo with default values", snapshot = false, continue-on-failure = true }, { argv = ["vpt", "list-dir", "vite-plus-monorepo"], comment = "check files created", continue-on-failure = true }, diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/tarballs/degit-0.0.0.tgz b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/tarballs/degit-0.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..cc1383c5f0d804e3d965816b28e074594a6134ac GIT binary patch literal 2309 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 literal 0 HcmV?d00001 diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/mock-manifest.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/mock-manifest.json new file mode 100644 index 0000000000..c682476ed6 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/mock-manifest.json @@ -0,0 +1,23 @@ +{ + "degit": { + "name": "degit", + "dist-tags": { + "latest": "0.0.0" + }, + "versions": { + "0.0.0": { + "name": "degit", + "version": "0.0.0", + "private": true, + "type": "module", + "bin": { + "degit": "cli.mjs" + }, + "dist": { + "tarball": "{REGISTRY}/degit/-/degit-0.0.0.tgz", + "integrity": "sha512-LjOTGOarcX+1htyKXXfwmIzY7NjCICaVUv6bFvvOOknqSQoovSKzj0Kd14kcev38sErykYrhEar4VhrW5NjvTQ==" + } + } + } + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/snapshots.toml index 41c0d7ab8e..7bc1e8cf8b 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/snapshots.toml @@ -2,6 +2,8 @@ name = "new_vite_monorepo_bun" vp = "global" skip-platforms = ["windows"] +local-registry = true +# Reuse the fixed library input documented in ../new_vite_monorepo/degit-fixture/README.md. steps = [ { argv = ["vp", "create", "vite:monorepo", "--no-interactive", "--package-manager", "bun", "--git"], comment = "create monorepo with bun", snapshot = false, continue-on-failure = true }, { argv = ["vpt", "list-dir", "vite-plus-monorepo"], comment = "check files created", continue-on-failure = true }, diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/tarballs/degit-0.0.0.tgz b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/tarballs/degit-0.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..cc1383c5f0d804e3d965816b28e074594a6134ac GIT binary patch literal 2309 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 literal 0 HcmV?d00001 diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_optional_packages/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_optional_packages/snapshots.toml index 3da7851eaa..378fea24c9 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_optional_packages/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_optional_packages/snapshots.toml @@ -3,6 +3,8 @@ name = "environments_temporal_html_and_worker_startup" vp = "global" skip-platforms = ["windows"] local-registry = true +# Keep npm network retries inside the 120-second step budget. +env = { npm_config_fetch_timeout = "15000", npm_config_fetch_retry_mintimeout = "1000", npm_config_fetch_retry_maxtimeout = "5000" } steps = [ { argv = ["npm", "install", "--ignore-scripts", "--no-audit", "--no-fund"], snapshot = false, timeout = 120000 }, { argv = ["vp", "test", "run", "--reporter=default", "--reporter=html", "--maxWorkers=1"], timeout = 120000 }, diff --git a/packages/tools/src/__tests__/local-npm-registry.spec.ts b/packages/tools/src/__tests__/local-npm-registry.spec.ts index dd03810f15..7622950980 100644 --- a/packages/tools/src/__tests__/local-npm-registry.spec.ts +++ b/packages/tools/src/__tests__/local-npm-registry.spec.ts @@ -1,10 +1,59 @@ import { execFileSync } from 'node:child_process'; -import { existsSync } from 'node:fs'; +import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { expect, it } from 'vitest'; +it('serves fixture versions and dist-tags without resolving them from the public registry', () => { + const script = fileURLToPath(new URL('../local-npm-registry.ts', import.meta.url)); + const cwd = mkdtempSync(path.join(tmpdir(), 'vp-registry-versions-')); + const names = ['degit', '@test/template']; + const manifest = Object.fromEntries( + names.map((name) => [ + name, + { + name, + 'dist-tags': { latest: '0.0.0' }, + versions: { + '0.0.0': { + name, + version: '0.0.0', + dist: { tarball: `{REGISTRY}/${name}/-/template.tgz` }, + }, + }, + }, + ]), + ); + try { + writeFileSync(path.join(cwd, 'mock-manifest.json'), JSON.stringify(manifest)); + const probe = ` + const assert = require('node:assert/strict'); + (async () => { + const registry = process.env.NPM_CONFIG_REGISTRY; + for (const name of ${JSON.stringify(names)}) { + const packument = await (await fetch(registry + '/' + encodeURIComponent(name))).json(); + for (const version of ['latest', '0.0.0']) { + const response = await fetch(registry + '/' + encodeURIComponent(name) + '/' + version); + assert.equal(response.status, 200); + const actual = await response.json(); + assert.deepEqual(actual, packument.versions['0.0.0']); + assert.equal(actual.dist.tarball, registry + '/' + name + '/-/template.tgz'); + } + } + })().catch((error) => { console.error(error); process.exitCode = 1; }); + `; + execFileSync(process.execPath, [script, '--', process.execPath, '-e', probe], { + cwd, + timeout: 10_000, + env: { ...process.env, SNAP_LOCAL_VP_PACKAGES_DIR: '' }, + }); + } finally { + rmSync(cwd, { recursive: true, force: true }); + } +}); + it('isolates and cleans the platform temp directory used by bunx on each registry run', () => { const script = fileURLToPath(new URL('../local-npm-registry.ts', import.meta.url)); const probe = ` diff --git a/packages/tools/src/__tests__/registry-request.spec.ts b/packages/tools/src/__tests__/registry-request.spec.ts new file mode 100644 index 0000000000..a274f1c4e9 --- /dev/null +++ b/packages/tools/src/__tests__/registry-request.spec.ts @@ -0,0 +1,71 @@ +import { once } from 'node:events'; +import { createServer, get, type Server } from 'node:http'; +import type { AddressInfo } from 'node:net'; + +import { afterEach, beforeEach, expect, it, vi } from 'vitest'; + +import { setRegistryRequestDeadline } from '../registry-request.ts'; + +let server: Server; +let url: string; + +beforeEach(async () => { + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }); + server = createServer(); + server.listen(0, '127.0.0.1'); + await once(server, 'listening'); + url = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; +}); + +afterEach(async () => { + vi.useRealTimers(); + const closed = once(server, 'close'); + server.close(); + server.closeAllConnections(); + await closed; +}); + +it('aborts an upstream that never sends response headers', async () => { + const received = once(server, 'request'); + const request = get(url); + const failed = once(request, 'error'); + setRegistryRequestDeadline(request, 100); + await received; + + vi.advanceTimersByTime(100); + const [error] = await failed; + expect(error.message).toBe('Registry upstream request timed out after 100ms'); + expect(request.destroyed).toBe(true); +}); + +it('keeps the deadline active while the response body is incomplete', async () => { + server.on('request', (_req, res) => { + res.writeHead(200, { 'content-type': 'application/json' }); + res.write('{'); + }); + const request = get(url); + const response = once(request, 'response'); + const failed = once(request, 'error'); + setRegistryRequestDeadline(request, 100); + const [upstream] = await response; + const bodyFailed = once(upstream, 'error'); + upstream.resume(); + + vi.advanceTimersByTime(100); + const [error] = await failed; + expect(error.message).toContain('timed out'); + await bodyFailed; + expect(upstream.complete).toBe(false); +}); + +it('clears the deadline when a response finishes', async () => { + server.on('request', (_req, res) => res.end('ok')); + const request = get(url, (response) => response.resume()); + const closed = once(request, 'close'); + setRegistryRequestDeadline(request, 100); + await closed; + + expect(vi.getTimerCount()).toBe(0); + vi.advanceTimersByTime(100); + expect(request.errored).toBeNull(); +}); diff --git a/packages/tools/src/local-npm-registry.ts b/packages/tools/src/local-npm-registry.ts index acce4c4eb7..f0a9c644f5 100644 --- a/packages/tools/src/local-npm-registry.ts +++ b/packages/tools/src/local-npm-registry.ts @@ -56,6 +56,7 @@ import { gunzipSync } from 'node:zlib'; import { exitCodeFromClose } from './exit-code.ts'; import { packLocalVitePlusPackages } from './pack-local-vite-plus.ts'; +import { setRegistryRequestDeadline } from './registry-request.ts'; interface PackageManifest { name: string; @@ -317,7 +318,7 @@ function serializeForRegistry(value: unknown, registry: string): string { function fetchUpstreamPackument(name: string): Promise { return new Promise((resolve) => { - httpsGet( + const request = httpsGet( `${UPSTREAM_REGISTRY}/${name.replace('/', '%2F')}`, { headers: { accept: 'application/json', 'accept-encoding': 'gzip' }, agent: upstreamAgent }, (upstream) => { @@ -340,6 +341,7 @@ function fetchUpstreamPackument(name: string): Promise { }); }, ).on('error', () => resolve(null)); + setRegistryRequestDeadline(request); }); } @@ -384,12 +386,12 @@ function resolveLocalVersion(key: string): unknown { } const name = key.slice(0, separator); const specifier = key.slice(separator + 1); - const packument = localPackuments.get(name); + const packument = localPackuments.get(name) ?? (manifest[name] as Packument | undefined); if (!packument) { return undefined; } - const version = packument['dist-tags'][specifier] ?? specifier; - return packument.versions[version]; + const version = packument['dist-tags']?.[specifier] ?? specifier; + return packument.versions?.[version]; } // Stream the upstream response byte-for-byte. Unlike `fetch`, `https` does not @@ -414,9 +416,11 @@ function proxyToUpstream(req: IncomingMessage, res: ServerResponse): void { // Stream errors can fire after headers are already sent (e.g. the upstream // connection resets mid-tarball), so guard against a second writeHead. const fail = (error: Error) => { - if (!res.headersSent) { - res.writeHead(502); + if (res.headersSent) { + res.destroy(error); + return; } + res.writeHead(502); res.end(`proxy error: ${error.message}`); }; // Forward the original method and body: npm-based clients POST to registry @@ -481,6 +485,7 @@ function proxyToUpstream(req: IncomingMessage, res: ServerResponse): void { }, ); upstreamRequest.on('error', fail); + setRegistryRequestDeadline(upstreamRequest); if (bodyless) { upstreamRequest.end(); } else { diff --git a/packages/tools/src/registry-request.ts b/packages/tools/src/registry-request.ts new file mode 100644 index 0000000000..7d1ac3f3ee --- /dev/null +++ b/packages/tools/src/registry-request.ts @@ -0,0 +1,18 @@ +import type { ClientRequest } from 'node:http'; + +// A stalled upstream must release local-package metadata lookups and let package +// managers retry proxied requests before a snapshot step reaches its 120s limit. +const UPSTREAM_REQUEST_TIMEOUT_MS = 15_000; + +export function setRegistryRequestDeadline( + request: ClientRequest, + timeout = UPSTREAM_REQUEST_TIMEOUT_MS, +): void { + // Socket timeouts do not cover DNS, connection setup, or waiting for an agent + // socket. Keep this deadline active until the response body finishes too. + const timer = setTimeout(() => { + request.destroy(new Error(`Registry upstream request timed out after ${timeout}ms`)); + }, timeout); + timer.unref(); + request.once('close', () => clearTimeout(timer)); +} From 03ee671286bcb1eb80593be6ce3c7fede25bc78c Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 16 Oct 2026 00:53:27 +0800 Subject: [PATCH 2/8] test: allow stalled CI downloads to retry --- .github/actions/setup-ci-node/action.yml | 12 +++++++++++- .../vp_cli_snapshots/tests/cli_snapshots/README.md | 4 ++++ crates/vp_cli_snapshots/tests/cli_snapshots/main.rs | 4 ++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/actions/setup-ci-node/action.yml b/.github/actions/setup-ci-node/action.yml index 63d4ef2558..cc10751385 100644 --- a/.github/actions/setup-ci-node/action.yml +++ b/.github/actions/setup-ci-node/action.yml @@ -17,8 +17,18 @@ runs: registry-url: https://registry.npmjs.org package-manager-cache: false - - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - name: Setup pnpm + id: setup-pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 if: runner.os != 'Linux' + continue-on-error: true + + # The bootstrap verifies the target version against registry metadata. + # A stalled or truncated response can fail that verification. Retry the + # complete setup once; both attempts retain the action's signature checks. + - name: Retry pnpm setup + if: runner.os != 'Linux' && steps.setup-pnpm.outcome == 'failure' + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - name: Install dependencies if: runner.os != 'Linux' diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/README.md b/crates/vp_cli_snapshots/tests/cli_snapshots/README.md index 6ce4e4ea36..2a411ec6f9 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/README.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/README.md @@ -282,6 +282,10 @@ the point. `seed-runtime = true` (default) symlinks a provisioned managed Node runtime into the case `VP_HOME` so commands do not download ~50MB per case. +The runner sets `VP_DOWNLOAD_TIMEOUT=20` so runtime and package-manager +downloads can retry within the default 120-second step timeout. Cases that +need a different download limit can override it with `env` or `envs`. + Fixture configs may import bare `vite-plus` and `@voidzero-dev/vite-plus-core`: the runner links the checkout packages into the run root's `node_modules`, where Node's upward walk finds them diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/main.rs b/crates/vp_cli_snapshots/tests/cli_snapshots/main.rs index ecdda27e2d..bb18d0112d 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/main.rs +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/main.rs @@ -844,6 +844,10 @@ fn compose_path_env(tool_dirs: &[PathBuf]) -> OsString { fn baseline_env(case_home: &CaseHome, install: &CaseInstall) -> BTreeMap { let mut env = case_home.base_env(install.path_env.clone()); env.insert("VP_EMIT_MILESTONES".into(), "1".into()); + // Leave room for the initial download and three retries inside the 120s + // step budget. The production 10-minute timeout would let one stalled + // runtime or package-manager transfer consume the whole step. + env.insert("VP_DOWNLOAD_TIMEOUT".into(), "20".into()); // Legacy-runner parity: `vp migrate` fixtures skip real dependency // installs (slow, network-bound). Cases that want real installs unset // this via `unset-env`. From 9439b11d3243889feb3916dc8939521e6c90e0e0 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 16 Oct 2026 01:03:27 +0800 Subject: [PATCH 3/8] test: pack the fixed template with Node.js --- .../new_vite_monorepo/degit-fixture/README.md | 2 +- .../new_vite_monorepo/degit-fixture/pack.mjs | 63 +++++++++++++++++++ .../new_vite_monorepo/degit-fixture/pack.py | 41 ------------ 3 files changed, 64 insertions(+), 42 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.mjs delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.py diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/README.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/README.md index 397ca8eedf..4d7e1da8e7 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/README.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/README.md @@ -14,5 +14,5 @@ After an intentional fixture change, regenerate both case tarballs and their integrity hashes from the repository root: ```sh -python3 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.py +node crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.mjs ``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.mjs new file mode 100644 index 0000000000..b7eba2702b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.mjs @@ -0,0 +1,63 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'; +import { join, sep } from 'node:path'; +import { gzipSync } from 'node:zlib'; + +// Pack the checked-in template without dependencies or external data. +const source = import.meta.dirname; +const files = [ + 'package.json', + 'cli.mjs', + 'LICENSE', + ...readdirSync(join(source, 'template'), { recursive: true }) + .map((name) => `template/${name.split(sep).join('/')}`) + .filter((name) => statSync(join(source, name)).isFile()) + .sort(), +]; + +const blocks = []; +for (const name of files) { + const body = readFileSync(join(source, name)); + const archiveName = `package/${name}`; + assert(Buffer.byteLength(archiveName) <= 100, `Archive path is too long: ${archiveName}`); + + // Write a ustar header with fixed ownership and timestamps. + const header = Buffer.alloc(512); + header.write(archiveName); + header.write(name === 'cli.mjs' ? '0000755\0' : '0000644\0', 100); + header.write('0000000\0', 108); + header.write('0000000\0', 116); + header.write(body.length.toString(8).padStart(11, '0') + '\0', 124); + header.write('00000000000\0', 136); + header.fill(' ', 148, 156); + header.write('0', 156); + header.write('ustar', 257); + header.write('00', 263); + const checksum = header.reduce((sum, byte) => sum + byte, 0); + header.write(checksum.toString(8).padStart(6, '0') + '\0 ', 148); + blocks.push(header, body, Buffer.alloc((512 - (body.length % 512)) % 512)); +} + +// End with two empty blocks and pad to tar's standard 20-block record size. +const archive = Buffer.concat([...blocks, Buffer.alloc(1024)]); +const padding = Buffer.alloc((10240 - (archive.length % 10240)) % 10240); +const tarball = gzipSync(Buffer.concat([archive, padding]), { level: 9 }); +// Omit the host OS from the gzip header to keep the hash platform-independent. +tarball[9] = 255; +const integrity = `sha512-${createHash('sha512').update(tarball).digest('base64')}`; +const pkg = JSON.parse(readFileSync(join(source, 'package.json'), 'utf8')); +pkg.dist = { tarball: '{REGISTRY}/degit/-/degit-0.0.0.tgz', integrity }; +const manifest = { + degit: { + name: 'degit', + 'dist-tags': { latest: '0.0.0' }, + versions: { '0.0.0': pkg }, + }, +}; +for (const name of ['new_vite_monorepo', 'new_vite_monorepo_bun']) { + const fixture = join(source, '..', '..', name); + mkdirSync(join(fixture, 'tarballs'), { recursive: true }); + writeFileSync(join(fixture, 'tarballs', 'degit-0.0.0.tgz'), tarball); + writeFileSync(join(fixture, 'mock-manifest.json'), JSON.stringify(manifest, null, 2) + '\n'); +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.py b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.py deleted file mode 100644 index 2fdbbbe195..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Pack the checked-in template without fetching external data.""" - -import base64 -import gzip -import hashlib -import io -import json -from pathlib import Path -import tarfile - -source = Path(__file__).resolve().parent -buffer = io.BytesIO() -with tarfile.open(fileobj=buffer, mode="w") as archive: - files = [source / name for name in ["package.json", "cli.mjs", "LICENSE"]] - files += sorted(path for path in (source / "template").rglob("*") if path.is_file()) - for path in files: - data = path.read_bytes() - info = tarfile.TarInfo("package/" + path.relative_to(source).as_posix()) - info.size = len(data) - info.mode = 0o755 if path.name == "cli.mjs" else 0o644 - archive.addfile(info, io.BytesIO(data)) - -tarball = gzip.compress(buffer.getvalue(), mtime=0) -integrity = "sha512-" + base64.b64encode(hashlib.sha512(tarball).digest()).decode() -package = json.loads((source / "package.json").read_text()) -package["dist"] = { - "tarball": "{REGISTRY}/degit/-/degit-0.0.0.tgz", - "integrity": integrity, -} -manifest = { - "degit": { - "name": "degit", - "dist-tags": {"latest": "0.0.0"}, - "versions": {"0.0.0": package}, - } -} -for name in ["new_vite_monorepo", "new_vite_monorepo_bun"]: - case = source.parent.parent / name - (case / "tarballs").mkdir(exist_ok=True) - (case / "tarballs" / "degit-0.0.0.tgz").write_bytes(tarball) - (case / "mock-manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") From 3cc7b56fea463f33607f46c0b22ef3563490124a Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 16 Oct 2026 01:13:03 +0800 Subject: [PATCH 4/8] fix: retry inactive HTTP streams without limiting active downloads --- .../tests/cli_snapshots/README.md | 4 - .../tests/cli_snapshots/main.rs | 4 - crates/vp_shared/Cargo.toml | 2 +- crates/vp_shared/src/http.rs | 73 ++++++++++++++++++- docs/guide/global-cli.md | 1 + 5 files changed, 73 insertions(+), 11 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/README.md b/crates/vp_cli_snapshots/tests/cli_snapshots/README.md index 2a411ec6f9..6ce4e4ea36 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/README.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/README.md @@ -282,10 +282,6 @@ the point. `seed-runtime = true` (default) symlinks a provisioned managed Node runtime into the case `VP_HOME` so commands do not download ~50MB per case. -The runner sets `VP_DOWNLOAD_TIMEOUT=20` so runtime and package-manager -downloads can retry within the default 120-second step timeout. Cases that -need a different download limit can override it with `env` or `envs`. - Fixture configs may import bare `vite-plus` and `@voidzero-dev/vite-plus-core`: the runner links the checkout packages into the run root's `node_modules`, where Node's upward walk finds them diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/main.rs b/crates/vp_cli_snapshots/tests/cli_snapshots/main.rs index bb18d0112d..ecdda27e2d 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/main.rs +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/main.rs @@ -844,10 +844,6 @@ fn compose_path_env(tool_dirs: &[PathBuf]) -> OsString { fn baseline_env(case_home: &CaseHome, install: &CaseInstall) -> BTreeMap { let mut env = case_home.base_env(install.path_env.clone()); env.insert("VP_EMIT_MILESTONES".into(), "1".into()); - // Leave room for the initial download and three retries inside the 120s - // step budget. The production 10-minute timeout would let one stalled - // runtime or package-manager transfer consume the whole step. - env.insert("VP_DOWNLOAD_TIMEOUT".into(), "20".into()); // Legacy-runner parity: `vp migrate` fixtures skip real dependency // installs (slow, network-bound). Cases that want real installs unset // this via `unset-env`. diff --git a/crates/vp_shared/Cargo.toml b/crates/vp_shared/Cargo.toml index bfaef1910f..ecb0f5d7b7 100644 --- a/crates/vp_shared/Cargo.toml +++ b/crates/vp_shared/Cargo.toml @@ -46,7 +46,7 @@ webpki-root-certs = { workspace = true } indicatif = { workspace = true, features = ["in_memory"] } serial_test = { workspace = true } tempfile = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt"] } +tokio = { workspace = true, features = ["io-util", "macros", "net", "rt", "time"] } # Enables the `test-utils` feature for this crate's own tests and doctests # (a package's dev-dependencies are active when it is the tested target). vp_shared = { path = ".", features = ["test-utils"] } diff --git a/crates/vp_shared/src/http.rs b/crates/vp_shared/src/http.rs index 6b9c30faff..8be2646d28 100644 --- a/crates/vp_shared/src/http.rs +++ b/crates/vp_shared/src/http.rs @@ -72,6 +72,11 @@ pub const fn request_timeout() -> Duration { /// retries (multiple minutes). const CONNECT_TIMEOUT: Duration = Duration::from_secs(30); +/// Maximum wait for the next response bytes. This restarts after each read, +/// so slow downloads can use their full total budget while stalled streams +/// fail soon enough for callers to retry. +const READ_TIMEOUT: Duration = Duration::from_secs(20); + /// Default per-request timeout for large file downloads (Node.js runtimes, /// package-manager tarballs). Those archives are tens of megabytes, so on /// slow or flaky connections the shared [`REQUEST_TIMEOUT`] aborts an @@ -219,8 +224,10 @@ fn build_with( insecure: bool, follow_redirects: bool, ) -> Result { - let mut builder = - reqwest::Client::builder().timeout(REQUEST_TIMEOUT).connect_timeout(CONNECT_TIMEOUT); + let mut builder = reqwest::Client::builder() + .timeout(REQUEST_TIMEOUT) + .connect_timeout(CONNECT_TIMEOUT) + .read_timeout(READ_TIMEOUT); if !extra_certs.is_empty() { builder = builder.tls_certs_merge(extra_certs); } @@ -294,6 +301,68 @@ mod tests { use super::*; + /// Send a body one byte at a time to distinguish idle and total timeouts. + async fn serve_slow_body( + chunks: usize, + interval: Duration, + ) -> (String, tokio::task::JoinHandle<()>) { + use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpListener, + }; + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let url = format!("http://{}/", listener.local_addr().unwrap()); + let server = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + let mut request = Vec::new(); + let mut buffer = [0; 1024]; + while !request.windows(4).any(|bytes| bytes == b"\r\n\r\n") { + let count = stream.read(&mut buffer).await.unwrap(); + assert_ne!(count, 0); + request.extend_from_slice(&buffer[..count]); + } + stream + .write_all( + format!("HTTP/1.1 200 OK\r\nContent-Length: {chunks}\r\n\r\n").as_bytes(), + ) + .await + .unwrap(); + for index in 0..chunks { + if index > 0 { + tokio::time::sleep(interval).await; + } + stream.write_all(b"x").await.unwrap(); + } + }); + (url, server) + } + + #[tokio::test] + async fn stalled_body_times_out_before_the_total_download_budget() { + crate::ensure_tls_provider(); + let client = build_with(Vec::new(), false, false).unwrap(); + let (url, server) = serve_slow_body(2, READ_TIMEOUT * 3).await; + let response = client.get(url).timeout(DEFAULT_DOWNLOAD_TIMEOUT).send().await.unwrap(); + let result = tokio::time::timeout(READ_TIMEOUT * 2, response.bytes()).await; + server.abort(); + + let error = result.expect("a stalled body must hit the read timeout").unwrap_err(); + assert!(error.is_timeout(), "{error}"); + } + + #[tokio::test] + async fn progressing_body_can_take_longer_than_the_read_timeout() { + crate::ensure_tls_provider(); + let client = build_with(Vec::new(), false, true).unwrap(); + let (url, server) = serve_slow_body(4, READ_TIMEOUT / 2).await; + let response = client.get(url).timeout(DEFAULT_DOWNLOAD_TIMEOUT).send().await.unwrap(); + let result = tokio::time::timeout(READ_TIMEOUT * 3, response.bytes()).await; + server.abort(); + + assert_eq!(result.unwrap().unwrap().as_ref(), b"xxxx"); + } + /// Valid PEM, invalid DER: `from_pem_bundle` accepts it, `build` rejects /// it. Portable way into the build-failure branch, unlike an empty trust /// store. diff --git a/docs/guide/global-cli.md b/docs/guide/global-cli.md index a89a91a0f5..53813e041e 100644 --- a/docs/guide/global-cli.md +++ b/docs/guide/global-cli.md @@ -255,6 +255,7 @@ These variables configure the installed Vite+ CLI. `VP_HOME` (above) also applie - **Purpose**: Per-request timeout, in seconds, for large downloads such as Node.js runtimes and package-manager tarballs - **Values**: Positive integer, at most `86400` (24 hours); invalid values are ignored with a warning - **Default**: `600` (10 minutes) +- **Inactivity limit**: Requests also time out after 20 seconds without response data. Each read resets this limit, so active downloads can use the full total timeout. - **Example**: ```bash # Allow up to 30 minutes per download on a slow connection From 0b687f474c261e992d38deba9ab3cd7b57b04b35 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 16 Oct 2026 01:35:17 +0800 Subject: [PATCH 5/8] ci: run Linux jobs on GitHub-hosted runners --- .github/workflows/ci.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fecace7384..cdfac7e291 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,7 +61,7 @@ jobs: # them (code changes get `vp check` in cli-e2e-test). The required `done` # job waits for this check and propagates failures. if: needs.detect-changes.outputs.code-changed == 'false' - runs-on: namespace-profile-linux-x64-default + runs-on: ubuntu-latest permissions: contents: read packages: read @@ -113,7 +113,7 @@ jobs: fail-fast: false matrix: include: - - os: namespace-profile-linux-x64-default + - os: ubuntu-latest target: x86_64-unknown-linux-gnu - os: namespace-profile-mac-default target: aarch64-apple-darwin @@ -144,7 +144,7 @@ jobs: needs: detect-changes if: needs.detect-changes.outputs.code-changed == 'true' name: Build Windows tests - runs-on: namespace-profile-linux-x64-default + runs-on: ubuntu-latest env: XWIN_ACCEPT_LICENSE: '1' # The MSVC STL from xwin's VS17 manifest static-asserts a minimum Clang @@ -269,7 +269,7 @@ jobs: needs: detect-changes if: needs.detect-changes.outputs.code-changed == 'true' name: Test (Linux x64 musl) - runs-on: namespace-profile-linux-x64-default + runs-on: ubuntu-latest container: image: node:25-alpine3.21 env: @@ -321,7 +321,7 @@ jobs: needs: detect-changes if: needs.detect-changes.outputs.code-changed == 'true' name: Lint - runs-on: namespace-profile-linux-x64-default + runs-on: ubuntu-latest steps: - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 - uses: ./.github/actions/clone @@ -373,7 +373,7 @@ jobs: needs: detect-changes if: needs.detect-changes.outputs.code-changed == 'true' name: Build Windows CLI binaries - runs-on: namespace-profile-linux-x64-default + runs-on: ubuntu-latest steps: - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 - uses: ./.github/actions/clone @@ -392,7 +392,7 @@ jobs: fail-fast: false matrix: include: - - os: namespace-profile-linux-x64-default + - os: ubuntu-latest target: x86_64-unknown-linux-gnu test-node: 22.18.0 - os: namespace-profile-mac-default @@ -485,7 +485,7 @@ jobs: RUST_MIN_STACK: 8388608 - name: Test global package install (powershell) - if: ${{ matrix.os != 'namespace-profile-linux-x64-default' }} + if: ${{ runner.os != 'Linux' }} shell: pwsh run: | $vpHome = Join-Path $HOME ".vite-plus" @@ -667,7 +667,7 @@ jobs: fi - name: Test upgrade (powershell) - if: ${{ matrix.os != 'namespace-profile-linux-x64-default' }} + if: ${{ runner.os != 'Linux' }} shell: pwsh run: | $vpHome = Join-Path $HOME ".vite-plus" @@ -883,7 +883,7 @@ jobs: vp --version - name: Test implode (powershell) - if: ${{ matrix.os != 'namespace-profile-linux-x64-default' }} + if: ${{ runner.os != 'Linux' }} shell: pwsh run: | $vpHome = Join-Path $HOME ".vite-plus" @@ -943,7 +943,7 @@ jobs: fail-fast: false matrix: include: - - os: namespace-profile-linux-x64-default + - os: ubuntu-latest target: x86_64-unknown-linux-gnu - os: namespace-profile-mac-default target: aarch64-apple-darwin @@ -1028,10 +1028,10 @@ jobs: strategy: fail-fast: false matrix: - os: [namespace-profile-linux-x64-default, namespace-profile-mac-default] + os: [ubuntu-latest, namespace-profile-mac-default] shard: [1, 2, 3] include: - - os: namespace-profile-linux-x64-default + - os: ubuntu-latest target: x86_64-unknown-linux-gnu - os: namespace-profile-mac-default target: aarch64-apple-darwin @@ -1303,7 +1303,7 @@ jobs: name: CLI E2E test (Linux x64 musl) needs: - download-previous-rolldown-binaries - runs-on: namespace-profile-linux-x64-default + runs-on: ubuntu-latest steps: - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 - uses: ./.github/actions/clone @@ -1386,7 +1386,7 @@ jobs: needs: detect-changes if: needs.detect-changes.outputs.code-changed == 'true' name: Cert-less image E2E test - runs-on: namespace-profile-linux-x64-default + runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 @@ -1442,7 +1442,7 @@ jobs: name: Local CLI `vp install` E2E test needs: - build-cli-test-binaries - runs-on: namespace-profile-linux-x64-default + runs-on: ubuntu-latest # Run if: not a PR, OR PR has 'test: install-e2e' label if: >- github.event_name != 'pull_request' || From 3306f694d680188104817e25e0316a39faea16b4 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 16 Oct 2026 01:40:18 +0800 Subject: [PATCH 6/8] test: keep active registry downloads running --- .../src/__tests__/registry-request.spec.ts | 52 ++++++++++++++++--- packages/tools/src/local-npm-registry.ts | 6 +-- packages/tools/src/registry-request.ts | 14 +++-- 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/packages/tools/src/__tests__/registry-request.spec.ts b/packages/tools/src/__tests__/registry-request.spec.ts index a274f1c4e9..71e2c71b2a 100644 --- a/packages/tools/src/__tests__/registry-request.spec.ts +++ b/packages/tools/src/__tests__/registry-request.spec.ts @@ -1,10 +1,11 @@ import { once } from 'node:events'; import { createServer, get, type Server } from 'node:http'; import type { AddressInfo } from 'node:net'; +import { setTimeout as delay } from 'node:timers/promises'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; -import { setRegistryRequestDeadline } from '../registry-request.ts'; +import { setRegistryRequestTimeout } from '../registry-request.ts'; let server: Server; let url: string; @@ -29,7 +30,7 @@ it('aborts an upstream that never sends response headers', async () => { const received = once(server, 'request'); const request = get(url); const failed = once(request, 'error'); - setRegistryRequestDeadline(request, 100); + setRegistryRequestTimeout(request, 100); await received; vi.advanceTimersByTime(100); @@ -38,7 +39,19 @@ it('aborts an upstream that never sends response headers', async () => { expect(request.destroyed).toBe(true); }); -it('keeps the deadline active while the response body is incomplete', async () => { +it('aborts an upstream while DNS lookup is stalled', async () => { + const request = get('http://registry.invalid', { lookup: () => {} }); + const failed = once(request, 'error'); + setRegistryRequestTimeout(request, 100); + + vi.advanceTimersByTime(100); + const [error] = await failed; + expect(error.message).toContain('timed out'); + expect(request.destroyed).toBe(true); +}); + +it('aborts a response body that stops sending data', async () => { + vi.useRealTimers(); server.on('request', (_req, res) => { res.writeHead(200, { 'content-type': 'application/json' }); res.write('{'); @@ -46,23 +59,50 @@ it('keeps the deadline active while the response body is incomplete', async () = const request = get(url); const response = once(request, 'response'); const failed = once(request, 'error'); - setRegistryRequestDeadline(request, 100); + setRegistryRequestTimeout(request, 100); const [upstream] = await response; const bodyFailed = once(upstream, 'error'); upstream.resume(); - vi.advanceTimersByTime(100); const [error] = await failed; expect(error.message).toContain('timed out'); await bodyFailed; expect(upstream.complete).toBe(false); }); +it('allows a response body to exceed the timeout while data keeps arriving', async () => { + vi.useRealTimers(); + server.on('request', async (_req, res) => { + res.write('a'); + for (let chunk = 0; chunk < 6; chunk++) { + await delay(250); + if (res.destroyed) { + return; + } + res.write('b'); + } + res.end(); + }); + const request = get(url); + const failed = vi.fn(); + request.on('error', failed); + setRegistryRequestTimeout(request, 1000); + const [upstream] = await once(request, 'response'); + const chunks: Buffer[] = []; + for await (const chunk of upstream) { + chunks.push(chunk); + } + + expect(Buffer.concat(chunks).toString()).toBe('abbbbbb'); + expect(upstream.complete).toBe(true); + expect(failed).not.toHaveBeenCalled(); +}); + it('clears the deadline when a response finishes', async () => { server.on('request', (_req, res) => res.end('ok')); const request = get(url, (response) => response.resume()); const closed = once(request, 'close'); - setRegistryRequestDeadline(request, 100); + setRegistryRequestTimeout(request, 100); await closed; expect(vi.getTimerCount()).toBe(0); diff --git a/packages/tools/src/local-npm-registry.ts b/packages/tools/src/local-npm-registry.ts index f0a9c644f5..0b1d5e20d2 100644 --- a/packages/tools/src/local-npm-registry.ts +++ b/packages/tools/src/local-npm-registry.ts @@ -56,7 +56,7 @@ import { gunzipSync } from 'node:zlib'; import { exitCodeFromClose } from './exit-code.ts'; import { packLocalVitePlusPackages } from './pack-local-vite-plus.ts'; -import { setRegistryRequestDeadline } from './registry-request.ts'; +import { setRegistryRequestTimeout } from './registry-request.ts'; interface PackageManifest { name: string; @@ -341,7 +341,7 @@ function fetchUpstreamPackument(name: string): Promise { }); }, ).on('error', () => resolve(null)); - setRegistryRequestDeadline(request); + setRegistryRequestTimeout(request); }); } @@ -485,7 +485,7 @@ function proxyToUpstream(req: IncomingMessage, res: ServerResponse): void { }, ); upstreamRequest.on('error', fail); - setRegistryRequestDeadline(upstreamRequest); + setRegistryRequestTimeout(upstreamRequest); if (bodyless) { upstreamRequest.end(); } else { diff --git a/packages/tools/src/registry-request.ts b/packages/tools/src/registry-request.ts index 7d1ac3f3ee..07c1e5aab4 100644 --- a/packages/tools/src/registry-request.ts +++ b/packages/tools/src/registry-request.ts @@ -4,15 +4,19 @@ import type { ClientRequest } from 'node:http'; // managers retry proxied requests before a snapshot step reaches its 120s limit. const UPSTREAM_REQUEST_TIMEOUT_MS = 15_000; -export function setRegistryRequestDeadline( +export function setRegistryRequestTimeout( request: ClientRequest, timeout = UPSTREAM_REQUEST_TIMEOUT_MS, ): void { - // Socket timeouts do not cover DNS, connection setup, or waiting for an agent - // socket. Keep this deadline active until the response body finishes too. - const timer = setTimeout(() => { + const abort = () => { request.destroy(new Error(`Registry upstream request timed out after ${timeout}ms`)); - }, timeout); + }; + // Bound DNS, connection setup, and the wait for response headers too; a socket + // timeout alone does not cover all of those stages. + const timer = setTimeout(abort, timeout); timer.unref(); + // Once headers arrive, allow downloads to continue as long as data arrives. + request.setTimeout(timeout, abort); + request.once('response', () => clearTimeout(timer)); request.once('close', () => clearTimeout(timer)); } From 4ef6697801afdfe080a2fb3d72f3fea9229e2cf9 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 16 Oct 2026 01:45:38 +0800 Subject: [PATCH 7/8] ci: keep only the Linux runner change --- .github/actions/setup-ci-node/action.yml | 12 +- .../snapshots.toml | 2 - .../new_vite_monorepo/degit-fixture/LICENSE | 21 ---- .../new_vite_monorepo/degit-fixture/README.md | 18 --- .../new_vite_monorepo/degit-fixture/cli.mjs | 12 -- .../new_vite_monorepo/degit-fixture/pack.mjs | 63 ---------- .../degit-fixture/package.json | 7 -- .../degit-fixture/template/.gitignore | 4 - .../degit-fixture/template/README.md | 23 ---- .../degit-fixture/template/package.json | 39 ------ .../degit-fixture/template/src/index.ts | 3 - .../template/tests/index.test.ts | 6 - .../degit-fixture/template/tsconfig.json | 20 ---- .../degit-fixture/template/vite.config.ts | 17 --- .../new_vite_monorepo/mock-manifest.json | 23 ---- .../fixtures/new_vite_monorepo/snapshots.toml | 2 - .../tarballs/degit-0.0.0.tgz | Bin 2309 -> 0 bytes .../new_vite_monorepo_bun/mock-manifest.json | 23 ---- .../new_vite_monorepo_bun/snapshots.toml | 2 - .../tarballs/degit-0.0.0.tgz | Bin 2309 -> 0 bytes .../test_v5_optional_packages/snapshots.toml | 2 - crates/vp_shared/Cargo.toml | 2 +- crates/vp_shared/src/http.rs | 73 +----------- docs/guide/global-cli.md | 1 - .../src/__tests__/local-npm-registry.spec.ts | 51 +------- .../src/__tests__/registry-request.spec.ts | 111 ------------------ packages/tools/src/local-npm-registry.ts | 17 +-- packages/tools/src/registry-request.ts | 22 ---- 28 files changed, 11 insertions(+), 565 deletions(-) delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/LICENSE delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/README.md delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/cli.mjs delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.mjs delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/package.json delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/.gitignore delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/README.md delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/package.json delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/src/index.ts delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/tests/index.test.ts delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/tsconfig.json delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/vite.config.ts delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/mock-manifest.json delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/tarballs/degit-0.0.0.tgz delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/mock-manifest.json delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/tarballs/degit-0.0.0.tgz delete mode 100644 packages/tools/src/__tests__/registry-request.spec.ts delete mode 100644 packages/tools/src/registry-request.ts diff --git a/.github/actions/setup-ci-node/action.yml b/.github/actions/setup-ci-node/action.yml index cc10751385..63d4ef2558 100644 --- a/.github/actions/setup-ci-node/action.yml +++ b/.github/actions/setup-ci-node/action.yml @@ -17,18 +17,8 @@ runs: registry-url: https://registry.npmjs.org package-manager-cache: false - - name: Setup pnpm - id: setup-pnpm - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 if: runner.os != 'Linux' - continue-on-error: true - - # The bootstrap verifies the target version against registry metadata. - # A stalled or truncated response can fail that verification. Retry the - # complete setup once; both attempts retain the action's signature checks. - - name: Retry pnpm setup - if: runner.os != 'Linux' && steps.setup-pnpm.outcome == 'failure' - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - name: Install dependencies if: runner.os != 'Linux' diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_existing_vite_plus/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_existing_vite_plus/snapshots.toml index 0c4ad95f34..b8bf12cafe 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_existing_vite_plus/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_existing_vite_plus/snapshots.toml @@ -3,8 +3,6 @@ name = "migration_oxlint_existing_vite_plus" vp = "global" skip-platforms = ["windows"] local-registry = true -# Keep npm network retries inside the 120-second step budget. -env = { npm_config_fetch_timeout = "15000", npm_config_fetch_retry_mintimeout = "1000", npm_config_fetch_retry_maxtimeout = "5000" } unset-env = ["VP_SKIP_INSTALL"] steps = [ { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], snapshot = false }, diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/LICENSE b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/LICENSE deleted file mode 100644 index 8701f9fe69..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright © 2025-PRESENT Kevin Deng (https://github.com/sxzz) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/README.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/README.md deleted file mode 100644 index 4d7e1da8e7..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Fixed library template - -The `new_vite_monorepo` and `new_vite_monorepo_bun` cases check project creation -and catalog migration. Their registry overlays replace `degit` with a local -template copy. The wrapper checks the requested repository and arguments. -The real package managers and Vite+ commands still run. Other cases retain -the real `degit` package. - -`template/` contains the `vite-plus/` template from -[`sxzz/tsdown-templates` at `c1d30b48`](https://github.com/sxzz/tsdown-templates/tree/c1d30b48c445cc73d2da92be1db6f7c27cc0110c/vite-plus). -`LICENSE` preserves its MIT license. Test runs never fetch or refresh this copy. - -After an intentional fixture change, regenerate both case tarballs and their -integrity hashes from the repository root: - -```sh -node crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.mjs -``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/cli.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/cli.mjs deleted file mode 100644 index 7e615cc664..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/cli.mjs +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env node -import assert from 'node:assert/strict'; -import { cpSync, existsSync } from 'node:fs'; - -// These cases check monorepo creation and catalog migration. Keep the -// downloaded template fixed while running the real package manager and CLI. -const [source, destination, ...extra] = process.argv.slice(2); -assert.equal(source, 'sxzz/tsdown-templates/vite-plus'); -assert.ok(destination, 'Missing template destination'); -assert.deepEqual(extra, [], 'Unexpected template arguments'); -assert.equal(existsSync(destination), false, 'Template destination already exists'); -cpSync(new URL('./template/', import.meta.url), destination, { recursive: true }); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.mjs deleted file mode 100644 index b7eba2702b..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/pack.mjs +++ /dev/null @@ -1,63 +0,0 @@ -import assert from 'node:assert/strict'; -import { createHash } from 'node:crypto'; -import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'; -import { join, sep } from 'node:path'; -import { gzipSync } from 'node:zlib'; - -// Pack the checked-in template without dependencies or external data. -const source = import.meta.dirname; -const files = [ - 'package.json', - 'cli.mjs', - 'LICENSE', - ...readdirSync(join(source, 'template'), { recursive: true }) - .map((name) => `template/${name.split(sep).join('/')}`) - .filter((name) => statSync(join(source, name)).isFile()) - .sort(), -]; - -const blocks = []; -for (const name of files) { - const body = readFileSync(join(source, name)); - const archiveName = `package/${name}`; - assert(Buffer.byteLength(archiveName) <= 100, `Archive path is too long: ${archiveName}`); - - // Write a ustar header with fixed ownership and timestamps. - const header = Buffer.alloc(512); - header.write(archiveName); - header.write(name === 'cli.mjs' ? '0000755\0' : '0000644\0', 100); - header.write('0000000\0', 108); - header.write('0000000\0', 116); - header.write(body.length.toString(8).padStart(11, '0') + '\0', 124); - header.write('00000000000\0', 136); - header.fill(' ', 148, 156); - header.write('0', 156); - header.write('ustar', 257); - header.write('00', 263); - const checksum = header.reduce((sum, byte) => sum + byte, 0); - header.write(checksum.toString(8).padStart(6, '0') + '\0 ', 148); - blocks.push(header, body, Buffer.alloc((512 - (body.length % 512)) % 512)); -} - -// End with two empty blocks and pad to tar's standard 20-block record size. -const archive = Buffer.concat([...blocks, Buffer.alloc(1024)]); -const padding = Buffer.alloc((10240 - (archive.length % 10240)) % 10240); -const tarball = gzipSync(Buffer.concat([archive, padding]), { level: 9 }); -// Omit the host OS from the gzip header to keep the hash platform-independent. -tarball[9] = 255; -const integrity = `sha512-${createHash('sha512').update(tarball).digest('base64')}`; -const pkg = JSON.parse(readFileSync(join(source, 'package.json'), 'utf8')); -pkg.dist = { tarball: '{REGISTRY}/degit/-/degit-0.0.0.tgz', integrity }; -const manifest = { - degit: { - name: 'degit', - 'dist-tags': { latest: '0.0.0' }, - versions: { '0.0.0': pkg }, - }, -}; -for (const name of ['new_vite_monorepo', 'new_vite_monorepo_bun']) { - const fixture = join(source, '..', '..', name); - mkdirSync(join(fixture, 'tarballs'), { recursive: true }); - writeFileSync(join(fixture, 'tarballs', 'degit-0.0.0.tgz'), tarball); - writeFileSync(join(fixture, 'mock-manifest.json'), JSON.stringify(manifest, null, 2) + '\n'); -} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/package.json deleted file mode 100644 index 528c4eb721..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "degit", - "version": "0.0.0", - "private": true, - "type": "module", - "bin": { "degit": "cli.mjs" } -} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/.gitignore b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/.gitignore deleted file mode 100644 index 7535211682..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -dist -*.log -.DS_Store diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/README.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/README.md deleted file mode 100644 index 6ae949b2c7..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# vite-plus-starter - -A starter for creating a Vite Plus project. - -## Development - -- Install dependencies: - -```bash -vp install -``` - -- Run the unit tests: - -```bash -vp test -``` - -- Build the library: - -```bash -vp pack -``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/package.json deleted file mode 100644 index cca257edb9..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "vite-plus-starter", - "version": "0.0.0", - "description": "A starter for creating a TypeScript package.", - "homepage": "https://github.com/author/library#readme", - "bugs": { - "url": "https://github.com/author/library/issues" - }, - "license": "MIT", - "author": "Author Name ", - "repository": { - "type": "git", - "url": "git+https://github.com/author/library.git" - }, - "files": [ - "dist" - ], - "type": "module", - "exports": { - ".": "./dist/index.mjs", - "./package.json": "./package.json" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "build": "vp pack", - "dev": "vp pack --watch", - "test": "vp test", - "check": "vp check", - "prepublishOnly": "vp run build" - }, - "devDependencies": { - "@types/node": "^26.1.1", - "bumpp": "^11.1.0", - "typescript": "^7.0.2", - "vite-plus": "^0.2.4" - } -} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/src/index.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/src/index.ts deleted file mode 100644 index 476d2c7edd..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function fn() { - return "Hello, tsdown!"; -} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/tests/index.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/tests/index.test.ts deleted file mode 100644 index 29a522ee7e..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/tests/index.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expect, test } from "vite-plus/test"; -import { fn } from "../src/index.ts"; - -test("fn", () => { - expect(fn()).toBe("Hello, tsdown!"); -}); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/tsconfig.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/tsconfig.json deleted file mode 100644 index ff4adab564..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "esnext", - "lib": ["es2023"], - "moduleDetection": "force", - "module": "nodenext", - "moduleResolution": "nodenext", - "resolveJsonModule": true, - "types": ["node"], - "strict": true, - "noUnusedLocals": true, - "declaration": true, - "noEmit": true, - "allowImportingTsExtensions": true, - "esModuleInterop": true, - "isolatedModules": true, - "verbatimModuleSyntax": true, - "skipLibCheck": true - } -} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/vite.config.ts deleted file mode 100644 index bffbbc1e6a..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/degit-fixture/template/vite.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { defineConfig } from "vite-plus"; - -export default defineConfig({ - pack: { - dts: { - tsgo: true, - }, - exports: true, - }, - lint: { - options: { - typeAware: true, - typeCheck: true, - }, - }, - fmt: {}, -}); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/mock-manifest.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/mock-manifest.json deleted file mode 100644 index c682476ed6..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/mock-manifest.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "degit": { - "name": "degit", - "dist-tags": { - "latest": "0.0.0" - }, - "versions": { - "0.0.0": { - "name": "degit", - "version": "0.0.0", - "private": true, - "type": "module", - "bin": { - "degit": "cli.mjs" - }, - "dist": { - "tarball": "{REGISTRY}/degit/-/degit-0.0.0.tgz", - "integrity": "sha512-LjOTGOarcX+1htyKXXfwmIzY7NjCICaVUv6bFvvOOknqSQoovSKzj0Kd14kcev38sErykYrhEar4VhrW5NjvTQ==" - } - } - } - } -} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/snapshots.toml index 690eefa972..84a08f56da 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/snapshots.toml @@ -2,8 +2,6 @@ name = "new_vite_monorepo" vp = "global" skip-platforms = ["windows"] -local-registry = true -# mock-manifest.json fixes the remote library input; see degit-fixture/README.md. steps = [ { argv = ["vp", "create", "vite:monorepo", "--no-interactive", "--git", "--editor", "vscode"], comment = "create monorepo with default values", snapshot = false, continue-on-failure = true }, { argv = ["vpt", "list-dir", "vite-plus-monorepo"], comment = "check files created", continue-on-failure = true }, diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/tarballs/degit-0.0.0.tgz b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/tarballs/degit-0.0.0.tgz deleted file mode 100644 index cc1383c5f0d804e3d965816b28e074594a6134ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2309 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 diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/mock-manifest.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/mock-manifest.json deleted file mode 100644 index c682476ed6..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/mock-manifest.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "degit": { - "name": "degit", - "dist-tags": { - "latest": "0.0.0" - }, - "versions": { - "0.0.0": { - "name": "degit", - "version": "0.0.0", - "private": true, - "type": "module", - "bin": { - "degit": "cli.mjs" - }, - "dist": { - "tarball": "{REGISTRY}/degit/-/degit-0.0.0.tgz", - "integrity": "sha512-LjOTGOarcX+1htyKXXfwmIzY7NjCICaVUv6bFvvOOknqSQoovSKzj0Kd14kcev38sErykYrhEar4VhrW5NjvTQ==" - } - } - } - } -} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/snapshots.toml index 7bc1e8cf8b..41c0d7ab8e 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/snapshots.toml @@ -2,8 +2,6 @@ name = "new_vite_monorepo_bun" vp = "global" skip-platforms = ["windows"] -local-registry = true -# Reuse the fixed library input documented in ../new_vite_monorepo/degit-fixture/README.md. steps = [ { argv = ["vp", "create", "vite:monorepo", "--no-interactive", "--package-manager", "bun", "--git"], comment = "create monorepo with bun", snapshot = false, continue-on-failure = true }, { argv = ["vpt", "list-dir", "vite-plus-monorepo"], comment = "check files created", continue-on-failure = true }, diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/tarballs/degit-0.0.0.tgz b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/tarballs/degit-0.0.0.tgz deleted file mode 100644 index cc1383c5f0d804e3d965816b28e074594a6134ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2309 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 diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_optional_packages/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_optional_packages/snapshots.toml index 378fea24c9..3da7851eaa 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_optional_packages/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_optional_packages/snapshots.toml @@ -3,8 +3,6 @@ name = "environments_temporal_html_and_worker_startup" vp = "global" skip-platforms = ["windows"] local-registry = true -# Keep npm network retries inside the 120-second step budget. -env = { npm_config_fetch_timeout = "15000", npm_config_fetch_retry_mintimeout = "1000", npm_config_fetch_retry_maxtimeout = "5000" } steps = [ { argv = ["npm", "install", "--ignore-scripts", "--no-audit", "--no-fund"], snapshot = false, timeout = 120000 }, { argv = ["vp", "test", "run", "--reporter=default", "--reporter=html", "--maxWorkers=1"], timeout = 120000 }, diff --git a/crates/vp_shared/Cargo.toml b/crates/vp_shared/Cargo.toml index ecb0f5d7b7..bfaef1910f 100644 --- a/crates/vp_shared/Cargo.toml +++ b/crates/vp_shared/Cargo.toml @@ -46,7 +46,7 @@ webpki-root-certs = { workspace = true } indicatif = { workspace = true, features = ["in_memory"] } serial_test = { workspace = true } tempfile = { workspace = true } -tokio = { workspace = true, features = ["io-util", "macros", "net", "rt", "time"] } +tokio = { workspace = true, features = ["macros", "rt"] } # Enables the `test-utils` feature for this crate's own tests and doctests # (a package's dev-dependencies are active when it is the tested target). vp_shared = { path = ".", features = ["test-utils"] } diff --git a/crates/vp_shared/src/http.rs b/crates/vp_shared/src/http.rs index 8be2646d28..6b9c30faff 100644 --- a/crates/vp_shared/src/http.rs +++ b/crates/vp_shared/src/http.rs @@ -72,11 +72,6 @@ pub const fn request_timeout() -> Duration { /// retries (multiple minutes). const CONNECT_TIMEOUT: Duration = Duration::from_secs(30); -/// Maximum wait for the next response bytes. This restarts after each read, -/// so slow downloads can use their full total budget while stalled streams -/// fail soon enough for callers to retry. -const READ_TIMEOUT: Duration = Duration::from_secs(20); - /// Default per-request timeout for large file downloads (Node.js runtimes, /// package-manager tarballs). Those archives are tens of megabytes, so on /// slow or flaky connections the shared [`REQUEST_TIMEOUT`] aborts an @@ -224,10 +219,8 @@ fn build_with( insecure: bool, follow_redirects: bool, ) -> Result { - let mut builder = reqwest::Client::builder() - .timeout(REQUEST_TIMEOUT) - .connect_timeout(CONNECT_TIMEOUT) - .read_timeout(READ_TIMEOUT); + let mut builder = + reqwest::Client::builder().timeout(REQUEST_TIMEOUT).connect_timeout(CONNECT_TIMEOUT); if !extra_certs.is_empty() { builder = builder.tls_certs_merge(extra_certs); } @@ -301,68 +294,6 @@ mod tests { use super::*; - /// Send a body one byte at a time to distinguish idle and total timeouts. - async fn serve_slow_body( - chunks: usize, - interval: Duration, - ) -> (String, tokio::task::JoinHandle<()>) { - use tokio::{ - io::{AsyncReadExt, AsyncWriteExt}, - net::TcpListener, - }; - - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let url = format!("http://{}/", listener.local_addr().unwrap()); - let server = tokio::spawn(async move { - let (mut stream, _) = listener.accept().await.unwrap(); - let mut request = Vec::new(); - let mut buffer = [0; 1024]; - while !request.windows(4).any(|bytes| bytes == b"\r\n\r\n") { - let count = stream.read(&mut buffer).await.unwrap(); - assert_ne!(count, 0); - request.extend_from_slice(&buffer[..count]); - } - stream - .write_all( - format!("HTTP/1.1 200 OK\r\nContent-Length: {chunks}\r\n\r\n").as_bytes(), - ) - .await - .unwrap(); - for index in 0..chunks { - if index > 0 { - tokio::time::sleep(interval).await; - } - stream.write_all(b"x").await.unwrap(); - } - }); - (url, server) - } - - #[tokio::test] - async fn stalled_body_times_out_before_the_total_download_budget() { - crate::ensure_tls_provider(); - let client = build_with(Vec::new(), false, false).unwrap(); - let (url, server) = serve_slow_body(2, READ_TIMEOUT * 3).await; - let response = client.get(url).timeout(DEFAULT_DOWNLOAD_TIMEOUT).send().await.unwrap(); - let result = tokio::time::timeout(READ_TIMEOUT * 2, response.bytes()).await; - server.abort(); - - let error = result.expect("a stalled body must hit the read timeout").unwrap_err(); - assert!(error.is_timeout(), "{error}"); - } - - #[tokio::test] - async fn progressing_body_can_take_longer_than_the_read_timeout() { - crate::ensure_tls_provider(); - let client = build_with(Vec::new(), false, true).unwrap(); - let (url, server) = serve_slow_body(4, READ_TIMEOUT / 2).await; - let response = client.get(url).timeout(DEFAULT_DOWNLOAD_TIMEOUT).send().await.unwrap(); - let result = tokio::time::timeout(READ_TIMEOUT * 3, response.bytes()).await; - server.abort(); - - assert_eq!(result.unwrap().unwrap().as_ref(), b"xxxx"); - } - /// Valid PEM, invalid DER: `from_pem_bundle` accepts it, `build` rejects /// it. Portable way into the build-failure branch, unlike an empty trust /// store. diff --git a/docs/guide/global-cli.md b/docs/guide/global-cli.md index 53813e041e..a89a91a0f5 100644 --- a/docs/guide/global-cli.md +++ b/docs/guide/global-cli.md @@ -255,7 +255,6 @@ These variables configure the installed Vite+ CLI. `VP_HOME` (above) also applie - **Purpose**: Per-request timeout, in seconds, for large downloads such as Node.js runtimes and package-manager tarballs - **Values**: Positive integer, at most `86400` (24 hours); invalid values are ignored with a warning - **Default**: `600` (10 minutes) -- **Inactivity limit**: Requests also time out after 20 seconds without response data. Each read resets this limit, so active downloads can use the full total timeout. - **Example**: ```bash # Allow up to 30 minutes per download on a slow connection diff --git a/packages/tools/src/__tests__/local-npm-registry.spec.ts b/packages/tools/src/__tests__/local-npm-registry.spec.ts index 7622950980..dd03810f15 100644 --- a/packages/tools/src/__tests__/local-npm-registry.spec.ts +++ b/packages/tools/src/__tests__/local-npm-registry.spec.ts @@ -1,59 +1,10 @@ import { execFileSync } from 'node:child_process'; -import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; +import { existsSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { expect, it } from 'vitest'; -it('serves fixture versions and dist-tags without resolving them from the public registry', () => { - const script = fileURLToPath(new URL('../local-npm-registry.ts', import.meta.url)); - const cwd = mkdtempSync(path.join(tmpdir(), 'vp-registry-versions-')); - const names = ['degit', '@test/template']; - const manifest = Object.fromEntries( - names.map((name) => [ - name, - { - name, - 'dist-tags': { latest: '0.0.0' }, - versions: { - '0.0.0': { - name, - version: '0.0.0', - dist: { tarball: `{REGISTRY}/${name}/-/template.tgz` }, - }, - }, - }, - ]), - ); - try { - writeFileSync(path.join(cwd, 'mock-manifest.json'), JSON.stringify(manifest)); - const probe = ` - const assert = require('node:assert/strict'); - (async () => { - const registry = process.env.NPM_CONFIG_REGISTRY; - for (const name of ${JSON.stringify(names)}) { - const packument = await (await fetch(registry + '/' + encodeURIComponent(name))).json(); - for (const version of ['latest', '0.0.0']) { - const response = await fetch(registry + '/' + encodeURIComponent(name) + '/' + version); - assert.equal(response.status, 200); - const actual = await response.json(); - assert.deepEqual(actual, packument.versions['0.0.0']); - assert.equal(actual.dist.tarball, registry + '/' + name + '/-/template.tgz'); - } - } - })().catch((error) => { console.error(error); process.exitCode = 1; }); - `; - execFileSync(process.execPath, [script, '--', process.execPath, '-e', probe], { - cwd, - timeout: 10_000, - env: { ...process.env, SNAP_LOCAL_VP_PACKAGES_DIR: '' }, - }); - } finally { - rmSync(cwd, { recursive: true, force: true }); - } -}); - it('isolates and cleans the platform temp directory used by bunx on each registry run', () => { const script = fileURLToPath(new URL('../local-npm-registry.ts', import.meta.url)); const probe = ` diff --git a/packages/tools/src/__tests__/registry-request.spec.ts b/packages/tools/src/__tests__/registry-request.spec.ts deleted file mode 100644 index 71e2c71b2a..0000000000 --- a/packages/tools/src/__tests__/registry-request.spec.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { once } from 'node:events'; -import { createServer, get, type Server } from 'node:http'; -import type { AddressInfo } from 'node:net'; -import { setTimeout as delay } from 'node:timers/promises'; - -import { afterEach, beforeEach, expect, it, vi } from 'vitest'; - -import { setRegistryRequestTimeout } from '../registry-request.ts'; - -let server: Server; -let url: string; - -beforeEach(async () => { - vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }); - server = createServer(); - server.listen(0, '127.0.0.1'); - await once(server, 'listening'); - url = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; -}); - -afterEach(async () => { - vi.useRealTimers(); - const closed = once(server, 'close'); - server.close(); - server.closeAllConnections(); - await closed; -}); - -it('aborts an upstream that never sends response headers', async () => { - const received = once(server, 'request'); - const request = get(url); - const failed = once(request, 'error'); - setRegistryRequestTimeout(request, 100); - await received; - - vi.advanceTimersByTime(100); - const [error] = await failed; - expect(error.message).toBe('Registry upstream request timed out after 100ms'); - expect(request.destroyed).toBe(true); -}); - -it('aborts an upstream while DNS lookup is stalled', async () => { - const request = get('http://registry.invalid', { lookup: () => {} }); - const failed = once(request, 'error'); - setRegistryRequestTimeout(request, 100); - - vi.advanceTimersByTime(100); - const [error] = await failed; - expect(error.message).toContain('timed out'); - expect(request.destroyed).toBe(true); -}); - -it('aborts a response body that stops sending data', async () => { - vi.useRealTimers(); - server.on('request', (_req, res) => { - res.writeHead(200, { 'content-type': 'application/json' }); - res.write('{'); - }); - const request = get(url); - const response = once(request, 'response'); - const failed = once(request, 'error'); - setRegistryRequestTimeout(request, 100); - const [upstream] = await response; - const bodyFailed = once(upstream, 'error'); - upstream.resume(); - - const [error] = await failed; - expect(error.message).toContain('timed out'); - await bodyFailed; - expect(upstream.complete).toBe(false); -}); - -it('allows a response body to exceed the timeout while data keeps arriving', async () => { - vi.useRealTimers(); - server.on('request', async (_req, res) => { - res.write('a'); - for (let chunk = 0; chunk < 6; chunk++) { - await delay(250); - if (res.destroyed) { - return; - } - res.write('b'); - } - res.end(); - }); - const request = get(url); - const failed = vi.fn(); - request.on('error', failed); - setRegistryRequestTimeout(request, 1000); - const [upstream] = await once(request, 'response'); - const chunks: Buffer[] = []; - for await (const chunk of upstream) { - chunks.push(chunk); - } - - expect(Buffer.concat(chunks).toString()).toBe('abbbbbb'); - expect(upstream.complete).toBe(true); - expect(failed).not.toHaveBeenCalled(); -}); - -it('clears the deadline when a response finishes', async () => { - server.on('request', (_req, res) => res.end('ok')); - const request = get(url, (response) => response.resume()); - const closed = once(request, 'close'); - setRegistryRequestTimeout(request, 100); - await closed; - - expect(vi.getTimerCount()).toBe(0); - vi.advanceTimersByTime(100); - expect(request.errored).toBeNull(); -}); diff --git a/packages/tools/src/local-npm-registry.ts b/packages/tools/src/local-npm-registry.ts index 0b1d5e20d2..acce4c4eb7 100644 --- a/packages/tools/src/local-npm-registry.ts +++ b/packages/tools/src/local-npm-registry.ts @@ -56,7 +56,6 @@ import { gunzipSync } from 'node:zlib'; import { exitCodeFromClose } from './exit-code.ts'; import { packLocalVitePlusPackages } from './pack-local-vite-plus.ts'; -import { setRegistryRequestTimeout } from './registry-request.ts'; interface PackageManifest { name: string; @@ -318,7 +317,7 @@ function serializeForRegistry(value: unknown, registry: string): string { function fetchUpstreamPackument(name: string): Promise { return new Promise((resolve) => { - const request = httpsGet( + httpsGet( `${UPSTREAM_REGISTRY}/${name.replace('/', '%2F')}`, { headers: { accept: 'application/json', 'accept-encoding': 'gzip' }, agent: upstreamAgent }, (upstream) => { @@ -341,7 +340,6 @@ function fetchUpstreamPackument(name: string): Promise { }); }, ).on('error', () => resolve(null)); - setRegistryRequestTimeout(request); }); } @@ -386,12 +384,12 @@ function resolveLocalVersion(key: string): unknown { } const name = key.slice(0, separator); const specifier = key.slice(separator + 1); - const packument = localPackuments.get(name) ?? (manifest[name] as Packument | undefined); + const packument = localPackuments.get(name); if (!packument) { return undefined; } - const version = packument['dist-tags']?.[specifier] ?? specifier; - return packument.versions?.[version]; + const version = packument['dist-tags'][specifier] ?? specifier; + return packument.versions[version]; } // Stream the upstream response byte-for-byte. Unlike `fetch`, `https` does not @@ -416,11 +414,9 @@ function proxyToUpstream(req: IncomingMessage, res: ServerResponse): void { // Stream errors can fire after headers are already sent (e.g. the upstream // connection resets mid-tarball), so guard against a second writeHead. const fail = (error: Error) => { - if (res.headersSent) { - res.destroy(error); - return; + if (!res.headersSent) { + res.writeHead(502); } - res.writeHead(502); res.end(`proxy error: ${error.message}`); }; // Forward the original method and body: npm-based clients POST to registry @@ -485,7 +481,6 @@ function proxyToUpstream(req: IncomingMessage, res: ServerResponse): void { }, ); upstreamRequest.on('error', fail); - setRegistryRequestTimeout(upstreamRequest); if (bodyless) { upstreamRequest.end(); } else { diff --git a/packages/tools/src/registry-request.ts b/packages/tools/src/registry-request.ts deleted file mode 100644 index 07c1e5aab4..0000000000 --- a/packages/tools/src/registry-request.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { ClientRequest } from 'node:http'; - -// A stalled upstream must release local-package metadata lookups and let package -// managers retry proxied requests before a snapshot step reaches its 120s limit. -const UPSTREAM_REQUEST_TIMEOUT_MS = 15_000; - -export function setRegistryRequestTimeout( - request: ClientRequest, - timeout = UPSTREAM_REQUEST_TIMEOUT_MS, -): void { - const abort = () => { - request.destroy(new Error(`Registry upstream request timed out after ${timeout}ms`)); - }; - // Bound DNS, connection setup, and the wait for response headers too; a socket - // timeout alone does not cover all of those stages. - const timer = setTimeout(abort, timeout); - timer.unref(); - // Once headers arrive, allow downloads to continue as long as data arrives. - request.setTimeout(timeout, abort); - request.once('response', () => clearTimeout(timer)); - request.once('close', () => clearTimeout(timer)); -} From b163c3abf4e2cd9c4f5c5f4ad5170a3c60304ea0 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 16 Oct 2026 01:58:40 +0800 Subject: [PATCH 8/8] test: refresh remote monorepo template snapshots --- .../fixtures/new_vite_monorepo/snapshots/new_vite_monorepo.md | 4 ++-- .../new_vite_monorepo_bun/snapshots/new_vite_monorepo_bun.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/snapshots/new_vite_monorepo.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/snapshots/new_vite_monorepo.md index 3d2400fe04..bb258b1685 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/snapshots/new_vite_monorepo.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/snapshots/new_vite_monorepo.md @@ -238,8 +238,8 @@ check utils normalizes vite-plus to catalog: "prepublishOnly": "vp run build" }, "devDependencies": { - "@types/node": "^26.1.1", - "bumpp": "^11.1.0", + "@types/node": "^26.6.5", + "bumpp": "^12.3.0", "typescript": "^7.0.2", "vite-plus": "catalog:" } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/snapshots/new_vite_monorepo_bun.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/snapshots/new_vite_monorepo_bun.md index bd6bf4c8ca..44f28f460d 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/snapshots/new_vite_monorepo_bun.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo_bun/snapshots/new_vite_monorepo_bun.md @@ -155,8 +155,8 @@ check utils normalizes vite-plus to catalog: "prepublishOnly": "vp run build" }, "devDependencies": { - "@types/node": "^26.1.1", - "bumpp": "^11.1.0", + "@types/node": "^26.6.5", + "bumpp": "^12.3.0", "typescript": "^7.0.2", "vite-plus": "catalog:" }