From 12a9dcf5414c29b3f903549a3d4bd5fc04ba82a5 Mon Sep 17 00:00:00 2001 From: Townrain <95573719+Townrain@users.noreply.github.com> Date: Sat, 15 Aug 2026 17:38:42 +0800 Subject: [PATCH] fix: dist ESM bundle crashes on startup; make dsh-qaq plugin zero-runtime-dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two deployment-blocking bugs found while deploying on Windows/Node 24: 1. `pnpm build` output (dist/qaq.mjs) crashed immediately on launch with "Dynamic require of \"events\" is not supported": esbuild bundles the CJS `ws` dependency into ESM output, and ws's internal require() calls to node builtins (events/stream) hit esbuild's __require shim, which throws in ESM scope. The test suite never caught this because it runs the source through tsx, never the dist bundle. Fix: add an esbuild banner that defines `require` via createRequire(import.meta.url). 2. `qaq install-plugin` could break the very boot it guards: the plugin imported @deepseek-ai/dsh-home-paths at runtime, but the plugin is mounted into a profile via a junction pointing outside the DSH tree, and bare specifiers resolve up the QAQ repo's node_modules (which never contains @deepseek-ai) — so the plugin entry would fail to load on the next boot. Fix: inline resolveDshHome (DSH_HOME or ~/.dsh, identical semantics to the DSH package) so the plugin has zero runtime dependencies. The rebuilt lib/index.js is committed alongside. Verified: 53/53 unit tests pass; dist/qaq.mjs runs (status/help); plugin imports cleanly from the junction location and snapshots latest-good + history correctly in an isolated DSH_HOME. --- package.json | 4 ++-- packages/dsh-qaq/lib/index.js | 9 +++++++-- packages/dsh-qaq/package.json | 6 ++---- packages/dsh-qaq/src/index.ts | 18 +++++++++++++++--- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index a8c6050..8bd8b55 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "qaq": "./bin/qaq.mjs" }, "scripts": { - "build": "esbuild src/cli.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/qaq.mjs", + "build": "esbuild src/cli.ts --bundle --platform=node --format=esm --target=node22 --banner:js=\"import { createRequire as __qaqCRequire } from 'module'; const require = __qaqCRequire(import.meta.url);\" --outfile=dist/qaq.mjs", "qaq": "tsx src/cli.ts", "test": "vitest run", "smoke": "node tools/smoke.mjs", @@ -31,4 +31,4 @@ "typescript": "^5.6.0", "vitest": "^4.1.0" } -} \ No newline at end of file +} diff --git a/packages/dsh-qaq/lib/index.js b/packages/dsh-qaq/lib/index.js index 958b919..dfaadcc 100644 --- a/packages/dsh-qaq/lib/index.js +++ b/packages/dsh-qaq/lib/index.js @@ -1,7 +1,12 @@ -import { join } from "node:path"; +import { join, resolve } from "node:path"; +import { homedir } from "node:os"; import { copyFileSync, mkdirSync, writeFileSync, existsSync, readdirSync, rmSync } from "node:fs"; -import { resolveDshHome } from "@deepseek-ai/dsh-home-paths"; const name = "dsh-qaq"; +function resolveDshHome() { + const fromEnv = process.env.DSH_HOME; + const selected = fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : join(homedir(), ".dsh"); + return resolve(selected); +} const QAQ_DIR = ".qaq"; const KEEP = 5; const FILES = ["package.json", "cordis.patch.yml"]; diff --git a/packages/dsh-qaq/package.json b/packages/dsh-qaq/package.json index df1aa97..eecf47a 100644 --- a/packages/dsh-qaq/package.json +++ b/packages/dsh-qaq/package.json @@ -18,12 +18,10 @@ } }, "peerDependencies": { - "@deepseek-ai/cordis": "^4.0.1", - "@deepseek-ai/dsh-home-paths": ">=0.1.0" + "@deepseek-ai/cordis": "^4.0.1" }, "peerDependenciesMeta": { - "@deepseek-ai/cordis": { "optional": true }, - "@deepseek-ai/dsh-home-paths": { "optional": true } + "@deepseek-ai/cordis": { "optional": true } }, "files": ["lib", "cordis.patch.yml"] } diff --git a/packages/dsh-qaq/src/index.ts b/packages/dsh-qaq/src/index.ts index 2705ab8..23e539a 100644 --- a/packages/dsh-qaq/src/index.ts +++ b/packages/dsh-qaq/src/index.ts @@ -5,13 +5,25 @@ * and history/. Backup-only: it never detects failure, never rolls back, and * changes no DSH behavior. */ -import { join } from 'node:path' +import { join, resolve } from 'node:path' +import { homedir } from 'node:os' import { copyFileSync, mkdirSync, writeFileSync, existsSync, readdirSync, rmSync } from 'node:fs' import type { Context } from '@deepseek-ai/cordis' -import { resolveDshHome } from '@deepseek-ai/dsh-home-paths' export const name = 'dsh-qaq' +/** Inline equivalent of @deepseek-ai/dsh-home-paths' resolveDshHome: a non-empty + * DSH_HOME wins, otherwise ~/.dsh. Inlined (instead of imported) so the plugin + * has ZERO runtime dependencies: it is mounted into a profile via a junction + * from outside the DSH tree, and a bare-specifier import would walk up the + * QAQ repo's node_modules — which never contains @deepseek-ai — and fail the + * very boot this tool exists to guard. */ +function resolveDshHome(): string { + const fromEnv = process.env.DSH_HOME + const selected = fromEnv !== undefined && fromEnv.trim().length > 0 ? fromEnv : join(homedir(), '.dsh') + return resolve(selected) +} + const QAQ_DIR = '.qaq' const KEEP = 5 const FILES = ['package.json', 'cordis.patch.yml'] as const @@ -72,4 +84,4 @@ function inferProfileName(ctx: Context): string | null { const cwd = (process.env.INIT_CWD ?? process.cwd()) const m = cwd.match(/profiles[\\/]([^\\/]+)/) return m ? m[1] : null -} \ No newline at end of file +}