diff --git a/.github/workflows/test-vp-create.yml b/.github/workflows/test-vp-create.yml index 2c1a5be5f3..c25a1ce717 100644 --- a/.github/workflows/test-vp-create.yml +++ b/.github/workflows/test-vp-create.yml @@ -175,9 +175,9 @@ jobs: # exact version like a real release. The vp binary was built before the # pack step pinned the package versions, so align the version explicitly. VP_VERSION: '0.0.0' - # Force full dependency rewriting so the library template's existing - # vite-plus dep gets overridden with the local build - VP_FORCE_MIGRATE: '1' + # Built-in templates must select the local version without forced migration. + # Keep the separate remote migration scenario in its existing force mode. + VP_FORCE_MIGRATE: ${{ matrix.template.verify-migration == 'true' && '1' || '' }} # yarn 4 quarantines packages published within `npmMinimalAgeGate` # (default 1440 min / 24h). When an oxlint bump landed <24h ago, the # migration step's `vp dlx @oxlint/migrate@` fails with @@ -350,15 +350,7 @@ jobs: - name: Verify local packages installed working-directory: ${{ runner.temp }}/test-project run: | - node -e " - const path = require('path'); - const pkg = require(path.resolve('node_modules/vite-plus/package.json')); - if (pkg.version !== '0.0.0') { - console.error('Expected vite-plus@0.0.0, got ' + pkg.version); - process.exit(1); - } - console.log('✓ vite-plus@' + pkg.version + ' installed correctly'); - " + node "$GITHUB_WORKSPACE/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_library_npm/assert-version.mjs" . 0.0.0 - name: Verify monorepo sub-package deps if: matrix.template.name == 'monorepo' @@ -367,8 +359,8 @@ jobs: PACKAGE_MANAGER: ${{ matrix.package-manager }} run: | # Issue 1: packages/utils inherits `vite-plus: ^x.y.z` from the - # library template. In catalog-supporting monorepos (pnpm/yarn/bun) - # the migrator must normalize it so siblings don't drift. + # library template. Creation must align it with the CLI before + # integration optionally converts the version to a catalog reference. # Issue 2: apps/website is scaffolded by create-vite which ships # `vite` (and sometimes `vitest`) in devDependencies. After # migration the scripts are rewritten to `vp ...` and `vite-plus` @@ -418,7 +410,7 @@ jobs: process.exit(1); } - if (pm !== 'npm' && appDev['vite-plus'] !== utilsDev['vite-plus']) { + if (appDev['vite-plus'] !== utilsDev['vite-plus']) { console.error('✗ vite-plus spec drift: apps/website=' + appDev['vite-plus'] + ' packages/utils=' + utilsDev['vite-plus']); process.exit(1); } @@ -498,3 +490,29 @@ jobs: exit 1 fi echo "✓ 100% cache hit verified" + + - name: Verify library added to existing workspace + if: matrix.template.name == 'monorepo' + working-directory: ${{ runner.temp }}/test-project + run: | + workspace_configs=(package.json) + for file in pnpm-workspace.yaml .yarnrc.yml; do + if [ -f "$file" ]; then + workspace_configs+=("$file") + fi + done + for file in "${workspace_configs[@]}"; do + cp "$file" "$RUNNER_TEMP/$file.before-library" + done + + vp create vite:library --directory packages/additional \ + --no-interactive --no-hooks --no-agent --no-editor + + for file in "${workspace_configs[@]}"; do + cmp "$file" "$RUNNER_TEMP/$file.before-library" + done + assert_script="$GITHUB_WORKSPACE/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_library_npm/assert-version.mjs" + node "$assert_script" . 0.0.0 + node "$assert_script" packages/additional 0.0.0 + cd packages/additional + vp test run diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_library_npm/assert-version.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_library_npm/assert-version.mjs new file mode 100644 index 0000000000..6bfa7ac594 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_library_npm/assert-version.mjs @@ -0,0 +1,26 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; + +const read = (file) => JSON.parse(fs.readFileSync(file, 'utf8')); +const directory = path.resolve(process.argv[2] ?? 'library'); +const pkg = read(path.join(directory, 'package.json')); +const version = process.argv[3] ?? pkg.overrides.vite.replace('npm:@voidzero-dev/vite-plus-core@', ''); +for (const relative of ['.', 'apps/website', 'packages/utils']) { + const manifest = path.join(directory, relative, 'package.json'); + if (relative !== '.' && !fs.existsSync(manifest)) continue; + const spec = read(manifest).devDependencies['vite-plus']; + assert.ok(spec === version || spec === 'catalog:', `${relative}: unexpected vite-plus ${spec}`); + const require = createRequire(manifest); + const cliPath = require.resolve('vite-plus/package.json'); + assert.equal(read(cliPath).version, version, `${relative}: installed vite-plus version`); + const corePath = createRequire(cliPath).resolve('vite/package.json'); + assert.equal(read(corePath).name, '@voidzero-dev/vite-plus-core'); + assert.equal(read(corePath).version, version, `${relative}: resolved core version`); +} +if (process.argv[2]) { + console.log('All scaffolded packages resolve the expected Vite+ and core versions.'); +} else { + console.log('Library dependency and installed Vite+ match the toolchain override.'); +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_library_npm/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_library_npm/snapshots.toml new file mode 100644 index 0000000000..6b83380e03 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_library_npm/snapshots.toml @@ -0,0 +1,10 @@ +[[case]] +name = "create_library_npm" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL", "CI"] +comment = "An npm library uses the creating CLI's version for both vite-plus and the Vite override." +steps = [ + { argv = ["vp", "create", "vite:library", "--directory", "library", "--package-manager", "npm", "--no-interactive", "--no-git", "--no-hooks", "--no-agent", "--no-editor"], snapshot = false, timeout = 120000 }, + { argv = ["node", "assert-version.mjs"] }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_library_npm/snapshots/create_library_npm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_library_npm/snapshots/create_library_npm.md new file mode 100644 index 0000000000..aacf62e9bc --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_library_npm/snapshots/create_library_npm.md @@ -0,0 +1,12 @@ +# create_library_npm + +An npm library uses the creating CLI's version for both vite-plus and the Vite override. + +## `vp create vite:library --directory library --package-manager npm --no-interactive --no-git --no-hooks --no-agent --no-editor` + + +## `node assert-version.mjs` + +``` +Library dependency and installed Vite+ match the toolchain override. +``` diff --git a/packages/cli/src/create/__tests__/builtin.spec.ts b/packages/cli/src/create/__tests__/builtin.spec.ts index d63370f272..3d29867546 100644 --- a/packages/cli/src/create/__tests__/builtin.spec.ts +++ b/packages/cli/src/create/__tests__/builtin.spec.ts @@ -1,5 +1,11 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + import { describe, expect, it, vi } from 'vitest'; +import { VITE_PLUS_VERSION } from '../../utils/constants.ts'; +import { readJsonFile } from '../../utils/json.ts'; import { executeBuiltinTemplate } from '../templates/builtin.js'; const { mockLogError } = vi.hoisted(() => ({ mockLogError: vi.fn() })); @@ -64,3 +70,50 @@ describe('executeBuiltinTemplate', () => { expect(mockLogError).not.toHaveBeenCalled(); }); }); + +describe('builtin library toolchain version', () => { + it.each([false, true])( + 'aligns the downloaded template with the CLI (monorepo: %s)', + async (isMonorepo) => { + const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-library-template-')); + try { + const { runRemoteTemplateCommand } = await import('../templates/remote.js'); + vi.mocked(runRemoteTemplateCommand).mockImplementationOnce(async () => { + const dir = path.join(rootDir, baseTemplateInfo.targetDir); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync( + path.join(dir, 'package.json'), + JSON.stringify({ + name: 'template-name', + devDependencies: { 'vite-plus': '^0.2.4', typescript: '^7.0.2' }, + peerDependencies: { react: '^19' }, + }), + ); + return { exitCode: 0 }; + }); + const result = await executeBuiltinTemplate( + { + ...workspaceInfo, + rootDir, + isMonorepo, + parentDirs: [], + packages: [], + downloadPackageManager: { binPrefix: '' }, + }, + { ...baseTemplateInfo, command: 'vite:library' }, + { silent: true }, + ); + expect(result.exitCode).toBe(0); + expect( + readJsonFile(path.join(rootDir, baseTemplateInfo.targetDir, 'package.json')), + ).toEqual({ + name: baseTemplateInfo.packageName, + devDependencies: { 'vite-plus': VITE_PLUS_VERSION, typescript: '^7.0.2' }, + peerDependencies: { react: '^19' }, + }); + } finally { + fs.rmSync(rootDir, { recursive: true, force: true }); + } + }, + ); +}); diff --git a/packages/cli/src/create/__tests__/monorepo-library-version.spec.ts b/packages/cli/src/create/__tests__/monorepo-library-version.spec.ts new file mode 100644 index 0000000000..133c0c60df --- /dev/null +++ b/packages/cli/src/create/__tests__/monorepo-library-version.spec.ts @@ -0,0 +1,86 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { PackageManager, type WorkspaceInfo } from '../../types/index.ts'; +import { VITE_PLUS_VERSION } from '../../utils/constants.ts'; +import { readJsonFile } from '../../utils/json.ts'; +import { executeMonorepoTemplate } from '../templates/monorepo.ts'; +import { runRemoteTemplateCommand } from '../templates/remote.ts'; +import { BuiltinTemplate, TemplateType } from '../templates/types.ts'; + +vi.mock('../templates/remote.ts', () => ({ runRemoteTemplateCommand: vi.fn() })); + +let rootDir: string; +afterEach(() => { + vi.resetAllMocks(); + if (rootDir) { + fs.rmSync(rootDir, { recursive: true, force: true }); + } +}); + +describe('monorepo library toolchain version', () => { + it.each([ + [PackageManager.npm, '12.2.0', VITE_PLUS_VERSION], + [PackageManager.pnpm, '12.8.1', 'catalog:'], + [PackageManager.yarn, '4.18.1', 'catalog:'], + [PackageManager.bun, '1.4.2', 'catalog:'], + ])('aligns the downloaded library for %s', async (packageManager, version, expected) => { + rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-monorepo-library-version-')); + const workspace: WorkspaceInfo = { + rootDir, + isMonorepo: false, + monorepoScope: '', + workspacePatterns: [], + parentDirs: [], + packages: [], + packageManager, + packageManagerVersion: version, + downloadPackageManager: { + name: packageManager, + packageName: packageManager, + version, + installDir: '', + binPrefix: '', + }, + }; + vi.mocked(runRemoteTemplateCommand).mockImplementation(async (_workspace, cwd, template) => { + const isLibrary = template.command === 'degit'; + const dir = path.join(cwd, isLibrary ? 'packages/utils' : 'apps/website'); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync( + path.join(dir, 'package.json'), + JSON.stringify({ + name: 'template', + devDependencies: isLibrary + ? { 'vite-plus': '^0.2.4', typescript: '^7.0.2', bumpp: '^11.1.0' } + : { vite: '^8.0.0', typescript: '^7.0.2' }, + }), + ); + return { exitCode: 0 }; + }); + + const result = await executeMonorepoTemplate( + workspace, + { + command: BuiltinTemplate.monorepo, + type: TemplateType.builtin, + packageName: 'my-monorepo', + targetDir: 'project', + interactive: false, + args: [], + envs: {}, + }, + { silent: true }, + ); + + expect(result.exitCode).toBe(0); + const pkg = readJsonFile(path.join(rootDir, 'project/packages/utils/package.json')); + expect(pkg).toMatchObject({ + name: 'utils', + devDependencies: { 'vite-plus': expected, bumpp: '^11.1.0' }, + }); + }); +}); diff --git a/packages/cli/src/create/templates/builtin.ts b/packages/cli/src/create/templates/builtin.ts index ee96165cd0..399a2028a7 100644 --- a/packages/cli/src/create/templates/builtin.ts +++ b/packages/cli/src/create/templates/builtin.ts @@ -9,6 +9,7 @@ import type { ExecutionWithProjectDir } from '../command.ts'; import { discoverTemplate } from '../discovery.ts'; import { setPackageName } from '../utils.ts'; import { executeGeneratorScaffold } from './generator.ts'; +import { prepareLibraryPackage } from './library.ts'; import { removeNestedLibraryToolConfig } from './monorepo.ts'; import { runRemoteTemplateCommand } from './remote.ts'; import { BuiltinTemplate, type BuiltinTemplateInfo, LibraryTemplateRepo } from './types.ts'; @@ -49,7 +50,7 @@ export async function executeBuiltinTemplate( return { exitCode: result.exitCode }; } const fullPath = path.join(workspaceInfo.rootDir, templateInfo.targetDir); - setPackageName(fullPath, templateInfo.packageName); + prepareLibraryPackage(fullPath, templateInfo.packageName); if (workspaceInfo.isMonorepo) { removeNestedLibraryToolConfig(fullPath); } diff --git a/packages/cli/src/create/templates/library.ts b/packages/cli/src/create/templates/library.ts new file mode 100644 index 0000000000..54f823bed5 --- /dev/null +++ b/packages/cli/src/create/templates/library.ts @@ -0,0 +1,18 @@ +import path from 'node:path'; + +import { VITE_PLUS_NAME, VITE_PLUS_VERSION } from '../../utils/constants.ts'; +import { editJsonFile } from '../../utils/json.ts'; + +export function prepareLibraryPackage(projectPath: string, packageName: string): void { + // The remote template can lag behind the CLI. Use the creating CLI's version + // before project integration writes its core override or catalog references. + editJsonFile<{ name?: string; devDependencies?: Record }>( + path.join(projectPath, 'package.json'), + (pkg) => { + pkg.name = packageName; + pkg.devDependencies ??= {}; + pkg.devDependencies[VITE_PLUS_NAME] = VITE_PLUS_VERSION; + return pkg; + }, + ); +} diff --git a/packages/cli/src/create/templates/monorepo.ts b/packages/cli/src/create/templates/monorepo.ts index b3ca28278d..5ce2292b0d 100644 --- a/packages/cli/src/create/templates/monorepo.ts +++ b/packages/cli/src/create/templates/monorepo.ts @@ -14,6 +14,7 @@ import { editYamlFile, readYamlFile } from '../../utils/yaml.ts'; import type { ExecutionWithProjectDir } from '../command.ts'; import { discoverTemplate } from '../discovery.ts'; import { copyDir, formatDisplayTargetDir, renameFiles, setPackageName } from '../utils.ts'; +import { prepareLibraryPackage } from './library.ts'; import { runRemoteTemplateCommand } from './remote.ts'; import { type BuiltinTemplateInfo, LibraryTemplateRepo } from './types.ts'; @@ -152,7 +153,7 @@ export async function executeMonorepoTemplate( ? `${workspaceInfo.monorepoScope}/utils` : 'utils'; const libraryProjectPath = path.join(fullPath, libraryDir); - setPackageName(libraryProjectPath, libraryPackageName); + prepareLibraryPackage(libraryProjectPath, libraryPackageName); removeNestedLibraryToolConfig(libraryProjectPath); // Perform auto-migration on the created library rewriteMonorepoProject(