Skip to content
Open
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
77 changes: 75 additions & 2 deletions packages/cli/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
* 3. buildNapiBinding() - Builds the native Rust binding via NAPI
* 4. syncCorePackageExports() - Creates shim files to re-export from @voidzero-dev/vite-plus-core
* 5. syncTestPackageExports() - Creates shim files to re-export from @voidzero-dev/vite-plus-test
* 6. copySkillDocs() - Copies docs into skills/vite-plus/docs for runtime MCP access
* 7. syncReadmeFromRoot() - Keeps package README in sync
* 6. syncVersionsExport() - Generates ./versions module with bundled tool versions
* 7. copySkillDocs() - Copies docs into skills/vite-plus/docs for runtime MCP access
* 8. syncReadmeFromRoot() - Keeps package README in sync
*
* The sync functions allow this package to be a drop-in replacement for 'vite' by
* re-exporting all the same subpaths (./client, ./types/*, etc.) while delegating
Expand Down Expand Up @@ -38,6 +39,8 @@ import {
} from 'typescript';

import { generateLicenseFile } from '../../scripts/generate-license.ts';
import corePkg from '../core/package.json' with { type: 'json' };
import testPkg from '../test/package.json' with { type: 'json' };

const projectDir = dirname(fileURLToPath(import.meta.url));
const TEST_PACKAGE_NAME = '@voidzero-dev/vite-plus-test';
Expand Down Expand Up @@ -80,6 +83,7 @@ if (!skipNative) {
if (!skipTs) {
await syncCorePackageExports();
await syncTestPackageExports();
await syncVersionsExport();
}
await copySkillDocs();
await syncReadmeFromRoot();
Expand Down Expand Up @@ -425,6 +429,75 @@ async function syncTestPackageExports() {
console.log(`\nSynced ${Object.keys(generatedExports).length} exports from test package`);
}

/**
* Read version from a dependency's package.json in node_modules.
* Uses readFile because these packages don't export ./package.json.
*
* TODO: Once https://github.com/oxc-project/oxc/pull/20784 lands and oxlint/oxfmt/oxlint-tsgolint
* export ./package.json, this function can be removed and replaced with static imports:
* ```js
* import oxlintPkg from 'oxlint/package.json' with { type: 'json' };
* import oxfmtPkg from 'oxfmt/package.json' with { type: 'json' };
* import oxlintTsgolintPkg from 'oxlint-tsgolint/package.json' with { type: 'json' };
* ```
*/
async function readDepVersion(packageName: string): Promise<string | null> {
try {
const pkgPath = join(projectDir, 'node_modules', packageName, 'package.json');
const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
return pkg.version ?? null;
} catch {
return null;
}
}

/**
* Generate ./versions export module with bundled tool versions.
*
* Collects versions from:
* - core/test package.json bundledVersions (vite, rolldown, tsdown, vitest)
* - CLI dependency package.json (oxlint, oxfmt, oxlint-tsgolint)
*
* Generates dist/versions.js and dist/versions.d.ts with inlined constants.
*/
async function syncVersionsExport() {
console.log('\nSyncing versions export...');
const distDir = join(projectDir, 'dist');

// Collect versions from bundledVersions (core + test)
const versions: Record<string, string> = {
...(corePkg as Record<string, any>).bundledVersions,
...(testPkg as Record<string, any>).bundledVersions,
};

// Collect versions from CLI dependencies (oxlint, oxfmt, oxlint-tsgolint)
// These don't export ./package.json, so we read from node_modules directly
const depTools = ['oxlint', 'oxfmt', 'oxlint-tsgolint'] as const;
for (const name of depTools) {
const version = await readDepVersion(name);
if (version) {
versions[name] = version;
}
}

// dist/versions.js — inlined constants (no runtime I/O)
await writeFile(
join(distDir, 'versions.js'),
`export const versions = ${JSON.stringify(versions, null, 2)};\n`,
);

// dist/versions.d.ts — type declarations
const typeFields = Object.keys(versions)
.map((k) => ` readonly '${k}': string;`)
.join('\n');
await writeFile(
join(distDir, 'versions.d.ts'),
`export declare const versions: {\n${typeFields}\n};\n`,
);

console.log(` Created ./versions (${Object.keys(versions).length} tools)`);
}

/**
* Copy markdown doc files from the monorepo docs/ directory into skills/vite-plus/docs/,
* preserving the relative directory structure. This keeps stable file paths for
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@
"types": "./dist/pack.d.ts",
"import": "./dist/pack.js"
},
"./versions": {
"types": "./dist/versions.d.ts",
"default": "./dist/versions.js"
},
"./test": {
"import": {
"types": "./dist/test/index.d.ts",
Expand Down
130 changes: 130 additions & 0 deletions packages/cli/src/__tests__/versions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* Verify that the vite-plus/versions export works correctly.
*
* Tests run against the already-built dist/ directory, ensuring
* that syncVersionsExport() produces correct artifacts.
*/
import fs from 'node:fs';
import path from 'node:path';
import url from 'node:url';

import { describe, expect, it } from '@voidzero-dev/vite-plus-test';

const cliPkgDir = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '../..');
const distDir = path.join(cliPkgDir, 'dist');
const corePkgPath = path.join(cliPkgDir, '../core/package.json');
const testPkgPath = path.join(cliPkgDir, '../test/package.json');

