Skip to content

Commit 8440f73

Browse files
ci(cli): gate publish with pack and tarball smoke checks
verify npm pack contains ui-dist assets required for React output add packed-tarball smoke test that runs autodocs build in temp fixture wire checks into preflight workflow and root npm scripts document local invocation and bundled-UI behavior in CLI README Signed-off-by: night-slayer18 <samanuaia257@gmail.com>
1 parent ad9ea44 commit 8440f73

6 files changed

Lines changed: 289 additions & 1 deletion

File tree

.github/workflows/preflight.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ jobs:
4040
npm -w packages/plugins/markdown pack
4141
npm -w packages/plugins/examples pack
4242
rm -f opensyntaxhq-*.tgz
43+
- name: Verify CLI package includes bundled UI
44+
run: npm run verify:cli:pack
45+
- name: Smoke test packed CLI artifact
46+
run: npm run test:smoke:cli-tarball
4347
- name: Publish dry-run (requires NPM_TOKEN)
4448
if: ${{ env.NPM_TOKEN != '' }}
4549
env:

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
"test:e2e": "playwright test",
2020
"test:e2e:ui": "playwright test --ui",
2121
"test:e2e:debug": "playwright test --debug",
22+
"verify:cli:pack": "node scripts/verify-cli-pack.js",
23+
"test:smoke:cli-tarball": "node scripts/smoke-cli-tarball.js",
2224
"serve:test": "node scripts/serve-test-docs.js",
2325
"type-check": "turbo run type-check",
24-
"docs:build": "npm run build && node packages/cli/dist/index.js build",
26+
"docs:build": "npm run build && npm -w packages/cli run prepare:ui-dist && node packages/cli/dist/index.js build",
2527
"version": "node scripts/version.js",
2628
"clean": "turbo run clean && rm -rf node_modules .turbo",
2729
"prepare": "husky"

