GitNexus/gitnexus/scripts/prepare-tree-sitter-bundle.cjs
Gergő Magyar a532e08ef9
fix(deps): make tree-sitter peer dependencies install cleanly (#3555)
* fix(deps): make tree-sitter peer installs clean

Keep the eight audited lockfile peer ranges consistent with the bundled tree-sitter 0.25.1 runtime and apply the existing manifest preparation during postinstall. Ordinary and strict npm installs resolve without peer warnings.

* docs: explain audited tree-sitter peer maintenance

* fix(deps): pin audited tree-sitter peers to 0.25.1

* fix(deps): normalize audited peers to the exact runtime

* docs: clarify exact tree-sitter runtime peer pins

* test(deps): cover exact peer pins and existing-install migration

Verify exact 0.25.1 peers, repeatable preparation, and migration from the previously widened peer metadata.

* fix(deps): support pnpm symlinks in bundle preparation (#3555)

Follow package symlinks and pnpm sibling dependency containers. Deduplicate real paths to avoid revisiting aliases or cycles while preserving the audited manifest and duplicate-instance checks.

Validated with strict npm ci, both typechecks, npm pack, a real pnpm layout, and a pnpm-installed tarball parsing all 19 grammars on main and worker threads.

* test(deps): cover pnpm layout and symlink cycles (#3555)

Exercise real package symlinks, transitive sibling dependencies, a self-cycle, metadata drift before writes, and rejection of distinct duplicate grammar instances.

The original script fails the pnpm regressions; the updated script passes all 10 bundle tests.

* chore(autofix): apply prettier + eslint fixes via /autofix command

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-10 13:01:20 +00:00

117 lines
4.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Prepare the tested native parser dependencies for installation and publication.
*
* A dependency's npm overrides do not apply in a consumer project. Several
* ABI-compatible grammars still advertise older runtime peers, so bundling
* alone leaves an invalid dependency tree. Admit only the runtime version we
* tested, in only the audited manifests below. Sources and binaries are intact.
*/
const fs = require('node:fs');
const path = require('node:path');
const RUNTIME = '0.25.1';
// Upstream metadata is an input to validate, never the peer range we ship.
const AUDITED_PEERS = new Map([
['tree-sitter-c@0.23.6', '^0.22.1'],
['tree-sitter-cpp@0.23.4', '^0.21.1'],
['tree-sitter-java@0.23.5', '^0.21.1'],
['tree-sitter-php@0.24.2', '^0.22.4'],
['tree-sitter-ruby@0.23.1', '^0.21.1'],
['tree-sitter-rust@0.24.0', '^0.22.1'],
['tree-sitter-javascript@0.23.1', '^0.21.1'],
['tree-sitter-typescript@0.23.2', '^0.21.0'],
]);
const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
function prepareTreeSitterBundle(packageRoot = path.resolve(__dirname, '..')) {
const modulesRoot = path.join(packageRoot, 'node_modules');
const manifest = readJson(path.join(packageRoot, 'package.json'));
if (manifest.dependencies['tree-sitter'] !== RUNTIME) {
throw new Error(`Tree-sitter bundle is only validated for runtime ${RUNTIME}`);
}
const grammars = Object.keys(manifest.dependencies).filter(
(name) => name === 'tree-sitter' || name.startsWith('tree-sitter-'),
);
for (const name of grammars) {
if (
!Array.isArray(manifest.bundleDependencies) ||
!manifest.bundleDependencies.includes(name)
) {
throw new Error(`${name} must be bundled; consumers do not inherit npm overrides`);
}
const installed = readJson(path.join(modulesRoot, name, 'package.json'));
if (installed.version !== manifest.dependencies[name]) {
throw new Error(`${name}: installed version differs from the exact package.json pin`);
}
}
const patches = [];
const visited = new Set();
function visit(modules) {
if (!fs.existsSync(modules)) return;
modules = fs.realpathSync(modules);
if (visited.has(modules)) return;
visited.add(modules);
for (const entry of fs.readdirSync(modules, { withFileTypes: true })) {
if (entry.name.startsWith('.')) continue;
const dir = path.join(modules, entry.name);
if (
!entry.isDirectory() &&
(!entry.isSymbolicLink() || !fs.statSync(dir, { throwIfNoEntry: false })?.isDirectory())
) {
continue;
}
if (entry.name.startsWith('@')) {
visit(dir);
continue;
}
const realDir = fs.realpathSync(dir);
if (visited.has(realDir)) continue;
visited.add(realDir);
const file = path.join(realDir, 'package.json');
if (fs.existsSync(file)) {
const pkg = readJson(file);
const original = AUDITED_PEERS.get(`${pkg.name}@${pkg.version}`);
if (original) {
// Also accept bundles prepared before peers were pinned exactly, so
// an existing checkout can migrate through ordinary npm install.
const accepted = [original, `${original} || ${RUNTIME}`, RUNTIME];
if (!accepted.includes(pkg.peerDependencies?.['tree-sitter'])) {
throw new Error(`Unexpected tree-sitter peer metadata for ${pkg.name}@${pkg.version}`);
}
pkg.peerDependencies['tree-sitter'] = RUNTIME;
patches.push({ file, pkg });
}
}
visit(path.join(realDir, 'node_modules'));
// pnpm links packages into node_modules but places their dependencies
// beside the real package. Follow that container without traversing the
// entire virtual store; realpaths deduplicate aliases and break cycles.
let parent = path.dirname(realDir);
if (path.basename(parent).startsWith('@')) parent = path.dirname(parent);
if (path.basename(parent) === 'node_modules') visit(parent);
}
}
visit(modulesRoot);
const identities = new Set(patches.map(({ pkg }) => `${pkg.name}@${pkg.version}`));
if (patches.length !== AUDITED_PEERS.size || identities.size !== AUDITED_PEERS.size) {
throw new Error('Tree-sitter dependency layout changed; revalidate the audited peer manifests');
}
// Validate the complete set before writing, so dependency drift cannot leave
// a partially prepared bundle behind. Repeated pack operations are idempotent.
for (const { file, pkg } of patches) {
fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + '\n');
}
return patches.map(({ pkg }) => `${pkg.name}@${pkg.version}`);
}
if (require.main === module) {
const prepared = prepareTreeSitterBundle();
console.log(
`[tree-sitter-bundle] Validated runtime ${RUNTIME}; prepared ${prepared.length} peer manifests.`,
);
}
module.exports = { prepareTreeSitterBundle };