Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
7bd5137
docs(rfc): vp migrate upgrade path for existing Vite+ projects
fengmk2 Jun 10, 2026
6861a65
docs(rfc): cover real 0.1.24->0.2.0 upgrade failure in vp migrate
fengmk2 Jun 18, 2026
90093b3
docs(rfc): align vp migrate upgrade with the v0.2.1 prompt spec
fengmk2 Jun 18, 2026
8f744cc
fix(migrate): make vp migrate upgrade v0.1.x projects to v0.2.x
fengmk2 Jun 18, 2026
29f102a
feat(migrate): manage vitest only when the project uses it directly
fengmk2 Jun 18, 2026
2328dc6
feat(migrate): align the full @vitest/* ecosystem to the bundled vitest
fengmk2 Jun 19, 2026
c7ae886
docs(rfc): revise migrate RFC for vitest provisioning and ecosystem r…
fengmk2 Jun 19, 2026
ed2123b
fix(migrate): make upgrade provisioning peer-safe
fengmk2 Jun 19, 2026
18f69a3
fix(migrate): validate upgrade scenarios in snapshots
fengmk2 Jun 19, 2026
3323404
test(migrate): update default vitest snapshots
fengmk2 Jun 21, 2026
3e30c21
fix(migrate): handle peer and override edge cases
fengmk2 Jun 21, 2026
baa95c2
fix(migrate): cover remaining vitest upgrade cases
fengmk2 Jun 21, 2026
764ad9f
fix(test): normalize snapshot file endings
fengmk2 Jun 21, 2026
9bf48ab
test(migrate): sync idempotency snapshots
fengmk2 Jun 21, 2026
7e19ead
test(create): update standalone Yarn catalog snapshot
fengmk2 Jun 21, 2026
59d1d4c
fix(migrate): preserve vitest imports for Nuxt tests
fengmk2 Jun 23, 2026
25aaa38
test(ecosystem-ci): update npmx.dev fixture
fengmk2 Jun 23, 2026
30d928c
test(cli): stabilize Nuxt lint snapshot
fengmk2 Jun 23, 2026
83a8e38
fix(migrate): preserve Vitest across Nuxt packages
fengmk2 Jun 23, 2026
0e65412
fix(migrate): convert Yarn PnP projects
fengmk2 Jun 23, 2026
a89a024
test(ecosystem): install Playwright for npmx.dev
fengmk2 Jun 23, 2026
b4e9a28
test(migrate): cover conservative monorepo retention
fengmk2 Jun 23, 2026
a163006
fix(migrate): pin pkg.pr.new targets in test helper
fengmk2 Jun 23, 2026
78d51db
fix(test): keep pkg.pr.new overrides minimal
fengmk2 Jun 23, 2026
35aa7ae
fix(migrate): allow pkg.pr.new pnpm subdependencies
fengmk2 Jun 23, 2026
979b303
fix(test): refresh mutable pkg.pr.new installs
fengmk2 Jun 24, 2026
d18b155
fix(migrate): preserve Vitest ecosystem catalogs
fengmk2 Jun 24, 2026
c90a703
fix(migrate): pin vite-plus toolchain versions
fengmk2 Jun 24, 2026
3422014
fix(test): reuse unchanged pkg.pr.new install
fengmk2 Jun 24, 2026
f1dcd99
fix(test): run pkg.pr.new migration from project root
fengmk2 Jun 24, 2026
74680c9
fix(migrate): isolate config compatibility checks
fengmk2 Jun 24, 2026
ee7a325
fix(test): pin pkg.pr.new migration builds by commit
fengmk2 Jun 24, 2026
17f340b
fix(migrate): move pnpm settings to workspace config
fengmk2 Jun 25, 2026
d63c8e5
docs(migrate): document user-facing migration rules
fengmk2 Jun 25, 2026
5b1d9e9
test(migrate): update migration snapshots
fengmk2 Jun 25, 2026
019592b
docs(migrate): clarify pnpm vite dependency rule
fengmk2 Jun 25, 2026
fb63d4c
fix(migrate): format projects after migration
fengmk2 Jun 25, 2026
90ec1be
fix(migrate): preserve unmigrated Prettier projects
fengmk2 Jun 25, 2026
36cb267
fix(migrate): detect legacy browser providers
fengmk2 Jun 25, 2026
d3c06fd
fix(ci): upgrade affected pnpm 11 versions for pkg.pr.new tests
fengmk2 Jun 26, 2026
e923fba
fix(migrate): preserve Bun config arrays
fengmk2 Jun 26, 2026
3dc3aee
fix(migrate): avoid Bun peer suppression
fengmk2 Jun 26, 2026
4d2e1ff
fix(ci): support Bun pkg.pr.new migration tests
fengmk2 Jun 26, 2026
4c0a59a
fix(ci): satisfy lint in Bun pkg helper
fengmk2 Jun 26, 2026
c2cb3f6
fix(migrate): rewrite tools invoked through bunx
fengmk2 Jun 26, 2026
5795990
fix(migrate): limit automatic formatting to changed files
fengmk2 Jun 26, 2026
422a33e
fix(migrate): defer supported formats to oxfmt
fengmk2 Jun 26, 2026
4d09dd2
fix(ci): upgrade affected pnpm 10 pkg-pr-new tests
fengmk2 Jun 26, 2026
6acea1a
fix(migrate): address review feedback
fengmk2 Jun 26, 2026
50f7fca
fix(migrate): preserve pnpm catalog layouts
fengmk2 Jun 26, 2026
ed5f244
test(migrate): use release version for default catalog
fengmk2 Jun 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions .github/scripts/bun-pkg-pr-new.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#!/usr/bin/env node

import fs from 'node:fs';
import path from 'node:path';

function usage() {
console.error(`Usage:
bun-pkg-pr-new.mjs is-bun-project <package.json>
bun-pkg-pr-new.mjs patch-package <package.json> <core-url> <vite-plus-url>
bun-pkg-pr-new.mjs add-core-dependency <package.json> <core-spec>
bun-pkg-pr-new.mjs normalize-vite-paths <project-dir> <tarball-path>`);
process.exit(2);
}

function readPackageJson(packageJsonPath) {
const text = fs.readFileSync(packageJsonPath, 'utf8');
return {
indent: text.match(/\n([\t ]+)"/)?.[1] ?? ' ',
pkg: JSON.parse(text),
};
}

function writePackageJson(packageJsonPath, pkg, indent) {
fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, indent)}\n`);
}

function isBunProject(packageJsonPath) {
const { pkg } = readPackageJson(packageJsonPath);
const packageManager =
typeof pkg.packageManager === 'string' ? pkg.packageManager.split('@')[0] : undefined;
const devEngine = pkg.devEngines?.packageManager;
const devEngines = Array.isArray(devEngine) ? devEngine : [devEngine];
const hasBunDevEngine = devEngines.some((entry) => {
const name = typeof entry === 'string' ? entry : entry?.name;
return name === 'bun';
});
process.exit(packageManager === 'bun' || hasBunDevEngine ? 0 : 1);
}

function patchPackage(packageJsonPath, coreUrl, vitePlusUrl) {
const { pkg } = readPackageJson(packageJsonPath);
const bundledViteVersion = pkg.bundledVersions?.vite;

pkg.name = 'vite';
pkg.version =
typeof bundledViteVersion === 'string' && bundledViteVersion.length > 0
? bundledViteVersion
: '8.0.0';
pkg.dependencies = {
...pkg.dependencies,
'@voidzero-dev/vite-plus-core': coreUrl,
'vite-plus': vitePlusUrl,
};

writePackageJson(packageJsonPath, pkg, ' ');
}

function addCoreDependency(packageJsonPath, coreSpec) {
const { indent, pkg } = readPackageJson(packageJsonPath);
pkg.devDependencies ??= {};
pkg.devDependencies['@voidzero-dev/vite-plus-core'] = coreSpec;
writePackageJson(packageJsonPath, pkg, indent);
}

function normalizeVitePaths(projectDir, tarballPath) {
const absoluteSpec = `file:${tarballPath}`;
const skippedDirectories = new Set([
'.git',
'.output',
'build',
'dist',
'node_modules',
'vendor',
]);

function rewriteValue(value, relativeSpec) {
if (value === absoluteSpec) {
return relativeSpec;
}
if (Array.isArray(value)) {
return value.map((item) => rewriteValue(item, relativeSpec));
}
if (value && typeof value === 'object') {
for (const [key, child] of Object.entries(value)) {
value[key] = rewriteValue(child, relativeSpec);
}
}
return value;
}

function visit(directory) {
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
if (!skippedDirectories.has(entry.name)) {
visit(entryPath);
}
continue;
}
if (!entry.isFile() || entry.name !== 'package.json') {
continue;
}

const text = fs.readFileSync(entryPath, 'utf8');
if (!text.includes(absoluteSpec)) {
continue;
}
const relativePath = path
.relative(path.dirname(entryPath), tarballPath)
.split(path.sep)
.join('/');
const relativeSpec = `file:${relativePath.startsWith('.') ? relativePath : `./${relativePath}`}`;
const pkg = rewriteValue(JSON.parse(text), relativeSpec);
const indent = text.match(/\n([\t ]+)"/)?.[1] ?? ' ';
writePackageJson(entryPath, pkg, indent);
}
}

visit(projectDir);
}

const [command, ...args] = process.argv.slice(2);

switch (command) {
case 'is-bun-project':
if (args.length !== 1) {
usage();
}
isBunProject(...args);
break;
case 'patch-package':
if (args.length !== 3) {
usage();
}
patchPackage(...args);
break;
case 'add-core-dependency':
if (args.length !== 2) {
usage();
}
addCoreDependency(...args);
break;
case 'normalize-vite-paths':
if (args.length !== 2) {
usage();
}
normalizeVitePaths(...args);
break;
default:
usage();
}
199 changes: 199 additions & 0 deletions .github/scripts/ensure-pkg-pr-new-pnpm-version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';

const LATEST_PNPM_10_VERSION = '10.34.4';
const SAFE_PNPM_11_VERSION = '11.9.0';
const SUPPORTED_PACKAGE_MANAGERS = new Set(['pnpm', 'yarn', 'npm', 'bun']);

function parseExactVersion(version) {
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version);
if (!match) {
return undefined;
}
return {
major: Number(match[1]),
minor: Number(match[2]),
patch: Number(match[3]),
prerelease: match[4]?.split('.'),
};
}

function compareIdentifiers(left, right) {
const leftNumber = /^\d+$/.test(left) ? Number(left) : undefined;
const rightNumber = /^\d+$/.test(right) ? Number(right) : undefined;
if (leftNumber !== undefined && rightNumber !== undefined) {
return leftNumber - rightNumber;
}
if (leftNumber !== undefined) {
return -1;
}
if (rightNumber !== undefined) {
return 1;
}
return left.localeCompare(right);
}

function compareVersions(left, right) {
for (const key of ['major', 'minor', 'patch']) {
if (left[key] !== right[key]) {
return left[key] - right[key];
}
}
if (!left.prerelease && !right.prerelease) {
return 0;
}
if (!left.prerelease) {
return 1;
}
if (!right.prerelease) {
return -1;
}
const length = Math.max(left.prerelease.length, right.prerelease.length);
for (let index = 0; index < length; index++) {
const leftIdentifier = left.prerelease[index];
const rightIdentifier = right.prerelease[index];
if (leftIdentifier === undefined) {
return -1;
}
if (rightIdentifier === undefined) {
return 1;
}
const compared = compareIdentifiers(leftIdentifier, rightIdentifier);
if (compared !== 0) {
return compared;
}
}
return 0;
}

function safePnpmVersionFor(version) {
const parsed = parseExactVersion(version);
if (!parsed) {
return undefined;
}

// pnpm before 10.2.0 rewrites non-semver overrides into peerDependencies,
// causing pkg.pr.new URLs to fail peer-spec validation. Stay on the same
// major and use the latest v10 release containing pnpm/pnpm#9000.
if (parsed.major === 10 && compareVersions(parsed, parseExactVersion('10.2.0')) < 0) {
return LATEST_PNPM_10_VERSION;
}

const pnpm11Lower = parseExactVersion('11.0.0');
const pnpm11Upper = parseExactVersion(SAFE_PNPM_11_VERSION);
if (compareVersions(parsed, pnpm11Lower) >= 0 && compareVersions(parsed, pnpm11Upper) < 0) {
return SAFE_PNPM_11_VERSION;
}

return undefined;
}

function parsePackageManagerSpec(spec) {
const match = /^([^@]+)@(.+)$/.exec(spec);
return match ? { name: match[1], version: match[2] } : undefined;
}

function devEngineEntries(pkg) {
const value = pkg.devEngines?.packageManager;
if (Array.isArray(value)) {
return value.filter((entry) => entry && typeof entry === 'object');
}
return value && typeof value === 'object' ? [value] : [];
}

function selectedDevEngineEntry(pkg) {
return devEngineEntries(pkg).find(
(entry) => typeof entry.name === 'string' && SUPPORTED_PACKAGE_MANAGERS.has(entry.name),
);
}

function serializeLike(source, pkg) {
const indentMatch = source.match(/\n([\t ]+)"/);
const indent = indentMatch?.[1].startsWith('\t') ? '\t' : (indentMatch?.[1].length ?? 2);
const newline = source.includes('\r\n') ? '\r\n' : '\n';
const finalNewline = /\r?\n$/.test(source) ? newline : '';
return JSON.stringify(pkg, null, indent).replaceAll('\n', newline) + finalNewline;
}

function replacePackageManagerSpec(source, previousSpec, targetVersion) {
const pattern = /("packageManager"\s*:\s*)("(?:\\.|[^"\\])*")/g;
return source.replace(pattern, (match, prefix, value) => {
if (JSON.parse(value) !== previousSpec) {
return match;
}
return `${prefix}${JSON.stringify(`pnpm@${targetVersion}`)}`;
});
}

export function ensureSafePkgPrNewPnpmVersion(source) {
const pkg = JSON.parse(source);
const previousVersions = [];
let packageManagerSpec;
let targetVersion;
let devEnginesChanged = false;

if (typeof pkg.packageManager === 'string') {
const parsed = parsePackageManagerSpec(pkg.packageManager);
targetVersion = parsed?.name === 'pnpm' ? safePnpmVersionFor(parsed.version) : undefined;
if (!targetVersion) {
return { changed: false, source, previousVersions };
}
packageManagerSpec = pkg.packageManager;
previousVersions.push(parsed.version);
pkg.packageManager = `pnpm@${targetVersion}`;

// Keep exact pnpm devEngines constraints in sync with the authoritative
// packageManager field so the two declarations do not conflict.
for (const entry of devEngineEntries(pkg)) {
if (
entry.name === 'pnpm' &&
typeof entry.version === 'string' &&
safePnpmVersionFor(entry.version)
) {
previousVersions.push(entry.version);
entry.version = targetVersion;
devEnginesChanged = true;
}
}
} else {
const selected = selectedDevEngineEntry(pkg);
targetVersion =
selected?.name === 'pnpm' && typeof selected.version === 'string'
? safePnpmVersionFor(selected.version)
: undefined;
if (!targetVersion || selected?.name !== 'pnpm' || typeof selected.version !== 'string') {
return { changed: false, source, previousVersions };
}
previousVersions.push(selected.version);
selected.version = targetVersion;
devEnginesChanged = true;
}

const updatedSource = devEnginesChanged
? serializeLike(source, pkg)
: replacePackageManagerSpec(source, packageManagerSpec, targetVersion);
return {
changed: true,
source: updatedSource,
previousVersions: [...new Set(previousVersions)],
version: targetVersion,
};
}

const invokedPath = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : undefined;
if (invokedPath === import.meta.url) {
const packageJsonPath = process.argv[2];
if (!packageJsonPath) {
console.error('Usage: ensure-pkg-pr-new-pnpm-version.mjs <package.json>');
process.exit(2);
}
const source = fs.readFileSync(packageJsonPath, 'utf8');
const result = ensureSafePkgPrNewPnpmVersion(source);
if (result.changed) {
fs.writeFileSync(packageJsonPath, result.source);
console.log(
`Updating project pnpm ${result.previousVersions.join(', ')} -> ${result.version} to avoid pkg.pr.new install failures`,
);
}
}
47 changes: 47 additions & 0 deletions .github/scripts/repack-vite-pr.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bash

set -euo pipefail

pr_ref="${1:-1891}"
project_input="${2:-$PWD}"

case "$pr_ref" in
'' | *[![:alnum:]._-]*)
echo "error: PR or commit contains unsupported characters: $pr_ref" >&2
exit 2
;;
esac

if [ ! -d "$project_input" ]; then
echo "error: project directory does not exist: $project_input" >&2
exit 2
fi

project_dir="$(cd "$project_input" && pwd -P)"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
output_path="$project_dir/vendor/vite-plus-core-as-vite-$pr_ref.tgz"
core_url="https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-core@$pr_ref"
vite_plus_url="https://pkg.pr.new/voidzero-dev/vite-plus/vite-plus@$pr_ref"
tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/vite-plus-core-as-vite.XXXXXX")"

cleanup() {
rm -rf "$tmp_dir"
}
trap cleanup EXIT

curl -fsSL "$core_url" -o "$tmp_dir/vite-plus-core.tgz"
mkdir -p "$tmp_dir/unpacked"
tar -xzf "$tmp_dir/vite-plus-core.tgz" -C "$tmp_dir/unpacked"

package_json="$tmp_dir/unpacked/package/package.json"
if [ ! -f "$package_json" ]; then
echo "error: downloaded package does not contain package/package.json" >&2
exit 1
fi

node "$script_dir/bun-pkg-pr-new.mjs" patch-package "$package_json" "$core_url" "$vite_plus_url"

mkdir -p "$(dirname "$output_path")"
tar -czf "$output_path" -C "$tmp_dir/unpacked" package

printf '%s\n' "$output_path"
Loading
Loading