Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/preflight.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ jobs:
npm -w packages/plugins/markdown pack
npm -w packages/plugins/examples pack
rm -f opensyntaxhq-*.tgz
- name: Verify CLI package includes bundled UI
run: npm run verify:cli:pack
- name: Smoke test packed CLI artifact
run: npm run test:smoke:cli-tarball
- name: Publish dry-run (requires NPM_TOKEN)
if: ${{ env.NPM_TOKEN != '' }}
env:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ Thumbs.db
# Autodocs
docs-dist
.autodocs-cache
packages/cli/ui-dist
14 changes: 14 additions & 0 deletions e2e/guides.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { test, expect } from './coverage';

test('guide navigation uses stable short IDs', async ({ page }) => {
await page.goto('/');

const openNav = page.getByRole('button', { name: /open navigation/i });
if (await openNav.isVisible()) {
await openNav.click();
}

await page.getByRole('link', { name: 'README' }).first().click();

await expect(page).toHaveURL(/\/guide\/[0-9a-f]{8}\/readme$/i);
});
3 changes: 2 additions & 1 deletion e2e/performance.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { test, expect } from './coverage';

test('homepage loads within acceptable time', async ({ page }) => {
const maxDurationMs = Number(process.env.E2E_HOME_LOAD_BUDGET_MS || '5000');
const start = Date.now();
await page.goto('/', { waitUntil: 'load' });
const duration = Date.now() - start;

expect(duration).toBeLessThan(15000);
expect(duration).toBeLessThan(maxDurationMs);
});
16 changes: 15 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import globals from 'globals';

const disableTypeCheckedForJs = tseslint.configs.disableTypeChecked;

