fix: run qmd from unbuilt checkouts

This commit is contained in:
Tobi Lutke 2026-05-19 15:22:13 -04:00
parent 632c34d120
commit 2b250f3dca
No known key found for this signature in database
2 changed files with 84 additions and 7 deletions

31
bin/qmd
View File

@ -25,6 +25,31 @@ if [ "$1" = "mcp" ]; then
export GGML_BACKEND_SILENT="${GGML_BACKEND_SILENT:-1}"
fi
JS="$DIR/dist/cli/qmd.js"
# In published packages dist/ is always present. In a fresh checkout, however,
# people often run ./bin/qmd before building. Prefer a source-mode fallback when
# dependencies are installed; otherwise fail with an actionable message instead
# of a low-level "Module not found" from Node/Bun.
if [ ! -f "$JS" ]; then
TS="$DIR/src/cli/qmd.ts"
if [ -f "$TS" ]; then
if [ -f "$DIR/bun.lock" ] || [ -f "$DIR/bun.lockb" ]; then
if command -v bun >/dev/null 2>&1; then
exec bun "$TS" "$@"
fi
fi
if [ -f "$DIR/node_modules/tsx/dist/cli.mjs" ]; then
exec node "$DIR/node_modules/tsx/dist/cli.mjs" "$TS" "$@"
fi
fi
echo "qmd is not built: missing $JS" >&2
echo "Run: bun install && bun run build" >&2
echo "Or: npm install && npm run build" >&2
exit 1
fi
# Detect the package manager that installed dependencies by checking lockfiles.
# $BUN_INSTALL is intentionally NOT checked — it only indicates that bun exists
# on the system, not that it was used to install this package (see #361).
@ -34,9 +59,9 @@ fi
# builds that use npm would be incorrectly routed to bun, causing ABI
# mismatches with better-sqlite3 / sqlite-vec (see #381).
if [ -f "$DIR/package-lock.json" ]; then
exec node "$DIR/dist/cli/qmd.js" "$@"
exec node "$JS" "$@"
elif [ -f "$DIR/bun.lock" ] || [ -f "$DIR/bun.lockb" ]; then
exec bun "$DIR/dist/cli/qmd.js" "$@"
exec bun "$JS" "$@"
else
exec node "$DIR/dist/cli/qmd.js" "$@"
exec node "$JS" "$@"
fi

View File

@ -2,7 +2,7 @@ import { afterEach, describe, expect, test } from "vitest";
import { chmodSync, copyFileSync, mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, relative } from "node:path";
import { execFileSync } from "node:child_process";
import { execFileSync, spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const repoRoot = fileURLToPath(new URL("..", import.meta.url));
@ -27,13 +27,24 @@ function makeTempFixture() {
return { root, capturePath, runtimeBin };
}
function makePackage(root: string, packagePath: string, lockfiles: string[] = []) {
function makePackage(root: string, packagePath: string, lockfiles: string[] = [], options: { dist?: boolean; source?: boolean; tsx?: boolean } = {}) {
const packageRoot = join(root, packagePath);
const includeDist = options.dist ?? true;
mkdirSync(join(packageRoot, "bin"), { recursive: true });
mkdirSync(join(packageRoot, "dist", "cli"), { recursive: true });
copyFileSync(join(repoRoot, "bin", "qmd"), join(packageRoot, "bin", "qmd"));
chmodSync(join(packageRoot, "bin", "qmd"), 0o755);
writeFileSync(join(packageRoot, "dist", "cli", "qmd.js"), "// fixture\n");
if (includeDist) {
mkdirSync(join(packageRoot, "dist", "cli"), { recursive: true });
writeFileSync(join(packageRoot, "dist", "cli", "qmd.js"), "// fixture\n");
}
if (options.source) {
mkdirSync(join(packageRoot, "src", "cli"), { recursive: true });
writeFileSync(join(packageRoot, "src", "cli", "qmd.ts"), "// source fixture\n");
}
if (options.tsx) {
mkdirSync(join(packageRoot, "node_modules", "tsx", "dist"), { recursive: true });
writeFileSync(join(packageRoot, "node_modules", "tsx", "dist", "cli.mjs"), "// tsx fixture\n");
}
for (const lockfile of lockfiles) {
writeFileSync(join(packageRoot, lockfile), "");
}
@ -161,4 +172,45 @@ describe("bin/qmd package wrapper", () => {
expect(result.runtime).toBe("node");
expect(result.scriptPath).toBe(realpathSync(join(packageRoot, "dist", "cli", "qmd.js")));
});
test("falls back to source with bun in an unbuilt Bun checkout", () => {
const { root, runtimeBin, capturePath } = makeTempFixture();
const packageRoot = makePackage(root, "qmd", ["bun.lock"], { dist: false, source: true });
const result = runWrapper(join(packageRoot, "bin", "qmd"), runtimeBin, capturePath);
expect(result.runtime).toBe("bun");
expect(result.scriptPath).toBe(realpathSync(join(packageRoot, "src", "cli", "qmd.ts")));
expect(result.args).toEqual(["--version"]);
});
test("falls back to source through tsx in an unbuilt Node checkout", () => {
const { root, runtimeBin, capturePath } = makeTempFixture();
const packageRoot = makePackage(root, "qmd", [], { dist: false, source: true, tsx: 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 });
const result = spawnSync(join(packageRoot, "bin", "qmd"), ["--version"], {
env: {
...process.env,
PATH: `${runtimeBin}:${process.env.PATH ?? ""}`,
},
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
expect(result.status).toBe(1);
expect(result.stderr).toContain("qmd is not built");
expect(result.stderr).toContain("bun install && bun run build");
expect(result.stderr).toContain("npm install && npm run build");
});
});