GitNexus/gitnexus/scripts/build.js
Gergő Magyar a348bc3957
fix(build): build the web UI from prepack, not from every npm ci (#3166)
* fix(build): build the web UI from prepack, not from every npm ci

gitnexus-web is a separate ~650-package tree (React, Vite, LangChain,
Mermaid). Because `prepare` built it, every `npm ci` in gitnexus/ also
installed and Vite-built a second product. On CI that install ran
uncached inside an execSync timeout, so a healthy-but-slow install was
SIGTERM'd mid-flight and surfaced as `spawnSync /bin/sh ETIMEDOUT` --
repeatedly killing node floor compat, a job that only import-links the
CLI dist and never needs the UI.

The UI is only needed inside the published tarball, so build it from
prepack instead. `npm run build` and `prepare` are now CLI-only; pass
--web (or npm run build:web) to include it. Jobs that pack or publish
install gitnexus-web in their own visible step, and the in-script
fallback install is untimed so a slow install can no longer be killed
halfway and reported as a build failure. The tsc/vite timeout default
goes 300s -> 600s so the remaining bounded steps have headroom.

Default build on this machine: 30s, no gitnexus-web work.

* fix(build): enforce web package artifact integrity

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(build): clarify web packaging helpers without changing behavior

Keep the same opt-in, fail-closed, and pack/publish preserve rules while
trimming comments, sharing the test harness, and reading index.html
directly instead of probing it first.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: skip prepare on typecheck so a cold shared install cannot cancel the job

quality/typecheck's 10-minute budget was spent on an uncached gitnexus-shared
npm install plus a full prepare tsc that tsc --noEmit does not need.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: stop typecheck-web from canceling before the npm cache can save

Hashing gitnexus-shared into the web cache key forced a cold 650-package
install; the 10-minute job then canceled and never wrote a warm cache.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: give format the same 10-minute budget as lint

A cold root npm ci already took 4m19s and canceled prettier at the 5-minute
cap. Lint does the same install and needed 7m41s on that run.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: stop installing TypeScript 7 just to compile gitnexus-shared

A dedicated npm ci in gitnexus-shared took 7 minutes to add two packages
(TypeScript 7's optional per-platform binaries) and cancelled typecheck,
Windows pack, and coverage shard 1. Compile shared with gitnexus's tsc.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3166)

- Run tsc via execFileSync so the compiler path is never interpolated into a shell.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3166)

- Run tsc as node typescript/bin/tsc so Windows never has to execFile a .cmd shim.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Launch tsc via node and lib/tsc.js on every OS.

The npm .bin/tsc shim is tsc.cmd on Windows, which execFileSync cannot spawn.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ci): lock eval containment against a dedicated shared npm ci

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 13:28:11 +01:00

132 lines
5.3 KiB
JavaScript

