mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* fix(grammars): load vendored tree-sitter grammars from vendor/ by absolute path (#2111) The recurring Windows `EPERM: operation not permitted, symlink` (errno -4048) when adding the MCP server to Antigravity is NOT the #2101/#2110 module-load crash — it is an install-time arborist failure during the `_npx` reify that the MCP client triggers on every `npx gitnexus` launch. Root cause: the `postinstall` materialize step copied each vendored grammar (`vendor/tree-sitter-{c,dart,proto,swift,kotlin}`) into `node_modules/gitnexus/node_modules/tree-sitter-*` as a real package so runtime `require('tree-sitter-dart')` would resolve. Those packages are in no dependency graph, so every subsequent npm/npx reify treats them as **extraneous** and prunes/relocates them — on Windows the relocation goes through `@npmcli/move-file`'s symlink path and throws EPERM (symlinks need Developer Mode/admin), and on every OS the 2nd run silently deletes the grammars. This is the same class as #1728, which the materialize step itself claimed to have fixed. Fix (the prebuildify + node-gyp-build ecosystem pattern): never copy grammars into node_modules. Load each by absolute path from `vendor/<name>` via the new `requireVendoredGrammar` helper — the grammar's own `bindings/node` runs `node-gyp-build(<dir>)` and loads the committed `vendor/<name>/prebuilds/ <platform>-<arch>/…` directly (all 5 ship all 6 tuples). vendor/ is inside the package but not a node_modules subtree, so arborist never sees the grammars and the reify is idempotent — no EPERM, no silent deletion. - new src/core/tree-sitter/vendored-grammars.ts (requireVendoredGrammar / vendoredGrammarDir / VENDORED_GRAMMAR_PACKAGES; VENDOR_ROOT stable in dev+dist) - route all consumers through it: parser-loader, parse-worker, grpc proto, include-extractor (C), http-patterns kotlin, cli optional-grammars probe - postinstall drops the materialize step; build-tree-sitter-grammars.cjs builds in-place under vendor/ (gitignored) and deletes materialize-vendor-grammars.cjs - tests + grammar-introspection helper load grammars from vendor/ too (single source of truth); new vendored-grammars.test.ts guards against reintroducing a bare `require('tree-sitter-<vendored>')` Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(grammars): throw on a non-vendored name in requireVendoredGrammar Drift guard (PR #2144 review, P3): validate the argument against VENDORED_GRAMMAR_PACKAGES and fail loudly on an unknown name, so the three grammar lists (package set / CLI probe / build registry) drifting out of sync surfaces as a clear error instead of a confusing absolute-path require miss. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(grammars): prepack guard against stray vendor/<g>/build/ shadowing prebuilds Publish hygiene (PR #2144 review, P2). Now that build-tree-sitter-grammars.cjs source-builds into vendor/<name>/build/, a stray build dir would ship in the tarball (files:["vendor"] overrides .gitignore/.npmignore) AND shadow the committed prebuild — node-gyp-build resolves build/Release before prebuilds/. assert-publish-grammar-coverage.cjs (prepack) now fails `npm pack` if any vendor/*/build exists (findStrayBuildArtifacts), with a clear `rm -rf` fix hint. Adds unit coverage for the new pure function. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(grammars): harden the #2111 no-bare-require regression guard PR #2144 review (P2). The guard regex missed dynamic import(), side-effect `import 'x'`, /subpath, and backtick loads, and only scanned src/. It now covers every node_modules-forcing form (single/double/backtick quotes, optional subpath), scans test/ too (excluding fixtures and the guard file itself), drops the `//`-substring false-negative (leading-comment-only heuristic), and adds a self-test asserting every load form is caught while prose mentions and tree-sitter-cpp are ignored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(grammars): correct stale vendored-grammar comments PR #2144 review (P3). kotlin/query.ts called tree-sitter-kotlin an "optionalDependency" — it is vendored and loaded from vendor/ by absolute path (#2111). proto.ts now states its remaining `_require` is only for the real `tree-sitter` dependency, not a vendored grammar (which goes through requireVendoredGrammar). Comment-only; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
204 lines
7.5 KiB
JavaScript
204 lines
7.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Publish guard: every vendored tree-sitter grammar must ship a loadable binding.
|
|
*
|
|
* The npm tarball includes gitnexus/vendor/ (package.json `files`). A grammar is
|
|
* "covered" on a platform-arch tuple if EITHER a prebuild ships for it OR the
|
|
* grammar's full source-build set ships (so the install can source-build it,
|
|
* toolchain permitting). A future lean publish — dropping the ~50 MB of generated
|
|
* source to ship prebuilds only — is safe ONLY once every grammar has all six
|
|
* prebuilds; doing it while any grammar still lacks a prebuild would ship a
|
|
* grammar with NO loadable binding (neither prebuild nor buildable source) → that
|
|
* language is silently dead for users.
|
|
*
|
|
* HOW SOURCE INCLUSION IS DECIDED. The `files` allow-list OVERRIDES `.npmignore`
|
|
* for the vendored subtree (verified: an active "vendor/(star-star)/src/parser.c"
|
|
* in .npmignore does NOT drop it from `npm pack`). So `.npmignore` can never
|
|
* exclude vendored source — the ONLY lever is the `files` field. A broad `vendor`
|
|
* ships the whole subtree (source + prebuilds); a lean publish narrows `files` to
|
|
* non-source subpaths. This guard therefore reads `files` directly rather than
|
|
* shelling out to `npm pack` (which, in prepack, would re-enter this guard and,
|
|
* on npm versions that don't honor --ignore-scripts for prepare/prepack, run the
|
|
* full build — slow enough to time out and fragile).
|
|
*
|
|
* Wired via `prepack`, so it fails `npm pack` / `npm publish` if the invariant is
|
|
* violated.
|
|
*/
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const TUPLES = [
|
|
'linux-x64',
|
|
'linux-arm64',
|
|
'darwin-x64',
|
|
'darwin-arm64',
|
|
'win32-x64',
|
|
'win32-arm64',
|
|
];
|
|
|
|
// Source-build inputs (relative to vendor/<name>/) whose presence makes a grammar
|
|
// source-buildable. Per-grammar we only require the ones that exist on disk (e.g.
|
|
// tree-sitter-c has no external scanner.c).
|
|
const SOURCE_BUILD_REL = [
|
|
'binding.gyp',
|
|
'bindings/node/binding.cc',
|
|
'src/parser.c',
|
|
'src/scanner.c',
|
|
'src/tree_sitter/parser.h',
|
|
];
|
|
|
|
/**
|
|
* Does the package.json `files` allow-list ship the WHOLE vendor subtree (and
|
|
* therefore the vendored grammar source)? A bare `vendor` (optionally with a
|
|
* trailing slash or `/**`/`/*`) includes everything under vendor/. A lean publish
|
|
* replaces that with non-source subpaths, so this returns false and grammars must
|
|
* then rely on prebuilds.
|
|
*/
|
|
function filesShipsVendorSource(filesField) {
|
|
return (filesField || []).some((f) => {
|
|
const n = String(f)
|
|
.replace(/\\/g, '/')
|
|
.replace(/\/+$/, '')
|
|
.replace(/\/\*\*?$/, '');
|
|
return n === 'vendor';
|
|
});
|
|
}
|
|
|
|
/** The on-disk source-build inputs for a grammar (relative paths). */
|
|
function sourceBuildSet(grammarDir) {
|
|
return SOURCE_BUILD_REL.filter((rel) => fs.existsSync(path.join(grammarDir, rel)));
|
|
}
|
|
|
|
/** True when a grammar can be source-built from its vendored files (has gyp + parser). */
|
|
function isBuildableFromSource(grammarDir) {
|
|
const set = sourceBuildSet(grammarDir);
|
|
return set.includes('binding.gyp') && set.includes('src/parser.c');
|
|
}
|
|
|
|
/** Count platform-arch tuples with a committed prebuilt .node on disk. */
|
|
function countPrebuiltTuples(grammarDir) {
|
|
const pdir = path.join(grammarDir, 'prebuilds');
|
|
let n = 0;
|
|
for (const t of TUPLES) {
|
|
const td = path.join(pdir, t);
|
|
try {
|
|
if (fs.statSync(td).isDirectory() && fs.readdirSync(td).some((f) => f.endsWith('.node'))) {
|
|
n++;
|
|
}
|
|
} catch {
|
|
/* tuple dir absent — not covered */
|
|
}
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/**
|
|
* Pure core (exported for tests). `grammars` is a list of
|
|
* `{ name, prebuilt: 0..6, shipsSource: boolean }`. Returns human-readable
|
|
* problem strings; an empty array means the pack is publish-safe.
|
|
*/
|
|
function findCoverageProblems({ grammars }) {
|
|
const problems = [];
|
|
for (const g of grammars) {
|
|
if (g.prebuilt < 6 && !g.shipsSource) {
|
|
const missing = 6 - g.prebuilt;
|
|
problems.push(
|
|
`${g.name}: ${g.prebuilt}/6 prebuilds and its vendored source is not shipped ` +
|
|
`(the package.json \`files\` field excludes it, or it is not buildable) — would ship ` +
|
|
`with no loadable binding on ${missing} platform-arch tuple(s).`,
|
|
);
|
|
}
|
|
}
|
|
return problems;
|
|
}
|
|
|
|
/**
|
|
* Stray local source-build outputs under `vendor/<name>/build/`. These would
|
|
* ship in the tarball (`files: ["vendor"]` overrides .gitignore/.npmignore) AND
|
|
* shadow the committed prebuilds — `node-gyp-build` resolves `build/Release`
|
|
* BEFORE `prebuilds/`, so a consumer on the publisher's platform would load the
|
|
* stray (possibly stale/wrong) binding instead of the curated prebuild. The
|
|
* build dir is gitignored and only appears if a maintainer source-built locally
|
|
* (e.g. on a no-prebuild platform); refuse to publish it. (#2144 review.)
|
|
*/
|
|
function findStrayBuildArtifacts(vendorDir) {
|
|
if (!fs.existsSync(vendorDir)) return [];
|
|
return fs
|
|
.readdirSync(vendorDir)
|
|
.filter((d) => /^tree-sitter-/.test(d))
|
|
.filter((d) => fs.existsSync(path.join(vendorDir, d, 'build')))
|
|
.map((d) => `vendor/${d}/build`);
|
|
}
|
|
|
|
function collectGrammars(vendorDir, shipsVendorSource) {
|
|
if (!fs.existsSync(vendorDir)) return [];
|
|
return fs
|
|
.readdirSync(vendorDir)
|
|
.filter((d) => /^tree-sitter-/.test(d))
|
|
.map((name) => {
|
|
const dir = path.join(vendorDir, name);
|
|
return {
|
|
name,
|
|
prebuilt: countPrebuiltTuples(dir),
|
|
// Source ships when `files` includes the vendor subtree AND the grammar
|
|
// actually carries a buildable source set on disk.
|
|
shipsSource: shipsVendorSource && isBuildableFromSource(dir),
|
|
};
|
|
});
|
|
}
|
|
|
|
function main() {
|
|
const gitnexusRoot = path.join(__dirname, '..');
|
|
const vendorDir = path.join(gitnexusRoot, 'vendor');
|
|
const pkg = JSON.parse(fs.readFileSync(path.join(gitnexusRoot, 'package.json'), 'utf8'));
|
|
const shipsVendorSource = filesShipsVendorSource(pkg.files);
|
|
|
|
const grammars = collectGrammars(vendorDir, shipsVendorSource);
|
|
if (grammars.length === 0) {
|
|
console.error(`[publish-guard] No vendored tree-sitter grammars found under ${vendorDir}.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const stray = findStrayBuildArtifacts(vendorDir);
|
|
if (stray.length > 0) {
|
|
console.error(
|
|
'[publish-guard] Refusing to publish — stray source-build output under vendor/ would\n' +
|
|
'ship and shadow the committed prebuilds (node-gyp-build loads build/Release before\n' +
|
|
'prebuilds/):',
|
|
);
|
|
for (const s of stray) console.error(` - ${s}`);
|
|
console.error('\nFix: remove it before packing, e.g. `rm -rf gitnexus/vendor/*/build`.');
|
|
process.exit(1);
|
|
}
|
|
|
|
const problems = findCoverageProblems({ grammars });
|
|
if (problems.length > 0) {
|
|
console.error('[publish-guard] Refusing to publish — a vendored grammar would ship unusable:');
|
|
for (const p of problems) console.error(` - ${p}`);
|
|
console.error(
|
|
'\nFix: either commit the missing prebuilds (run the build-tree-sitter-prebuilds\n' +
|
|
'workflow) or keep the vendored source in the package.json `files` field.',
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const sourceShippers = grammars.filter((g) => g.shipsSource).length;
|
|
console.log(
|
|
`[publish-guard] OK — ${grammars.length} vendored grammar(s) covered ` +
|
|
`(${sourceShippers} shipping source, ${grammars.length - sourceShippers} prebuilds-only).`,
|
|
);
|
|
}
|
|
|
|
if (require.main === module) main();
|
|
|
|
module.exports = {
|
|
findCoverageProblems,
|
|
findStrayBuildArtifacts,
|
|
filesShipsVendorSource,
|
|
isBuildableFromSource,
|
|
sourceBuildSet,
|
|
countPrebuiltTuples,
|
|
collectGrammars,
|
|
TUPLES,
|
|
SOURCE_BUILD_REL,
|
|
};
|