Skip to content

Instantly share code, notes, and snippets.

@roman01la
Created October 30, 2025 21:40
Show Gist options
  • Save roman01la/b9b5cbd60bcd87551884417428e38749 to your computer and use it in GitHub Desktop.
Save roman01la/b9b5cbd60bcd87551884417428e38749 to your computer and use it in GitHub Desktop.
#!/usr/bin/env node
/*
Generate JavaScript stubs for npm dependencies so Cursive can index them
and provide CLJS completions even when node_modules is excluded.
Usage:
node generate-js-stubs.js --output stubs/js --depth 2 --max-props 200 --include-dev false --filter react*,lodash*
*/
const fs = require("fs");
const path = require("path");
const { pathToFileURL } = require("url");
function parseArgs(argv) {
const args = {
output: "resources/stubs",
depth: 10,
maxProps: 1000,
includeDev: true,
filter: null,
packageJson: "package.json",
};
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
const next = () => (i + 1 < argv.length ? argv[++i] : undefined);
if (a === "--output") args.output = next();
else if (a === "--depth") args.depth = Number(next());
else if (a === "--max-props") args.maxProps = Number(next());
else if (a === "--include-dev")
args.includeDev = /^(true|1)$/i.test(next());
else if (a === "--filter") args.filter = next();
else if (a === "--package-json") args.packageJson = next();
}
return args;
}
function readJson(file) {
return JSON.parse(fs.readFileSync(file, "utf8"));
}
function ensureDir(dir) {
fs.mkdirSync(dir, { recursive: true });
}
function isValidIdentifierStart(ch) {
return /[A-Za-z_$]/.test(ch);
}
function isValidIdentifierChar(ch) {
return /[A-Za-z0-9_$]/.test(ch);
}
function toIdentifier(name) {
let out = "";
for (const ch of name.replace(/^@/, "").replace(/[\/]/g, "_")) {
out += isValidIdentifierChar(ch) ? ch : "_";
}
if (!out || !isValidIdentifierStart(out[0])) out = "_" + out;
return out;
}
function globToRegex(glob) {
// very small glob: * matches any chars, ? one char
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
const re = "^" + escaped.replace(/\*/g, ".*").replace(/\?/g, ".") + "$";
return new RegExp(re);
}
function filterPkgs(pkgs, filter) {
if (!filter) return pkgs;
const parts = filter
.split(",")
.map((s) => s.trim())
.filter(Boolean);
const regs = parts.map(globToRegex);
return pkgs.filter((p) => regs.some((r) => r.test(p)));
}
async function tryLoad(pkgName, basedir) {
// Try CommonJS require first; if it is ESM-only, try dynamic import.
try {
return { mod: require(pkgName), kind: "cjs" };
} catch (e) {
// Resolve path to file for ESM import
try {
const resolved = require.resolve(pkgName, { paths: [basedir] });
const url = pathToFileURL(resolved).href;
const ns = await import(url);
// Prefer default export if present
return { mod: ns && (ns.default ?? ns), kind: "esm" };
} catch (e2) {
return { error: e2 };
}
}
}
function isPlainObject(x) {
return (
x && typeof x === "object" && Object.getPrototypeOf(x) === Object.prototype
);
}
function safeOwnKeys(obj, max) {
try {
const names = Object.getOwnPropertyNames(obj);
return names.slice(0, max);
} catch (_) {
return [];
}
}
function argNamesFor(fn) {
const n = Math.max(0, Math.min(4, fn.length || 0));
return Array.from({ length: n }, (_, i) => "arg" + (i + 1));
}
function enumerateEntries(
value,
opts,
depth = 0,
seen = new WeakSet(),
prefix = []
) {
const entries = [];
if (!value || typeof value !== "object") return entries;
if (seen.has(value)) return entries;
seen.add(value);
if (depth >= opts.depth) return entries;
const keys = safeOwnKeys(value, opts.maxProps);
for (const k of keys) {
let v;
try {
v = value[k];
} catch (_) {
continue;
}
const pathSeg = String(k);
const newPrefix = prefix.concat(pathSeg);
if (typeof v === "function") {
entries.push({
path: newPrefix,
kind: "function",
arity: Math.min(4, v.length || 0),
});
} else if (v && typeof v === "object") {
// record the object itself as a field to expose it, then traverse
entries.push({ path: newPrefix, kind: "object" });
entries.push(...enumerateEntries(v, opts, depth + 1, seen, newPrefix));
} else {
// primitive – treat as a field for completion
entries.push({ path: newPrefix, kind: "object" });
}
}
return entries;
}
function pathToExpr(rootAlias, pathArray) {
let expr = rootAlias;
for (const seg of pathArray) {
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(seg)) expr += "." + seg;
else expr += `[${JSON.stringify(seg)}]`;
}
return expr;
}
function generateFlatStub(alias, rootValue, opts) {
const entries = enumerateEntries(rootValue, opts, 0, new WeakSet(), []);
const initSet = new Set();
const lines = [];
lines.push(`/* Generated stub — for editor completions only */`);
lines.push(`var ${alias} = {};`);
function ensureParent(pathArr) {
for (let i = 1; i <= pathArr.length - 1; i++) {
const parentPath = pathArr.slice(0, i);
const key = parentPath.join("\u0000");
if (initSet.has(key)) continue;
initSet.add(key);
const parentExpr = pathToExpr(alias, parentPath);
lines.push(`${parentExpr} = ${parentExpr} || {};`);
}
}
for (const e of entries) {
ensureParent(e.path);
const expr = pathToExpr(alias, e.path);
if (e.kind === "function") {
const args = Array.from(
{ length: e.arity },
(_, i) => `arg${i + 1}`
).join(", ");
lines.push(`${expr} = function(${args}) {};`);
} else {
lines.push(`${expr} = ${expr} || {};`);
}
}
lines.push(`module.exports = ${alias};`);
lines.push(`export default ${alias};`);
return lines.join("\n");
}
function writeStub(outDir, pkgName, alias, contents) {
// Support scoped packages by writing nested paths, e.g. out/@scope/pkg.js
const file = path.join(outDir, ...pkgName.split("/")) + ".js";
ensureDir(path.dirname(file));
fs.writeFileSync(file, contents, "utf8");
return file;
}
async function main() {
const args = parseArgs(process.argv);
const pkgJsonPath = path.resolve(args.packageJson);
if (!fs.existsSync(pkgJsonPath)) {
console.error(`package.json not found at ${pkgJsonPath}`);
process.exit(1);
}
const pkg = readJson(pkgJsonPath);
const deps = Object.keys(pkg.dependencies || {});
const devDeps = Object.keys(pkg.devDependencies || {});
let all = args.includeDev ? deps.concat(devDeps) : deps;
all = filterPkgs(all, args.filter);
if (!all.length) {
console.log("No packages selected. Use --filter to target specific deps.");
return;
}
const outDir = path.resolve(args.output);
ensureDir(outDir);
const basedir = path.dirname(pkgJsonPath);
const opts = { depth: args.depth, maxProps: args.maxProps };
const manifest = [];
for (const name of all) {
const alias = toIdentifier(name);
try {
const { mod, error } = await tryLoad(name, basedir);
if (error || !mod) {
console.warn(
`Skipping ${name}: cannot load module (${
(error && error.message) || "unknown error"
})`
);
continue;
}
const root =
mod && (typeof mod === "object" || typeof mod === "function")
? mod
: {};
const stub = generateFlatStub(alias, root, opts);
const file = writeStub(outDir, name, alias, stub);
manifest.push({ package: name, alias, file });
console.log(`Stub written: ${file} (alias: ${alias})`);
} catch (e) {
console.warn(`Error stubbing ${name}: ${e.message}`);
}
}
console.log(
`\nDone. ${manifest.length} stub(s) generated.`
);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment