Skip to content

Commit d25fc34

Browse files
committed
feat: replicate openilink-style docs landing and safe docs build
1 parent 5f41459 commit d25fc34

5 files changed

Lines changed: 706 additions & 101 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"dev": "node cli.js run",
2828
"start": "node cli.js",
2929
"docs:dev": "node ./node_modules/vitepress/dist/node/cli.js dev site",
30-
"docs:build": "node ./node_modules/vitepress/dist/node/cli.js build site",
30+
"docs:build": "node scripts/docs-build-safe.js",
31+
"docs:build:raw": "node ./node_modules/vitepress/dist/node/cli.js build site",
3132
"docs:preview": "node ./node_modules/vitepress/dist/node/cli.js preview site",
3233
"test": "npm run test:unit && npm run test:e2e",
3334
"test:unit": "node tests/unit/run.mjs",

scripts/docs-build-safe.js

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#!/usr/bin/env node
2+
const fs = require('fs');
3+
const os = require('os');
4+
const path = require('path');
5+
const { spawnSync } = require('child_process');
6+
7+
const root = path.resolve(__dirname, '..');
8+
const tempRoot = path.join(os.homedir(), '.cache', 'codexmate-docs-build');
9+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
10+
11+
function removePath(targetPath) {
12+
if (!fs.existsSync(targetPath)) return;
13+
14+
if (typeof fs.rmSync === 'function') {
15+
fs.rmSync(targetPath, { recursive: true, force: true });
16+
return;
17+
}
18+
19+
const stat = fs.lstatSync(targetPath);
20+
if (stat.isDirectory()) {
21+
for (const entry of fs.readdirSync(targetPath)) {
22+
removePath(path.join(targetPath, entry));
23+
}
24+
fs.rmdirSync(targetPath);
25+
return;
26+
}
27+
fs.unlinkSync(targetPath);
28+
}
29+
30+
function copyRecursive(sourcePath, targetPath) {
31+
const stat = fs.statSync(sourcePath);
32+
if (stat.isDirectory()) {
33+
fs.mkdirSync(targetPath, { recursive: true });
34+
for (const entry of fs.readdirSync(sourcePath)) {
35+
copyRecursive(path.join(sourcePath, entry), path.join(targetPath, entry));
36+
}
37+
return;
38+
}
39+
40+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
41+
fs.copyFileSync(sourcePath, targetPath);
42+
}
43+
44+
function ensureEsbuildExecutable(targetRoot) {
45+
const esbuildPkgRoot = path.join(targetRoot, 'node_modules', '@esbuild');
46+
if (!fs.existsSync(esbuildPkgRoot)) return;
47+
48+
for (const packageName of fs.readdirSync(esbuildPkgRoot)) {
49+
const binaryPath = path.join(esbuildPkgRoot, packageName, 'bin', 'esbuild');
50+
if (!fs.existsSync(binaryPath)) continue;
51+
try {
52+
fs.chmodSync(binaryPath, 0o755);
53+
} catch (_) {
54+
// Best-effort permission fix for copied noexec binaries.
55+
}
56+
}
57+
}
58+
59+
function flushOutput(result) {
60+
if (result.stdout) process.stdout.write(result.stdout);
61+
if (result.stderr) process.stderr.write(result.stderr);
62+
}
63+
64+
function runCommand(cmd, args, cwd, options = {}) {
65+
const { echo = true } = options;
66+
const result = spawnSync(cmd, args, {
67+
cwd,
68+
env: process.env,
69+
encoding: 'utf8',
70+
});
71+
72+
if (echo) {
73+
flushOutput(result);
74+
}
75+
if (result.error) {
76+
console.error(result.error.message || result.error);
77+
}
78+
return result;
79+
}
80+
81+
function runDocsBuild(cwd, options = {}) {
82+
const vitepressCli = path.join(cwd, 'node_modules', 'vitepress', 'dist', 'node', 'cli.js');
83+
return runCommand(process.execPath, [vitepressCli, 'build', 'site'], cwd, options);
84+
}
85+
86+
function ensureVitepress(cwd) {
87+
const vitepressCli = path.join(cwd, 'node_modules', 'vitepress', 'dist', 'node', 'cli.js');
88+
if (fs.existsSync(vitepressCli)) return true;
89+
90+
const installResult = runCommand(npmCmd, ['install', '--include=dev'], cwd);
91+
if (installResult.status !== 0) {
92+
process.exit(installResult.status == null ? 1 : installResult.status);
93+
}
94+
return fs.existsSync(vitepressCli);
95+
}
96+
97+
function isNoexecFailure(result) {
98+
const payload = `${result.stdout || ''}\n${result.stderr || ''}\n${result.error || ''}`;
99+
return /EACCES|ERR_DLOPEN_FAILED|not accessible for the namespace|@rollup\/rollup-|node_modules\/esbuild/i.test(
100+
payload,
101+
);
102+
}
103+
104+
function copyInputWorkspace(targetRoot) {
105+
removePath(targetRoot);
106+
fs.mkdirSync(targetRoot, { recursive: true });
107+
108+
for (const fileName of ['package.json', 'package-lock.json', '.npmrc']) {
109+
const source = path.join(root, fileName);
110+
if (!fs.existsSync(source)) continue;
111+
fs.copyFileSync(source, path.join(targetRoot, fileName));
112+
}
113+
114+
copyRecursive(path.join(root, 'site'), path.join(targetRoot, 'site'));
115+
116+
const rootNodeModules = path.join(root, 'node_modules');
117+
if (fs.existsSync(rootNodeModules)) {
118+
copyRecursive(rootNodeModules, path.join(targetRoot, 'node_modules'));
119+
ensureEsbuildExecutable(targetRoot);
120+
}
121+
}
122+
123+
function copyDistBack(sourceRoot) {
124+
const sourceDist = path.join(sourceRoot, 'site', '.vitepress', 'dist');
125+
if (!fs.existsSync(sourceDist)) {
126+
return;
127+
}
128+
129+
const targetDist = path.join(root, 'site', '.vitepress', 'dist');
130+
removePath(targetDist);
131+
fs.mkdirSync(path.dirname(targetDist), { recursive: true });
132+
copyRecursive(sourceDist, targetDist);
133+
}
134+
135+
if (!ensureVitepress(root)) {
136+
console.error('[codexmate] vitepress is unavailable after dependency installation.');
137+
process.exit(1);
138+
}
139+
140+
const directBuild = runDocsBuild(root, { echo: false });
141+
if (directBuild.status === 0) {
142+
flushOutput(directBuild);
143+
process.exit(0);
144+
}
145+
146+
if (!isNoexecFailure(directBuild)) {
147+
flushOutput(directBuild);
148+
process.exit(directBuild.status == null ? 1 : directBuild.status);
149+
}
150+
151+
console.log('[codexmate] Detected noexec native-module failure, retrying in temp workspace...');
152+
153+
copyInputWorkspace(tempRoot);
154+
155+
if (!ensureVitepress(tempRoot)) {
156+
console.error('[codexmate] vitepress is unavailable in temp workspace.');
157+
process.exit(1);
158+
}
159+
160+
const tempBuild = runDocsBuild(tempRoot);
161+
if (tempBuild.status !== 0) {
162+
process.exit(tempBuild.status == null ? 1 : tempBuild.status);
163+
}
164+
165+
copyDistBack(tempRoot);
166+
console.log(`[codexmate] Docs build succeeded via temp workspace: ${tempRoot}`);

0 commit comments

Comments
 (0)