packages/cli/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@ Engineer-first documentation generator for TypeScript.
88
npm install -D @opensyntaxhq/autodocs
99
```
1010

11+
Run via local binary:
12+
13+
```bash
14+
npx autodocs build
15+
# or
16+
npm exec autodocs build
17+
```
18+
19+
If you want `autodocs` available globally in your shell:
20+
21+
```bash
22+
npm install -g @opensyntaxhq/autodocs
23+
```
24+
1125
## Quick start
1226

1327
```bash
@@ -45,3 +59,5 @@ export default defineConfig({
4559
## Notes
4660

4761
Set `SITE_URL` (env or config) to generate `sitemap.xml` and `robots.txt`.
62+
63+
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.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import fs from 'fs/promises';
2+
import path from 'path';
3+
import { execFile } from 'child_process';
4+
import { promisify } from 'util';
5+
import { createTempDir } from './helpers/temp';
6+
7+
const execFileAsync = promisify(execFile);
8+
9+
async function pathExists(target: string): Promise<boolean> {
10+
try {
11+
await fs.access(target);
12+
return true;
13+
} catch {
14+
return false;
15+
}
16+
}
17+
18+
function parsePackJson(output: string): Array<{ files?: Array<{ path: string }> }> {
19+
const match = output.match(/\[\s*\{[\s\S]*\}\s*\]\s*$/);
20+
if (!match) {
21+
throw new Error('npm pack did not produce JSON output');
22+
}
23+
24+
const jsonPayload = match[0];
25+
return JSON.parse(jsonPayload) as Array<{ files?: Array<{ path: string }> }>;
26+
}
27+
28+
describe('CLI package artifact', () => {
29+
it('includes bundled ui-dist assets in npm pack output', async () => {
30+
const cliDir = path.resolve(__dirname, '..');
31+
const sourceUiDist = await createTempDir('autodocs-ui-dist-');
32+
const npmCacheRoot = await createTempDir('autodocs-npm-cache-');
33+
const packagedUiDist = path.join(cliDir, 'ui-dist');
34+
const packagedUiDistBackup = path.join(cliDir, 'ui-dist.__bak__');
35+
const hadPackagedUiDist = await pathExists(packagedUiDist);
36+
37+
await fs.mkdir(path.join(sourceUiDist, 'assets'), { recursive: true });
38+
await fs.writeFile(
39+
path.join(sourceUiDist, 'index.html'),
40+
'<html><body><div id="root"></div></body></html>',
41+
'utf-8'
42+
);
43+
await fs.writeFile(path.join(sourceUiDist, 'assets', 'app.js'), 'console.log("ui");', 'utf-8');
44+
45+
if (hadPackagedUiDist) {
46+
await fs.rm(packagedUiDistBackup, { recursive: true, force: true });
47+
await fs.rename(packagedUiDist, packagedUiDistBackup);
48+
}
49+
50+
try {
51+
const { stdout } = await execFileAsync('npm', ['pack', '--dry-run', '--json'], {
52+
cwd: cliDir,
53+
env: {
54+
...process.env,
55+
AUTODOCS_UI_DIST_SOURCE: sourceUiDist,
56+
npm_config_cache: path.join(npmCacheRoot, 'cache'),
57+
},
58+
});
59+
60+
const packed = parsePackJson(stdout);
61+
const packedPaths = new Set((packed[0]?.files || []).map((entry) => entry.path));
62+
63+
expect(packedPaths.has('ui-dist/index.html')).toBe(true);
64+
expect(Array.from(packedPaths).some((entry) => entry.startsWith('ui-dist/assets/'))).toBe(
65+
true
66+
);
67+
} finally {
68+
await fs.rm(packagedUiDist, { recursive: true, force: true });
69+
if (hadPackagedUiDist && (await pathExists(packagedUiDistBackup))) {
70+
await fs.rename(packagedUiDistBackup, packagedUiDist);
71+
} else {
72+
await fs.rm(packagedUiDistBackup, { recursive: true, force: true });
73+
}
74+
}
75+
});
76+
});

scripts/smoke-cli-tarball.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs/promises');
4+
const path = require('path');
5+
const os = require('os');
6+
const { execFile } = require('child_process');
7+
const { promisify } = require('util');
8+
9+
const execFileAsync = promisify(execFile);
10+
11+
async function runCommand(command, args, options) {
12+
return execFileAsync(command, args, options);
13+
}
14+
15+
function parsePackJson(output) {
16+
const match = output.match(/\[\s*\{[\s\S]*\}\s*\]\s*$/);
17+
if (!match) {
18+
throw new Error('npm pack did not produce JSON output');
19+
}
20+
21+
const jsonPayload = match[0];
22+
return JSON.parse(jsonPayload);
23+
}
24+
25+
async function run() {
26+
const repoRoot = path.resolve(__dirname, '..');
27+
const cliDir = path.join(repoRoot, 'packages', 'cli');
28+
const uiDistDir = process.env.AUTODOCS_UI_DIST_SOURCE
29+
? path.resolve(process.env.AUTODOCS_UI_DIST_SOURCE)
30+
: path.join(repoRoot, 'packages', 'ui', 'dist');
31+
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'autodocs-cli-smoke-'));
32+
const extractDir = path.join(tmpRoot, 'extract');
33+
const fixtureDir = path.join(tmpRoot, 'fixture');
34+
const npmCache = path.join(tmpRoot, 'npm-cache');
35+
36+
let tarballPath;
37+
38+
try {
39+
const { stdout: packStdout } = await runCommand('npm', ['pack', '--json'], {
40+
cwd: cliDir,
41+
env: {
42+
...process.env,
43+
AUTODOCS_UI_DIST_SOURCE: uiDistDir,
44+
npm_config_cache: npmCache,
45+
},
46+
});
47+
48+
const packed = parsePackJson(packStdout);
49+
const tarballFile = packed[0]?.filename;
50+
if (!tarballFile) {
51+
throw new Error('npm pack did not return a tarball filename');
52+
}
53+
54+
tarballPath = path.join(cliDir, tarballFile);
55+
56+
await fs.mkdir(extractDir, { recursive: true });
57+
await runCommand('tar', ['-xzf', tarballPath, '-C', extractDir], { cwd: repoRoot });
58+
59+
const extractedPackageDir = path.join(extractDir, 'package');
60+
const extractedNodeModulesDir = path.join(extractedPackageDir, 'node_modules');
61+
const extractedCoreScopeDir = path.join(extractedNodeModulesDir, '@opensyntaxhq');
62+
const extractedCoreLink = path.join(extractedCoreScopeDir, 'autodocs-core');
63+
const localCorePackageDir = path.join(repoRoot, 'packages', 'core');
64+
65+
await fs.mkdir(extractedCoreScopeDir, { recursive: true });
66+
await fs.rm(extractedCoreLink, { recursive: true, force: true });
67+
await fs.symlink(localCorePackageDir, extractedCoreLink, 'dir');
68+
69+
const cliEntry = path.join(extractedPackageDir, 'dist', 'index.js');
70+
71+
await fs.mkdir(path.join(fixtureDir, 'src'), { recursive: true });
72+
await fs.writeFile(path.join(fixtureDir, 'src', 'index.ts'), 'export const value = 1;\n', 'utf-8');
73+
await fs.writeFile(
74+
path.join(fixtureDir, 'autodocs.config.json'),
75+
JSON.stringify(
76+
{
77+
include: ['src/**/*.ts'],
78+
output: { dir: './docs-dist', format: 'static', clean: true },
79+
},
80+
null,
81+
2
82+
),
83+
'utf-8'
84+
);
85+
86+
const nodePathParts = [path.join(repoRoot, 'node_modules')];
87+
if (process.env.NODE_PATH) {
88+
nodePathParts.push(process.env.NODE_PATH);
89+
}
90+
91+
await runCommand(
92+
process.execPath,
93+
[cliEntry, 'build', '--config', path.join(fixtureDir, 'autodocs.config.json')],
94+
{
95+
cwd: fixtureDir,
96+
env: {
97+
...process.env,
98+
NODE_PATH: nodePathParts.join(path.delimiter),
99+
npm_config_cache: npmCache,
100+
},
101+
}
102+
);
103+
104+
const outputDir = path.join(fixtureDir, 'docs-dist');
105+
const indexHtmlPath = path.join(outputDir, 'index.html');
106+
const docsJsonPath = path.join(outputDir, 'docs.json');
107+
const configJsonPath = path.join(outputDir, 'config.json');
108+
109+
await fs.access(indexHtmlPath);
110+
await fs.access(docsJsonPath);
111+
await fs.access(configJsonPath);
112+
113+
const indexHtml = await fs.readFile(indexHtmlPath, 'utf-8');
114+
if (!indexHtml.includes('<div id="root"></div>')) {
115+
throw new Error('Generated index.html does not appear to be React UI output');
116+
}
117+
118+
console.log('[smoke-cli-tarball] Packed CLI generated React UI output successfully');
119+
} finally {
120+
if (tarballPath) {
121+
await fs.rm(tarballPath, { force: true }).catch(() => undefined);
122+
}
123+
await fs.rm(tmpRoot, { recursive: true, force: true }).catch(() => undefined);
124+
}
125+
}
126+
127+
run().catch((error) => {
128+
const message = error instanceof Error ? error.message : String(error);
129+
console.error(`[smoke-cli-tarball] ${message}`);
130+
process.exit(1);
131+
});

