Skip to content

Commit 74770fa

Browse files
committed
feat(cli): expose bundled tool versions via vite-plus/versions export
1 parent 3424da0 commit 74770fa

3 files changed

Lines changed: 198 additions & 2 deletions

File tree

packages/cli/build.ts

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
* 3. buildNapiBinding() - Builds the native Rust binding via NAPI
88
* 4. syncCorePackageExports() - Creates shim files to re-export from @voidzero-dev/vite-plus-core
99
* 5. syncTestPackageExports() - Creates shim files to re-export from @voidzero-dev/vite-plus-test
10-
* 6. copySkillDocs() - Copies docs into skills/vite-plus/docs for runtime MCP access
11-
* 7. syncReadmeFromRoot() - Keeps package README in sync
10+
* 6. syncVersionsExport() - Generates ./versions module with bundled tool versions
11+
* 7. copySkillDocs() - Copies docs into skills/vite-plus/docs for runtime MCP access
12+
* 8. syncReadmeFromRoot() - Keeps package README in sync
1213
*
1314
* The sync functions allow this package to be a drop-in replacement for 'vite' by
1415
* re-exporting all the same subpaths (./client, ./types/*, etc.) while delegating
@@ -38,6 +39,8 @@ import {
3839
} from 'typescript';
3940

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

4245
const projectDir = dirname(fileURLToPath(import.meta.url));
4346
const TEST_PACKAGE_NAME = '@voidzero-dev/vite-plus-test';
@@ -80,6 +83,7 @@ if (!skipNative) {
8083
if (!skipTs) {
8184
await syncCorePackageExports();
8285
await syncTestPackageExports();
86+
await syncVersionsExport();
8387
}
8488
await copySkillDocs();
8589
await syncReadmeFromRoot();
@@ -425,6 +429,67 @@ async function syncTestPackageExports() {
425429
console.log(`\nSynced ${Object.keys(generatedExports).length} exports from test package`);
426430
}
427431

432+
/**
433+
* Read version from a dependency's package.json in node_modules.
434+
* Uses readFile because these packages don't export ./package.json.
435+
*/
436+
async function readDepVersion(packageName: string): Promise<string | null> {
437+
try {
438+
const pkgPath = join(projectDir, 'node_modules', packageName, 'package.json');
439+
const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
440+
return pkg.version ?? null;
441+
} catch {
442+
return null;
443+
}
444+
}
445+
446+
/**
447+
* Generate ./versions export module with bundled tool versions.
448+
*
449+
* Collects versions from:
450+
* - core/test package.json bundledVersions (vite, rolldown, tsdown, vitest)
451+
* - CLI dependency package.json (oxlint, oxfmt, oxlint-tsgolint)
452+
*
453+
* Generates dist/versions.js and dist/versions.d.ts with inlined constants.
454+
*/
455+
async function syncVersionsExport() {
456+
console.log('\nSyncing versions export...');
457+
const distDir = join(projectDir, 'dist');
458+
459+
// Collect versions from bundledVersions (core + test)
460+
const versions: Record<string, string> = {
461+
...(corePkg as Record<string, any>).bundledVersions,
462+
...(testPkg as Record<string, any>).bundledVersions,
463+
};
464+
465+
// Collect versions from CLI dependencies (oxlint, oxfmt, oxlint-tsgolint)
466+
// These don't export ./package.json, so we read from node_modules directly
467+
const depTools = ['oxlint', 'oxfmt', 'oxlint-tsgolint'] as const;
468+
for (const name of depTools) {
469+
const version = await readDepVersion(name);
470+
if (version) {
471+
versions[name] = version;
472+
}
473+
}
474+
475+
// dist/versions.js — inlined constants (no runtime I/O)
476+
await writeFile(
477+
join(distDir, 'versions.js'),
478+
`export const versions = ${JSON.stringify(versions, null, 2)};\n`,
479+
);
480+
481+
// dist/versions.d.ts — type declarations
482+
const typeFields = Object.keys(versions)
483+
.map((k) => ` readonly '${k}': string;`)
484+
.join('\n');
485+
await writeFile(
486+
join(distDir, 'versions.d.ts'),
487+
`export declare const versions: {\n${typeFields}\n};\n`,
488+
);
489+
490+
console.log(` Created ./versions (${Object.keys(versions).length} tools)`);
491+
}
492+
428493
/**
429494
* Copy markdown doc files from the monorepo docs/ directory into skills/vite-plus/docs/,
430495
* preserving the relative directory structure. This keeps stable file paths for

packages/cli/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@
7575
"types": "./dist/pack.d.ts",
7676
"import": "./dist/pack.js"
7777
},
78+
"./versions": {
79+
"types": "./dist/versions.d.ts",
80+
"default": "./dist/versions.js"
81+
},
7882
"./test": {
7983
"import": {
8084
"types": "./dist/test/index.d.ts",
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* Verify that the vite-plus/versions export works correctly.
3+
*
4+
* Tests run against the already-built dist/ directory, ensuring
5+
* that syncVersionsExport() produces correct artifacts.
6+
*/
7+
import fs from 'node:fs';
8+
import path from 'node:path';
9+
import url from 'node:url';
10+
11+
import { describe, expect, it } from '@voidzero-dev/vite-plus-test';
12+
13+
const cliPkgDir = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '../..');
14+
const distDir = path.join(cliPkgDir, 'dist');
15+
const corePkgPath = path.join(cliPkgDir, '../core/package.json');
16+
const testPkgPath = path.join(cliPkgDir, '../test/package.json');
17+
18+
describe('versions export', () => {
19+
describe('build artifacts', () => {
20+
it('dist/versions.js should exist', () => {
21+
expect(fs.existsSync(path.join(distDir, 'versions.js'))).toBe(true);
22+
});
23+
24+
it('dist/versions.d.ts should exist', () => {
25+
expect(fs.existsSync(path.join(distDir, 'versions.d.ts'))).toBe(true);
26+
});
27+
28+
it('dist/versions.js should export a versions object', () => {
29+
const content = fs.readFileSync(path.join(distDir, 'versions.js'), 'utf-8');
30+
expect(content).toContain('export const versions');
31+
});
32+
33+
it('dist/versions.d.ts should declare a versions type', () => {
34+
const content = fs.readFileSync(path.join(distDir, 'versions.d.ts'), 'utf-8');
35+
expect(content).toContain('export declare const versions');
36+
});
37+
});
38+
39+
describe('bundledVersions consistency', () => {
40+
it('should contain all core bundledVersions', () => {
41+
const corePkg = JSON.parse(fs.readFileSync(corePkgPath, 'utf-8'));
42+
const content = fs.readFileSync(path.join(distDir, 'versions.js'), 'utf-8');
43+
for (const [key, value] of Object.entries(
44+
corePkg.bundledVersions as Record<string, string>,
45+
)) {
46+
expect(content).toContain(`${key}:`);
47+
expect(content).toContain(`'${value}'`);
48+
}
49+
});
50+
51+
it('should contain all test bundledVersions', () => {
52+
const testPkg = JSON.parse(fs.readFileSync(testPkgPath, 'utf-8'));
53+
const content = fs.readFileSync(path.join(distDir, 'versions.js'), 'utf-8');
54+
for (const [key, value] of Object.entries(
55+
testPkg.bundledVersions as Record<string, string>,
56+
)) {
57+
expect(content).toContain(`${key}:`);
58+
expect(content).toContain(`'${value}'`);
59+
}
60+
});
61+
});
62+
63+
describe('dependency tool versions', () => {
64+
it('should contain oxlint version', () => {
65+
const content = fs.readFileSync(path.join(distDir, 'versions.js'), 'utf-8');
66+
expect(content).toContain('oxlint:');
67+
});
68+
69+
it('should contain oxfmt version', () => {
70+
const content = fs.readFileSync(path.join(distDir, 'versions.js'), 'utf-8');
71+
expect(content).toContain('oxfmt:');
72+
});
73+
74+
it('should contain oxlint-tsgolint version', () => {
75+
const content = fs.readFileSync(path.join(distDir, 'versions.js'), 'utf-8');
76+
expect(content).toContain('oxlint-tsgolint');
77+
});
78+
});
79+
80+
describe('type declarations', () => {
81+
it('should have type fields for all bundled tools', () => {
82+
const content = fs.readFileSync(path.join(distDir, 'versions.d.ts'), 'utf-8');
83+
const expectedKeys = [
84+
'vite',
85+
'rolldown',
86+
'tsdown',
87+
'vitest',
88+
'oxlint',
89+
'oxfmt',
90+
'oxlint-tsgolint',
91+
];
92+
for (const key of expectedKeys) {
93+
expect(content).toContain(key);
94+
}
95+
});
96+
97+
it('should declare all fields as readonly string', () => {
98+
const content = fs.readFileSync(path.join(distDir, 'versions.d.ts'), 'utf-8');
99+
const fieldMatches = content.match(/readonly [\w'-]+: string;/g);
100+
expect(fieldMatches).not.toBeNull();
101+
expect(fieldMatches!.length).toBeGreaterThanOrEqual(7);
102+
});
103+
});
104+
105+
describe('runtime import', () => {
106+
it('should be importable and return an object with expected keys', async () => {
107+
const { versions } = await import('../../dist/versions.js');
108+
expect(versions).toBeDefined();
109+
expect(typeof versions).toBe('object');
110+
expect(versions.vite).toBeTypeOf('string');
111+
expect(versions.rolldown).toBeTypeOf('string');
112+
expect(versions.tsdown).toBeTypeOf('string');
113+
expect(versions.vitest).toBeTypeOf('string');
114+
expect(versions.oxlint).toBeTypeOf('string');
115+
expect(versions.oxfmt).toBeTypeOf('string');
116+
expect(versions['oxlint-tsgolint']).toBeTypeOf('string');
117+
});
118+
119+
it('should have valid semver-like versions', async () => {
120+
const { versions } = await import('../../dist/versions.js');
121+
const semverPattern = /^\d+\.\d+\.\d+/;
122+
for (const [key, value] of Object.entries(versions as Record<string, string>)) {
123+
expect(value, `${key} should be a valid version`).toMatch(semverPattern);
124+
}
125+
});
126+
});
127+
});

0 commit comments

Comments
 (0)