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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ VITE+ - The Unified Toolchain for the Web
Package: cowsay@1.6.0
Binaries: cowsay, cowthink
Node: <version>
Installed: 2026-07-10
Installed: <date>
```

## `vp remove -g cowsay`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ check website keeps aliased vite for pnpm (workspace override stays effective)
"preview": "vp preview"
},
"devDependencies": {
"typescript": "~6.0.2",
"typescript": "^7.0.2",
"vite": "catalog:",
"vite-plus": "catalog:"
}
Expand Down Expand Up @@ -239,10 +239,9 @@ check utils normalizes vite-plus to catalog:
"prepublishOnly": "vp run build"
},
"devDependencies": {
"@types/node": "^25.6.2",
"@typescript/native-preview": "7.0.0-dev.20260509.2",
"@types/node": "^26.1.1",
"bumpp": "^11.1.0",
"typescript": "^6.0.3",
"typescript": "^7.0.2",
"vite": "catalog:",
"vite-plus": "catalog:"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ check website uses catalog:
"preview": "vp preview"
},
"devDependencies": {
"typescript": "~6.0.2",
"typescript": "^7.0.2",
"vite-plus": "catalog:"
}
}
Expand Down Expand Up @@ -155,10 +155,9 @@ check utils normalizes vite-plus to catalog:
"prepublishOnly": "vp run build"
},
"devDependencies": {
"@types/node": "^25.6.2",
"@typescript/native-preview": "7.0.0-dev.20260509.2",
"@types/node": "^26.1.1",
"bumpp": "^11.1.0",
"typescript": "^6.0.3",
"typescript": "^7.0.2",
"vite-plus": "catalog:"
}
}
Expand Down
7 changes: 7 additions & 0 deletions crates/vite_cli_snapshots/tests/cli_snapshots/redact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ static MANAGED_TEST_VERSION_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
static WHICH_NODE_VERSION_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r#"(Node:\s+|"nodeVersion":\s*")\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?"#).unwrap()
});
// `vp env which` reports the wall-clock day on which a managed runtime was
// installed. The date describes the runner cache, not fixture behavior.
static WHICH_INSTALLED_DATE_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"(Installed:\s+)\d{4}-\d{2}-\d{2}").unwrap());
// Output bytes differ across OSes (line endings, embedded paths), so byte
// sizes and content-derived asset hashes can never be part of a shared
// snapshot. The unit is kept ("<size> kB"): it only changes when content
Expand Down Expand Up @@ -386,6 +390,9 @@ pub fn redact_output(
// context (see WHICH_NODE_VERSION_RE).
output = WHICH_NODE_VERSION_RE.replace_all(&output, "${1}<version>").into_owned();

// Redact the managed runtime's environment-specific installation date.
output = WHICH_INSTALLED_DATE_RE.replace_all(&output, "${1}<date>").into_owned();

// Redact thread counts like "16 threads" to "<n> threads"
output = THREAD_RE.replace_all(&output, "<n> threads").into_owned();

Expand Down
9 changes: 9 additions & 0 deletions crates/vite_cli_snapshots/tests/redact_unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ fn masks_bare_runtime_tool_versions_by_name_context() {
);
}

#[test]
fn masks_managed_runtime_install_date_by_label_context() {
let input = " Installed: 2026-07-13\n Created: fixture-value\n".to_owned();
assert_eq!(
redact_output(input, &[], true),
" Installed: <date>\n Created: fixture-value\n"
);
}

#[test]
fn masks_vite_plus_version_by_context_only() {
// The workspace vite-plus/core version bumps every release and is masked by
Expand Down
101 changes: 101 additions & 0 deletions packages/cli/src/__tests__/typescript-dts.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

import type { ResolvedConfig } from '@voidzero-dev/vite-plus-core/pack';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import { configureTypeScript7Dts } from '../utils/typescript-dts.js';

function writeJson(file: string, value: unknown): void {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, JSON.stringify(value));
}

describe('configureTypeScript7Dts', () => {
let cwd: string;

beforeEach(() => {
cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-typescript-dts-'));
fs.writeFileSync(path.join(cwd, 'package.json'), '{}');
});

afterEach(() => {
fs.rmSync(cwd, { recursive: true, force: true });
});

function installTypeScript(version: string, includeCompiler = true): string {
writeJson(path.join(cwd, 'node_modules/typescript/package.json'), {
name: 'typescript',
version,
exports: { './package.json': './package.json' },
});

const platformPackage = `typescript-${process.platform}-${process.arch}`;
const platformRoot = path.join(cwd, 'node_modules/@typescript', platformPackage);
if (includeCompiler) {
writeJson(path.join(platformRoot, 'package.json'), {
name: `@typescript/${platformPackage}`,
version,
exports: { './package.json': './package.json' },
});
const executable = path.join(
platformRoot,
'lib',
process.platform === 'win32' ? 'tsc.exe' : 'tsc',
);
fs.mkdirSync(path.dirname(executable), { recursive: true });
fs.writeFileSync(executable, '');
return executable;
}
return path.join(platformRoot, 'lib', 'tsc');
}

function config(dts: ResolvedConfig['dts']): ResolvedConfig {
return { cwd, dts } as ResolvedConfig;
}

it('uses the native compiler shipped with TypeScript 7', () => {
const executable = installTypeScript('7.0.2');
const resolved = config({});

configureTypeScript7Dts(resolved);

expect(resolved.dts && resolved.dts.tsgo).toEqual({ path: fs.realpathSync(executable) });
});

it('preserves a custom native compiler path', () => {
installTypeScript('7.0.2', false);
const resolved = config({ tsgo: { path: '/custom/tsc' } });

configureTypeScript7Dts(resolved);

expect(resolved.dts && resolved.dts.tsgo).toEqual({ path: '/custom/tsc' });
});

it('leaves the Oxc declaration pipeline unchanged', () => {
installTypeScript('7.0.2', false);
const resolved = config({ oxc: true });

configureTypeScript7Dts(resolved);

expect(resolved.dts).toEqual({ oxc: true });
});

it('rejects an explicitly disabled native declaration pipeline', () => {
installTypeScript('7.0.2', false);

expect(() => configureTypeScript7Dts(config({ tsgo: false }))).toThrow(
'TypeScript 7 declaration generation requires',
);
});

it('keeps the legacy pipeline for TypeScript 6', () => {
installTypeScript('6.0.2', false);
const resolved = config({});

configureTypeScript7Dts(resolved);

expect(resolved.dts).toEqual({});
});
});
78 changes: 64 additions & 14 deletions packages/cli/src/create/__tests__/monorepo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,67 @@ import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import { PackageManager } from '../../types/index.js';
import { dropAliasedRuntimeDevDeps } from '../templates/monorepo.js';
import {
alignMonorepoTypeScriptVersion,
dropAliasedRuntimeDevDeps,
} from '../templates/monorepo.js';

function writePackageJson(directory: string, devDependencies: Record<string, string>): void {
fs.writeFileSync(
path.join(directory, 'package.json'),
JSON.stringify({ private: true, devDependencies }, null, 2),
);
}

function readDevDependencies(directory: string): Record<string, string> {
const pkg = JSON.parse(fs.readFileSync(path.join(directory, 'package.json'), 'utf8')) as {
devDependencies?: Record<string, string>;
};
return pkg.devDependencies ?? {};
}

describe('alignMonorepoTypeScriptVersion', () => {
let tmpDir: string;
let appDir: string;
let libraryDir: string;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-monorepo-typescript-'));
appDir = path.join(tmpDir, 'apps', 'website');
libraryDir = path.join(tmpDir, 'packages', 'utils');
fs.mkdirSync(appDir, { recursive: true });
fs.mkdirSync(libraryDir, { recursive: true });
});

afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});

it('aligns the app with the library TypeScript range', () => {
writePackageJson(appDir, { typescript: '~6.0.2', 'vite-plus': 'catalog:' });
writePackageJson(libraryDir, {
typescript: '^7.0.2',
'@types/node': '^26.1.1',
'vite-plus': 'catalog:',
});

alignMonorepoTypeScriptVersion(appDir, libraryDir);

expect(readDevDependencies(appDir)).toEqual({
typescript: '^7.0.2',
'vite-plus': 'catalog:',
});
});

it('leaves the app unchanged when it has no TypeScript dependency', () => {
writePackageJson(appDir, { 'vite-plus': 'catalog:' });
writePackageJson(libraryDir, { typescript: '^7.0.2', 'vite-plus': 'catalog:' });

alignMonorepoTypeScriptVersion(appDir, libraryDir);

expect(readDevDependencies(appDir).typescript).toBeUndefined();
});
});

describe('dropAliasedRuntimeDevDeps', () => {
let tmpDir: string;
Expand All @@ -19,17 +79,7 @@ describe('dropAliasedRuntimeDevDeps', () => {
});

function writeWebsitePackageJson(devDependencies: Record<string, string>): void {
fs.writeFileSync(
path.join(tmpDir, 'package.json'),
JSON.stringify({ name: 'website', private: true, devDependencies }, null, 2),
);
}

function readDevDependencies(): Record<string, string> {
const pkg = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8')) as {
devDependencies?: Record<string, string>;
};
return pkg.devDependencies ?? {};
writePackageJson(tmpDir, devDependencies);
}

// Regression test for "vp why vite reports the override as ineffective" in a
Expand All @@ -48,7 +98,7 @@ describe('dropAliasedRuntimeDevDeps', () => {

dropAliasedRuntimeDevDeps(tmpDir, PackageManager.pnpm);

const devDependencies = readDevDependencies();
const devDependencies = readDevDependencies(tmpDir);
expect(devDependencies.vite).toBe('catalog:');
expect(devDependencies.vitest).toBe('catalog:');
expect(devDependencies['vite-plus']).toBe('catalog:');
Expand All @@ -68,7 +118,7 @@ describe('dropAliasedRuntimeDevDeps', () => {

dropAliasedRuntimeDevDeps(tmpDir, packageManager);

const devDependencies = readDevDependencies();
const devDependencies = readDevDependencies(tmpDir);
expect(devDependencies.vite).toBeUndefined();
expect(devDependencies.vitest).toBeUndefined();
expect(devDependencies['vite-plus']).toBe('latest');
Expand Down
42 changes: 42 additions & 0 deletions packages/cli/src/create/templates/monorepo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,51 @@ export async function executeMonorepoTemplate(
options?.silent ?? false,
);

alignMonorepoTypeScriptVersion(appProjectPath, libraryProjectPath);

return { exitCode: 0, projectDir: templateInfo.targetDir };
}

/**
* Keep the built-in app and library on the same TypeScript version.
*
* The two projects come from independently updated remote templates. If their
* TypeScript majors drift apart, package managers either create separate
* vite-plus peer contexts (pnpm) or hoist a compiler that the bundled tsdown
* resolver cannot use for the other workspace (npm/Yarn). The library
* template is the compatibility baseline because its declaration build may
* require a newer compiler, so the app adopts its TypeScript range instead of
* silently downgrading the library.
*/
export function alignMonorepoTypeScriptVersion(
appProjectPath: string,
libraryProjectPath: string,
): void {
const libraryPackage = JSON.parse(
fs.readFileSync(path.join(libraryProjectPath, 'package.json'), 'utf8'),
) as {
devDependencies?: Record<string, string>;
};
const typescriptVersion = libraryPackage.devDependencies?.typescript;
if (!typescriptVersion) {
return;
}

editJsonFile<{ devDependencies?: Record<string, string> }>(
path.join(appProjectPath, 'package.json'),
(pkg) => {
if (
!pkg.devDependencies?.typescript ||
pkg.devDependencies.typescript === typescriptVersion
) {
return undefined;
}
pkg.devDependencies.typescript = typescriptVersion;
return pkg;
},
);
}

/**
* Drop the aliased `vite` / `vitest` devDeps that `create-vite` leaves on a
* scaffolded sub-package. After migration its scripts already use `vp ...` and
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/pack-bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { cac } from 'cac';

import { resolveViteConfig } from './resolve-vite-config.ts';
import { configureTypeScript7Dts } from './utils/typescript-dts.ts';

// Matches a `.d.ts` / `.d.mts` / `.d.cts` importer.
const RE_DTS = /\.d\.[cm]?ts$/;
Expand Down Expand Up @@ -166,6 +167,9 @@ cli
merged.plugins = [...existingPlugins, externalDtsTypeOnlyPlugin()];
}
const resolvedConfig = await resolveUserConfig(merged, flags, configDeps);
for (const config of resolvedConfig) {
configureTypeScript7Dts(config);
}
configs.push(...resolvedConfig);
}

Expand Down
Loading
Loading