Skip to content

Commit ad9ea44

Browse files
fix(cli): bundle ui-dist and prefer it at runtime
prepare ui-dist during prepack and include it in published files remove runtime UI build dependency from static generation path prefer bundled assets in buildReactUI and keep HTML fallback safety update CLI tests for bundled-asset resolution behavior Signed-off-by: night-slayer18 <samanuaia257@gmail.com>
1 parent 752822e commit ad9ea44

6 files changed

Lines changed: 94 additions & 67 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,4 @@ Thumbs.db
5555
# Autodocs
5656
docs-dist
5757
.autodocs-cache
58+
packages/cli/ui-dist

packages/cli/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@
66
"autodocs": "dist/index.js"
77
},
88
"files": [
9-
"dist"
9+
"dist",
10+
"ui-dist"
1011
],
1112
"scripts": {
1213
"build": "tsup src/index.ts src/index-exports.ts --format cjs --dts --no-splitting",
14+
"prepare:ui-dist": "node scripts/prepare-ui-dist.js",
15+
"prepack": "npm run prepare:ui-dist",
1316
"dev": "tsup src/index.ts --format cjs --watch",
1417
"lint": "eslint .",
1518
"test": "jest",
1619
"type-check": "tsc --noEmit",
17-
"clean": "rm -rf dist"
20+
"clean": "rm -rf dist ui-dist"
1821
},
1922
"exports": {
2023
".": {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs/promises');
4+
const path = require('path');
5+
6+
async function pathExists(target) {
7+
try {
8+
await fs.access(target);
9+
return true;
10+
} catch {
11+
return false;
12+
}
13+
}
14+
15+
async function main() {
16+
const cliDir = path.resolve(__dirname, '..');
17+
const defaultUiDistDir = path.resolve(cliDir, '../ui/dist');
18+
const sourceUiDistDir = process.env.AUTODOCS_UI_DIST_SOURCE
19+
? path.resolve(process.env.AUTODOCS_UI_DIST_SOURCE)
20+
: defaultUiDistDir;
21+
const targetUiDistDir = path.resolve(cliDir, 'ui-dist');
22+
23+
if (!(await pathExists(sourceUiDistDir))) {
24+
console.error(
25+
`[prepare-ui-dist] Missing UI build at ${sourceUiDistDir}. Run "npm -w packages/ui run build" first.`
26+
);
27+
process.exit(1);
28+
}
29+
30+
await fs.rm(targetUiDistDir, { recursive: true, force: true });
31+
await fs.cp(sourceUiDistDir, targetUiDistDir, { recursive: true });
32+
33+
console.log(`[prepare-ui-dist] Copied ${sourceUiDistDir} -> ${targetUiDistDir}`);
34+
}
35+
36+
main().catch((error) => {
37+
const message = error instanceof Error ? error.message : String(error);
38+
console.error(`[prepare-ui-dist] Failed: ${message}`);
39+
process.exit(1);
40+
});

packages/cli/src/commands/build.ts

Lines changed: 47 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import chalk from 'chalk';
44
import ora from 'ora';
55
import { Command } from 'commander';
66
import { glob } from 'glob';
7-
import { exec } from 'child_process';
8-
import { promisify } from 'util';
97
import fsPromises from 'fs/promises';
108
import type { CompilerOptions, Diagnostic } from 'typescript';
119
import { loadConfig, resolveConfigPaths } from '../config';
@@ -22,12 +20,24 @@ import {
2220
VERSION,
2321
} from '@opensyntaxhq/autodocs-core';
2422

25-
const execAsync = promisify(exec);
26-
2723
function toCompilerOptions(options?: Record<string, unknown>): CompilerOptions | undefined {
2824
return options ? (options as unknown as CompilerOptions) : undefined;
2925
}
3026

27+
interface UiSourceCandidate {
28+
label: string;
29+
distDir: string;
30+
}
31+
32+
async function pathExists(target: string): Promise<boolean> {
33+
try {
34+
await fsPromises.access(target);
35+
return true;
36+
} catch {
37+
return false;
38+
}
39+
}
40+
3141
interface BuildOptions {
3242
config?: string;
3343
output?: string;
@@ -258,50 +268,49 @@ export async function buildReactUI(
258268
siteName?: string;
259269
}
260270
): Promise<void> {
261-
// Find the UI package using require.resolve - works in monorepo
262-
let uiDir: string;
263-
let uiDistDir: string;
271+
const candidates: UiSourceCandidate[] = [];
272+
const candidateDistDirs = new Set<string>();
273+
const addCandidate = (candidate: UiSourceCandidate): void => {
274+
if (candidateDistDirs.has(candidate.distDir)) {
275+
return;
276+
}
277+
candidateDistDirs.add(candidate.distDir);
278+
candidates.push(candidate);
279+
};
264280

265281
if (options.uiDir) {
266-
uiDir = options.uiDir;
267-
uiDistDir = path.join(uiDir, 'dist');
282+
addCandidate({
283+
label: `custom uiDir (${options.uiDir})`,
284+
distDir: path.join(options.uiDir, 'dist'),
285+
});
268286
} else {
269-
try {
270-
// Try to resolve the UI package from the monorepo
271-
const uiPackageJson = require.resolve('@opensyntaxhq/autodocs-ui/package.json');
272-
uiDir = path.dirname(uiPackageJson);
273-
uiDistDir = path.join(uiDir, 'dist');
274-
} catch {
275-
// Fallback: resolve relative to CLI package in monorepo
276-
uiDir = path.resolve(__dirname, '../../ui');
277-
uiDistDir = path.join(uiDir, 'dist');
278-
}
287+
// Bundled UI assets are the primary source for published CLI packages.
288+
addCandidate({
289+
label: 'bundled CLI assets',
290+
distDir: path.resolve(__dirname, '../ui-dist'),
291+
});
292+
addCandidate({
293+
label: 'bundled CLI assets',
294+
distDir: path.resolve(__dirname, '../../ui-dist'),
295+
});
279296
}
280297

281-
// Check if UI package exists
282-
try {
283-
await fsPromises.access(uiDir);
284-
} catch {
285-
// UI package not found, fall back to basic HTML
286-
spinner.text = 'React UI not found, using basic HTML generator...';
287-
const { generateHtml } = await import('@opensyntaxhq/autodocs-core');
288-
await generateHtml(docs, outputDir);
289-
return;
298+
let selectedSource: UiSourceCandidate | null = null;
299+
for (const candidate of candidates) {
300+
if (await pathExists(candidate.distDir)) {
301+
selectedSource = candidate;
302+
break;
303+
}
290304
}
291305

292-
// Step 1: Build the React UI
293-
spinner.text = 'Building React UI...';
294-
295-
try {
296-
await execAsync('npm run build', { cwd: uiDir });
297-
} catch {
298-
spinner.fail(chalk.red('Failed to build React UI, falling back to basic HTML'));
306+
if (!selectedSource) {
307+
spinner.text = 'React UI not found, using basic HTML generator...';
299308
const { generateHtml } = await import('@opensyntaxhq/autodocs-core');
300309
await generateHtml(docs, outputDir);
301310
return;
302311
}
303312

304-
spinner.succeed(chalk.green('React UI built'));
313+
spinner.succeed(chalk.green(`React UI ready (${selectedSource.label})`));
305314

306315
// Step 2: Clean and create output directory
307316
spinner.start('Preparing output directory...');
@@ -310,9 +319,9 @@ export async function buildReactUI(
310319
await fsPromises.mkdir(outputDir, { recursive: true });
311320

312321
// Step 3: Copy React UI assets
313-
spinner.text = 'Copying UI assets...';
322+
spinner.text = `Copying UI assets (${selectedSource.label})...`;
314323

315-
await copyDirectory(uiDistDir, outputDir);
324+
await copyDirectory(selectedSource.distDir, outputDir);
316325

317326
spinner.succeed(chalk.green('UI assets copied'));
318327

packages/cli/tests/build-helpers.test.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,6 @@ jest.mock('../src/config', () => ({
2727
resolveConfigPaths: jest.fn(),
2828
}));
2929

30-
jest.mock('child_process', () => ({
31-
exec: jest.fn(),
32-
}));
33-
3430
const pluginManagerInstances: Array<{ cleanup: jest.Mock; runHook: jest.Mock }> = [];
3531

3632
jest.mock('@opensyntaxhq/autodocs-core', () => ({
@@ -53,7 +49,6 @@ jest.mock('@opensyntaxhq/autodocs-core', () => ({
5349
}));
5450

5551
import { glob } from 'glob';
56-
import { ChildProcess, exec } from 'child_process';
5752
import { loadConfig, resolveConfigPaths } from '../src/config';
5853
import {
5954
createProgram,
@@ -66,8 +61,6 @@ import {
6661
import { loadPlugins, writeStaticDocs, buildReactUI, registerBuild } from '../src/commands/build';
6762

6863
const globMock = glob as unknown as jest.MockedFunction<typeof glob>;
69-
const execMock = exec as unknown as jest.MockedFunction<typeof exec>;
70-
const createChildProcess = (): ChildProcess => ({ pid: 0 }) as ChildProcess;
7164

7265
describe('build helpers', () => {
7366
beforeEach(() => {
@@ -253,17 +246,10 @@ describe('build helpers', () => {
253246
expect(generateHtml).toHaveBeenCalled();
254247
});
255248

256-
it('falls back to HTML generator when UI build fails', async () => {
249+
it('falls back to HTML generator when custom uiDir has no dist folder', async () => {
257250
const tempDir = await createTempDir('autodocs-build-');
258251
const uiDir = path.join(tempDir, 'ui');
259252
await fs.mkdir(uiDir, { recursive: true });
260-
execMock.mockImplementation((...args: Parameters<typeof exec>) => {
261-
const cb = typeof args[1] === 'function' ? args[1] : args[2];
262-
if (cb) {
263-
cb(new Error('build failed'), '', '');
264-
}
265-
return createChildProcess();
266-
});
267253

268254
await buildReactUI(
269255
[

packages/cli/tests/build.test.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,6 @@ import path from 'path';
33
import type { DocEntry } from '@opensyntaxhq/autodocs-core';
44
import { createTempDir } from './helpers/temp';
55

6-
jest.mock('child_process', () => ({
7-
exec: jest.fn(
8-
(
9-
_command: string,
10-
_options: unknown,
11-
callback: (error: Error | null, stdout: string, stderr: string) => void
12-
) => {
13-
callback(null, '', '');
14-
}
15-
),
16-
}));
17-
186
import { buildReactUI } from '../src/commands/build';
197

208
import type { Ora } from 'ora';

0 commit comments

Comments
 (0)