#!/usr/bin/env node
/**
* Build script that compiles gitnexus and inlines gitnexus-shared into the dist.
*
* Steps:
* 1. Build gitnexus-shared (tsc)
* 2. Build gitnexus (tsc)
* 3. Copy gitnexus-shared/dist → dist/_shared
* 4. Rewrite bare 'gitnexus-shared' specifiers → relative paths
*/
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { runWebBuild } from './build-web.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const SHARED_ROOT = path.resolve(ROOT, '..', 'gitnexus-shared');
const DIST = path.join(ROOT, 'dist');
const SHARED_DEST = path.join(DIST, '_shared');
const DEFAULT_BUILD_TIMEOUT_MS = 600_000;
function getBuildTimeoutMs() {
const raw = process.env.GITNEXUS_BUILD_TIMEOUT_MS;
if (raw === undefined || raw.trim() === '') return DEFAULT_BUILD_TIMEOUT_MS;
const parsed = Number.parseInt(raw, 10);
if (Number.isFinite(parsed) && parsed > 0) return parsed;
console.warn(
`[build] ignoring invalid GITNEXUS_BUILD_TIMEOUT_MS=${JSON.stringify(raw)}; using ${DEFAULT_BUILD_TIMEOUT_MS}ms`,
);
return DEFAULT_BUILD_TIMEOUT_MS;
}
const BUILD_TIMEOUT_MS = getBuildTimeoutMs();
// Published-package guard: when installed from the npm registry the
// monorepo sibling `gitnexus-shared` does not exist and `dist/` is
// already pre-built. Skip the build to avoid a misleading ENOENT
// crash (#1795).
if (!fs.existsSync(SHARED_ROOT)) {
if (fs.existsSync(DIST)) {
console.log('[build] skipping — dist/ already present (published package).');
process.exit(0);
}
console.error(
`[build] gitnexus-shared not found at ${SHARED_ROOT} and no dist/ exists.\n` +
'Are you running from the monorepo checkout? Run `npm install` from the repo root first.',
);
process.exit(1);
}
// Launch tsc as `node typescript/lib/tsc.js` on every OS. The `.bin/tsc` /
// `tsc.cmd` shims are Windows-only wrappers; `execFileSync` cannot spawn a
// `.cmd` without a shell, and a separate `npm ci` in gitnexus-shared pulls
// TypeScript 7 optional platform packages (7+ minutes in CI).
const tscJs = path.join(ROOT, 'node_modules', 'typescript', 'lib', 'tsc.js');
if (!fs.existsSync(tscJs)) {
console.error(
`[build] missing ${tscJs}. Install gitnexus dependencies first (npm ci in gitnexus/).`,
);
process.exit(1);
}
function runTsc(cwd) {
execFileSync(process.execPath, [tscJs], { cwd, stdio: 'inherit', timeout: BUILD_TIMEOUT_MS });
}
// ── 1. Build gitnexus-shared ───────────────────────────────────────
console.log('[build] compiling gitnexus-shared…');
runTsc(SHARED_ROOT);
// ── 2. Build gitnexus ──────────────────────────────────────────────
console.log('[build] compiling gitnexus…');
runTsc(ROOT);
// ── 3. Copy shared dist ────────────────────────────────────────────
console.log('[build] copying shared module into dist/_shared…');
fs.cpSync(path.join(SHARED_ROOT, 'dist'), SHARED_DEST, { recursive: true });
// ── 4. Rewrite imports ─────────────────────────────────────────────
console.log('[build] rewriting gitnexus-shared imports…');
let rewritten = 0;
function rewriteFile(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
if (!content.includes('gitnexus-shared')) return;
const relDir = path.relative(path.dirname(filePath), SHARED_DEST);
// Always use posix separators and point to the package index
const relImport = relDir.split(path.sep).join('/') + '/index.js';
const updated = content
.replace(/from\s+['"]gitnexus-shared['"]/g, `from '${relImport}'`)
.replace(/import\(\s*['"]gitnexus-shared['"]\s*\)/g, `import('${relImport}')`);
if (updated !== content) {
fs.writeFileSync(filePath, updated);
rewritten++;
}
}
function walk(dir, extensions, cb) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(full, extensions, cb);
} else if (extensions.some((ext) => entry.name.endsWith(ext))) {
cb(full);
}
}
}
walk(DIST, ['.js', '.d.ts'], rewriteFile);
// ── 5. Make CLI entry executable ────────────────────────────────────
const cliEntry = path.join(DIST, 'cli', 'index.js');
if (process.platform !== 'win32' && fs.existsSync(cliEntry)) {
fs.chmodSync(cliEntry, 0o755);
}
// ── 6. Build & copy web UI (opt-in) ─────────────────────────────────
// Web UI is a separate package and is only required in the published
// tarball, so it is built by `prepack --web`, not by `prepare`. Serve
// falls back to the landing page when web/ is absent. CLI-only builds
// delete stale web/ except during npm pack/publish prepare, which must
// keep the prepack output.
runWebBuild({ root: ROOT, dist: DIST, timeoutMs: BUILD_TIMEOUT_MS });
console.log(`[build] done — rewrote ${rewritten} files.`);