fix(launcher): prefer Node+tsx over Bun in source mode when both lockfiles exist
Source-mode runner selection now mirrors the dist-mode 'npm priority' rule: if both package-lock.json and bun.lock are present in the package root, use Node + tsx instead of Bun. pnpm/npm installs ship Node-ABI native modules (better-sqlite3, sqlite-vec), and routing through Bun produces ABI mismatches. This also fixes pnpm-global installs, which copy the entire working tree (including .git and bun.lock) into <prefix>/node_modules/@tobilu/qmd/. The old logic saw .git + bun.lock + bun-on-PATH and routed to Bun against the Node-installed native modules. Adds a regression test covering the both-lockfiles source-checkout case.
This commit is contained in:
parent
3de3162e1a
commit
0d7fdb7589
@ -35,6 +35,15 @@
|
||||
boolean aliases (`--json`/`--csv`/`--md`/`--xml`/`--files`) still work but are
|
||||
no longer in `--help`; prefer `--format`.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Launcher: source-mode runner selection now prefers Node + tsx over Bun when
|
||||
both `package-lock.json` and `bun.lock` are present in the package root,
|
||||
mirroring the dist-mode "npm priority" rule. Fixes pnpm-global installs that
|
||||
copy the entire working tree (including `.git` and `bun.lock`) into the
|
||||
install dir and previously routed through Bun, causing ABI mismatches with
|
||||
the Node-built `better-sqlite3` / `sqlite-vec` native modules.
|
||||
|
||||
### Docs
|
||||
|
||||
- qmd skill: emphasize reading line ranges with `get`'s built-in
|
||||
|
||||
50
bin/qmd
50
bin/qmd
@ -43,22 +43,52 @@ function hasBun() {
|
||||
// dist/ is often ignored and can be stale after git reset or branch switches.
|
||||
// Prefer source mode only for checkouts so ./bin/qmd reflects the checked-out
|
||||
// source without changing packaged/runtime behavior.
|
||||
//
|
||||
// Critical: source-mode detection must NOT trigger when a package manager
|
||||
// installed us. `pnpm install -g .` (and `npm install -g .`) copy the entire
|
||||
// working tree — including .git/, bun.lock, package-lock.json, src/, and even
|
||||
// node_modules/ — into <prefix>/node_modules/@tobilu/qmd/, so .git and a
|
||||
// lockfile being present is not a reliable "this is a working tree" signal.
|
||||
// What IS reliable: a package-manager install always lands the package
|
||||
// directory inside a `node_modules/` segment; a bare working-tree checkout
|
||||
// (with `bun link` or a direct path invocation) does not. Gate source mode
|
||||
// on that. Allow QMD_SOURCE_MODE=1 / =0 as an explicit override for the
|
||||
// rare case where the heuristic disagrees with the user.
|
||||
const sourceOverride = process.env.QMD_SOURCE_MODE;
|
||||
const looksInstalled = pkgDir.split("/").includes("node_modules");
|
||||
const sourceAllowed = sourceOverride === "1"
|
||||
|| (sourceOverride !== "0" && !looksInstalled);
|
||||
|
||||
let useSourceMode = false;
|
||||
let sourceRunner = null;
|
||||
let sourceArgs = [];
|
||||
|
||||
if (existsSync(resolve(pkgDir, ".git")) && existsSync(tsEntry)) {
|
||||
if (existsSync(resolve(pkgDir, "bun.lock")) || existsSync(resolve(pkgDir, "bun.lockb"))) {
|
||||
if (hasBun()) {
|
||||
useSourceMode = true;
|
||||
sourceRunner = "bun";
|
||||
sourceArgs = [tsEntry, ...process.argv.slice(2)];
|
||||
}
|
||||
}
|
||||
if (!useSourceMode && existsSync(resolve(pkgDir, "node_modules/tsx/dist/cli.mjs"))) {
|
||||
if (sourceAllowed && existsSync(resolve(pkgDir, ".git")) && existsSync(tsEntry)) {
|
||||
// Lockfile-driven runner selection — mirror the dist-mode logic below so
|
||||
// source mode picks the same runtime the user's deps were installed for.
|
||||
// package-lock.json wins over bun.lock when both are present: pnpm/npm
|
||||
// installs ship the Node-ABI native modules (better-sqlite3, sqlite-vec),
|
||||
// and running Bun against them produces ABI mismatches. This also fixes
|
||||
// pnpm-global installs, which copy the whole working tree — including .git
|
||||
// and bun.lock — into the install dir and used to route through Bun even
|
||||
// when the user installed via npm/pnpm.
|
||||
const hasNpmLock = existsSync(resolve(pkgDir, "package-lock.json"));
|
||||
const hasBunLock = existsSync(resolve(pkgDir, "bun.lock")) || existsSync(resolve(pkgDir, "bun.lockb"));
|
||||
const tsxEntry = resolve(pkgDir, "node_modules/tsx/dist/cli.mjs");
|
||||
const tsxAvailable = existsSync(tsxEntry);
|
||||
|
||||
if (hasNpmLock && tsxAvailable) {
|
||||
useSourceMode = true;
|
||||
sourceRunner = "node";
|
||||
sourceArgs = [resolve(pkgDir, "node_modules/tsx/dist/cli.mjs"), tsEntry, ...process.argv.slice(2)];
|
||||
sourceArgs = [tsxEntry, tsEntry, ...process.argv.slice(2)];
|
||||
} else if (hasBunLock && hasBun()) {
|
||||
useSourceMode = true;
|
||||
sourceRunner = "bun";
|
||||
sourceArgs = [tsEntry, ...process.argv.slice(2)];
|
||||
} else if (tsxAvailable) {
|
||||
useSourceMode = true;
|
||||
sourceRunner = "node";
|
||||
sourceArgs = [tsxEntry, tsEntry, ...process.argv.slice(2)];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -226,6 +226,21 @@ describe("bin/qmd package wrapper", () => {
|
||||
expect(result.args).toEqual([realpathSync(join(packageRoot, "src", "cli", "qmd.ts")), "--version"]);
|
||||
});
|
||||
|
||||
test("source checkout with both bun.lock and package-lock.json prefers node+tsx", () => {
|
||||
// Mirrors the dist-mode "npm priority" rule: a working tree that has both
|
||||
// lockfiles (because the user ran `npm install` against a repo that also
|
||||
// ships bun.lock) installed native modules for Node's ABI, so source mode
|
||||
// must route through tsx to avoid better-sqlite3 / sqlite-vec mismatches.
|
||||
const { root, runtimeBin, capturePath } = makeTempFixture();
|
||||
const packageRoot = makePackage(root, "qmd", ["bun.lock", "package-lock.json"], { source: true, tsx: true, git: true });
|
||||
|
||||
const result = runWrapper(join(packageRoot, "bin", "qmd"), runtimeBin, capturePath);
|
||||
|
||||
expect(result.runtime).toBe("node");
|
||||
expect(result.scriptPath).toBe(realpathSync(join(packageRoot, "node_modules", "tsx", "dist", "cli.mjs")));
|
||||
expect(result.args).toEqual([realpathSync(join(packageRoot, "src", "cli", "qmd.ts")), "--version"]);
|
||||
});
|
||||
|
||||
test("explains how to build when dist is missing and source cannot run", () => {
|
||||
const { root, runtimeBin } = makeTempFixture();
|
||||
const packageRoot = makePackage(root, "qmd", [], { dist: false });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user