export default tseslint.config(
{
ignores: [
'**/dist/**',
'**/ui-dist/**',
'**/node_modules/**',
'**/.turbo/**',
'**/coverage/**',
Expand Down Expand Up @@ -43,7 +46,18 @@ export default tseslint.config(
},
{
files: ['**/*.{js,mjs,cjs}'],
...tseslint.configs.disableTypeChecked,
...disableTypeCheckedForJs,
languageOptions: {
...(disableTypeCheckedForJs.languageOptions ?? {}),
globals: {
...globals.node,
...globals.es2021,
},
},
rules: {
...(disableTypeCheckedForJs.rules ?? {}),
'@typescript-eslint/no-require-imports': 'off',
},
},
{
files: ['**/tests/**/*.ts', '**/*.test.ts', '**/*.spec.ts'],
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "autodocs-monorepo",
"version": "2.0.0",
"version": "2.1.0",
"private": true,
"license": "Apache-2.0",
"packageManager": "npm@11.9.0",
Expand All @@ -19,9 +19,11 @@
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug",
"verify:cli:pack": "node scripts/verify-cli-pack.js",
"test:smoke:cli-tarball": "node scripts/smoke-cli-tarball.js",
"serve:test": "node scripts/serve-test-docs.js",
"type-check": "turbo run type-check",
"docs:build": "npm run build && node packages/cli/dist/index.js build",
"docs:build": "npm run build && npm -w packages/cli run prepare:ui-dist && node packages/cli/dist/index.js build",
"version": "node scripts/version.js",
"clean": "turbo run clean && rm -rf node_modules .turbo",
"prepare": "husky"
Expand Down
16 changes: 16 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ Engineer-first documentation generator for TypeScript.
npm install -D @opensyntaxhq/autodocs
```

Run via local binary:

```bash
npx autodocs build
# or
npm exec autodocs build
```

If you want `autodocs` available globally in your shell:

```bash
npm install -g @opensyntaxhq/autodocs
```

## Quick start

```bash
Expand Down Expand Up @@ -45,3 +59,5 @@ export default defineConfig({
## Notes

Set `SITE_URL` (env or config) to generate `sitemap.xml` and `robots.txt`.

From `2.0.1+`, the React UI assets are bundled with the CLI package, so `autodocs build` static output does not require installing a separate UI package.
1 change: 0 additions & 1 deletion packages/cli/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* global module, process */
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
Expand Down
13 changes: 8 additions & 5 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
{
"name": "@opensyntaxhq/autodocs",
"version": "2.0.0",
"version": "2.1.0",
"description": "CLI for Autodocs documentation generator",
"bin": {
"autodocs": "dist/index.js"
},
"files": [
"dist"
"dist",
"ui-dist"
],
"scripts": {
"build": "tsup src/index.ts src/index-exports.ts --format cjs --dts --no-splitting",
"prepare:ui-dist": "node scripts/prepare-ui-dist.js",
"prepack": "npm run prepare:ui-dist",
"dev": "tsup src/index.ts --format cjs --watch",
"lint": "eslint .",
"test": "jest",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist"
"clean": "rm -rf dist ui-dist"
},
"exports": {
".": {
Expand All @@ -27,7 +30,7 @@
}
},
"dependencies": {
"@opensyntaxhq/autodocs-core": "^2.0.0",
"@opensyntaxhq/autodocs-core": "^2.1.0",
"chalk": "^5.6.2",
"chokidar": "^5.0.0",
"commander": "^14.0.3",
Expand Down Expand Up @@ -73,7 +76,7 @@
"publishConfig": {
"access": "public"
},
"author": "",
"author": "OpenSyntaxHQ",
"license": "Apache-2.0",
"type": "commonjs"
}
40 changes: 40 additions & 0 deletions packages/cli/scripts/prepare-ui-dist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env node

const fs = require('fs/promises');
const path = require('path');

async function pathExists(target) {
try {
await fs.access(target);
return true;
} catch {
return false;
}
}

async function main() {
const cliDir = path.resolve(__dirname, '..');
const defaultUiDistDir = path.resolve(cliDir, '../ui/dist');
const sourceUiDistDir = process.env.AUTODOCS_UI_DIST_SOURCE
? path.resolve(process.env.AUTODOCS_UI_DIST_SOURCE)
: defaultUiDistDir;
const targetUiDistDir = path.resolve(cliDir, 'ui-dist');

if (!(await pathExists(sourceUiDistDir))) {
console.error(
`[prepare-ui-dist] Missing UI build at ${sourceUiDistDir}. Run "npm -w packages/ui run build" first.`
);
process.exit(1);
}

await fs.rm(targetUiDistDir, { recursive: true, force: true });
await fs.cp(sourceUiDistDir, targetUiDistDir, { recursive: true });

console.log(`[prepare-ui-dist] Copied ${sourceUiDistDir} -> ${targetUiDistDir}`);
}

main().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
console.error(`[prepare-ui-dist] Failed: ${message}`);
process.exit(1);
});
96 changes: 54 additions & 42 deletions packages/cli/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import chalk from 'chalk';
import ora from 'ora';
import { Command } from 'commander';
import { glob } from 'glob';
import { exec } from 'child_process';
import { promisify } from 'util';
import fsPromises from 'fs/promises';
import type { CompilerOptions, Diagnostic } from 'typescript';
import { loadConfig, resolveConfigPaths } from '../config';
import { computeConfigHash } from '../utils/configHash';
import {
Expand All @@ -21,7 +20,23 @@ import {
VERSION,
} from '@opensyntaxhq/autodocs-core';

const execAsync = promisify(exec);
function toCompilerOptions(options?: Record<string, unknown>): CompilerOptions | undefined {
return options ? (options as unknown as CompilerOptions) : undefined;
}

interface UiSourceCandidate {
label: string;
distDir: string;
}

async function pathExists(target: string): Promise<boolean> {
try {
await fsPromises.access(target);
return true;
} catch {
return false;
}
}

interface BuildOptions {
config?: string;
Expand Down Expand Up @@ -253,50 +268,49 @@ export async function buildReactUI(
siteName?: string;
}
): Promise<void> {
// Find the UI package using require.resolve - works in monorepo
let uiDir: string;
let uiDistDir: string;
const candidates: UiSourceCandidate[] = [];
const candidateDistDirs = new Set<string>();
const addCandidate = (candidate: UiSourceCandidate): void => {
if (candidateDistDirs.has(candidate.distDir)) {
return;
}
candidateDistDirs.add(candidate.distDir);
candidates.push(candidate);
};

if (options.uiDir) {
uiDir = options.uiDir;
uiDistDir = path.join(uiDir, 'dist');
addCandidate({
label: `custom uiDir (${options.uiDir})`,
distDir: path.join(options.uiDir, 'dist'),
});
} else {
try {
// Try to resolve the UI package from the monorepo
const uiPackageJson = require.resolve('@opensyntaxhq/autodocs-ui/package.json');
uiDir = path.dirname(uiPackageJson);
uiDistDir = path.join(uiDir, 'dist');
} catch {
// Fallback: resolve relative to CLI package in monorepo
uiDir = path.resolve(__dirname, '../../ui');
uiDistDir = path.join(uiDir, 'dist');
}
// Bundled UI assets are the primary source for published CLI packages.
addCandidate({
label: 'bundled CLI assets',
distDir: path.resolve(__dirname, '../ui-dist'),
});
addCandidate({
label: 'bundled CLI assets',
distDir: path.resolve(__dirname, '../../ui-dist'),
});
}

// Check if UI package exists
try {
await fsPromises.access(uiDir);
} catch {
// UI package not found, fall back to basic HTML
spinner.text = 'React UI not found, using basic HTML generator...';
const { generateHtml } = await import('@opensyntaxhq/autodocs-core');
await generateHtml(docs, outputDir);
return;
let selectedSource: UiSourceCandidate | null = null;
for (const candidate of candidates) {
if (await pathExists(candidate.distDir)) {
selectedSource = candidate;
break;
}
}

// Step 1: Build the React UI
spinner.text = 'Building React UI...';

try {
await execAsync('npm run build', { cwd: uiDir });
} catch {
spinner.fail(chalk.red('Failed to build React UI, falling back to basic HTML'));
if (!selectedSource) {
spinner.text = 'React UI not found, using basic HTML generator...';
const { generateHtml } = await import('@opensyntaxhq/autodocs-core');
await generateHtml(docs, outputDir);
return;
}

spinner.succeed(chalk.green('React UI built'));
spinner.succeed(chalk.green(`React UI ready (${selectedSource.label})`));

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

// Step 3: Copy React UI assets
spinner.text = 'Copying UI assets...';
spinner.text = `Copying UI assets (${selectedSource.label})...`;

await copyDirectory(uiDistDir, outputDir);
await copyDirectory(selectedSource.distDir, outputDir);

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

Expand Down Expand Up @@ -403,7 +417,7 @@ export function registerBuild(program: Command): void {

let docs: DocEntry[] = [];
let rootDir = process.cwd();
let diagnostics: Array<import('typescript').Diagnostic> = [];
let diagnostics: Diagnostic[] = [];

spinner.start('Parsing TypeScript...');

Expand All @@ -417,8 +431,7 @@ export function registerBuild(program: Command): void {
files,
cache,
tsconfig: config.tsconfig,
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
compilerOptions: config.compilerOptions as any,
compilerOptions: toCompilerOptions(config.compilerOptions),
configHash,
onProgram: async (program, parsedSourceFiles) => {
await manager.runHook('afterParse', program);
Expand All @@ -438,8 +451,7 @@ export function registerBuild(program: Command): void {
} else {
const parseResult = createProgram(files, {
configFile: config.tsconfig,
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
compilerOptions: config.compilerOptions as any,
compilerOptions: toCompilerOptions(config.compilerOptions),
skipLibCheck: true,
});

Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ export function registerServe(program: Command): void {
console.log(chalk.gray('\nPress Ctrl+C to stop'));

if (open) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
open.default(url);
void Promise.resolve(open.default(url)).catch((openError: unknown) => {
console.warn(chalk.yellow('Could not open browser:'), openError);
});
}
});
} catch (error) {
Expand Down
Loading