scripts/verify-cli-pack.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env node
2+
3+
const { execFile } = require('child_process');
4+
const path = require('path');
5+
const { promisify } = require('util');
6+
7+
const execFileAsync = promisify(execFile);
8+
9+
function parsePackJson(output) {
10+
const match = output.match(/\[\s*\{[\s\S]*\}\s*\]\s*$/);
11+
if (!match) {
12+
throw new Error('npm pack did not produce JSON output');
13+
}
14+
15+
const jsonPayload = match[0];
16+
return JSON.parse(jsonPayload);
17+
}
18+
19+
async function run() {
20+
const repoRoot = path.resolve(__dirname, '..');
21+
const cliDir = path.join(repoRoot, 'packages', 'cli');
22+
const uiDistDir = path.join(repoRoot, 'packages', 'ui', 'dist');
23+
24+
const { stdout } = await execFileAsync('npm', ['pack', '--dry-run', '--json'], {
25+
cwd: cliDir,
26+
env: {
27+
...process.env,
28+
AUTODOCS_UI_DIST_SOURCE: uiDistDir,
29+
npm_config_cache: '/tmp/npm-cache',
30+
},
31+
});
32+
33+
let packed;
34+
try {
35+
packed = parsePackJson(stdout);
36+
} catch (error) {
37+
const message = error instanceof Error ? error.message : String(error);
38+
throw new Error(`Failed to parse npm pack output: ${message}`);
39+
}
40+
41+
const files = new Set((packed[0]?.files || []).map((entry) => entry.path));
42+
43+
if (!files.has('ui-dist/index.html')) {
44+
throw new Error('CLI pack output is missing ui-dist/index.html');
45+
}
46+
47+
const hasUiAssets = Array.from(files).some((entry) => entry.startsWith('ui-dist/assets/'));
48+
if (!hasUiAssets) {
49+
throw new Error('CLI pack output is missing ui-dist/assets/*');
50+
}
51+
52+
console.log('[verify-cli-pack] CLI package contains bundled UI assets');
53+
}
54+
55+
run().catch((error) => {
56+
const message = error instanceof Error ? error.message : String(error);
57+
console.error(`[verify-cli-pack] ${message}`);
58+
process.exit(1);
59+
});

0 commit comments

Comments
 (0)