describe('versions export', () => {
describe('build artifacts', () => {
it('dist/versions.js should exist', () => {
expect(fs.existsSync(path.join(distDir, 'versions.js'))).toBe(true);
});

it('dist/versions.d.ts should exist', () => {
expect(fs.existsSync(path.join(distDir, 'versions.d.ts'))).toBe(true);
});

it('dist/versions.js should export a versions object', () => {
const content = fs.readFileSync(path.join(distDir, 'versions.js'), 'utf-8');
expect(content).toContain('export const versions');
});

it('dist/versions.d.ts should declare a versions type', () => {
const content = fs.readFileSync(path.join(distDir, 'versions.d.ts'), 'utf-8');
expect(content).toContain('export declare const versions');
});
});

describe('bundledVersions consistency', () => {
it('should contain all core bundledVersions', async () => {
const corePkg = JSON.parse(fs.readFileSync(corePkgPath, 'utf-8'));
const mod = await import('../../dist/versions.js');
const versions = mod.versions as Record<string, string>;
for (const [key, value] of Object.entries(
corePkg.bundledVersions as Record<string, string>,
)) {
expect(versions[key], `versions.${key} should match core bundledVersions`).toBe(value);
}
});

it('should contain all test bundledVersions', async () => {
const testPkg = JSON.parse(fs.readFileSync(testPkgPath, 'utf-8'));
const mod = await import('../../dist/versions.js');
const versions = mod.versions as Record<string, string>;
for (const [key, value] of Object.entries(
testPkg.bundledVersions as Record<string, string>,
)) {
expect(versions[key], `versions.${key} should match test bundledVersions`).toBe(value);
}
});
});

describe('dependency tool versions', () => {
it('should contain oxlint version', async () => {
const mod = await import('../../dist/versions.js');
const versions = mod.versions as Record<string, string>;
expect(versions.oxlint).toBeTypeOf('string');
});

it('should contain oxfmt version', async () => {
const mod = await import('../../dist/versions.js');
const versions = mod.versions as Record<string, string>;
expect(versions.oxfmt).toBeTypeOf('string');
});

it('should contain oxlint-tsgolint version', async () => {
const mod = await import('../../dist/versions.js');
const versions = mod.versions as Record<string, string>;
expect(versions['oxlint-tsgolint']).toBeTypeOf('string');
});
});

describe('type declarations', () => {
it('should have type fields for all bundled tools', () => {
const content = fs.readFileSync(path.join(distDir, 'versions.d.ts'), 'utf-8');
const expectedKeys = [
'vite',
'rolldown',
'tsdown',
'vitest',
'oxlint',
'oxfmt',
'oxlint-tsgolint',
];
for (const key of expectedKeys) {
expect(content).toContain(key);
}
});

it('should declare all fields as readonly string', () => {
const content = fs.readFileSync(path.join(distDir, 'versions.d.ts'), 'utf-8');
const fieldMatches = content.match(/readonly [\w'-]+: string;/g);
expect(fieldMatches).not.toBeNull();
expect(fieldMatches!.length).toBeGreaterThanOrEqual(7);
});
});

describe('runtime import', () => {
it('should be importable and return an object with expected keys', async () => {
const { versions } = await import('../../dist/versions.js');
expect(versions).toBeDefined();
expect(typeof versions).toBe('object');
expect(versions.vite).toBeTypeOf('string');
expect(versions.rolldown).toBeTypeOf('string');
expect(versions.tsdown).toBeTypeOf('string');
expect(versions.vitest).toBeTypeOf('string');
expect(versions.oxlint).toBeTypeOf('string');
expect(versions.oxfmt).toBeTypeOf('string');
expect(versions['oxlint-tsgolint']).toBeTypeOf('string');
});

it('should have valid semver-like versions', async () => {
const { versions } = await import('../../dist/versions.js');
const semverPattern = /^\d+\.\d+\.\d+/;
for (const [key, value] of Object.entries(versions as Record<string, string>)) {
expect(value, `${key} should be a valid version`).toMatch(semverPattern);
}
});
});
});
Loading