From 9899a2203077df5cb03027abda1424b40d6490da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=91=A8=E6=B6=9B?= Date: Tue, 9 Jun 2026 13:54:49 +0800 Subject: [PATCH 1/2] fix(cli): make scaffolded projects start cleanly under pnpm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `npx @objectstack/cli init` followed by `pnpm install` && `pnpm start` failed in two ways: 1. better-sqlite3 (pulled in transitively by the default standalone SQLite store, via knex) shipped uncompiled because pnpm 10+ blocks dependency build scripts by default, so `serve` crashed with "Could not locate the bindings file". The allowlist must live in `pnpm-workspace.yaml` — current pnpm ignores the `pnpm` field in package.json — so init now generates one listing better-sqlite3 and esbuild (mirrors how this repo configures itself). npm/yarn/bun ignore the file and build native modules by default. 2. `start` ran `objectstack serve` (production, boots from the compiled dist/objectstack.json), which does not exist right after install, so `pnpm start` failed before any build. The start script now runs `objectstack compile && objectstack serve` so it works straight after `pnpm install`. Verified end-to-end with pnpm v10.33.0: init -> install -> pnpm start auto-compiles and reaches "Server is ready" (HTTP 302), with the better-sqlite3 binding compiled. Added unit coverage for the generated pnpm-workspace.yaml and that the allowlist is not placed in package.json. --- packages/cli/src/commands/init.ts | 44 ++++++++++++++++++++++++++++++- packages/cli/test/init.test.ts | 36 ++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 582e40668..59ff67657 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -61,6 +61,36 @@ export function sanitizeNamespace(name: string): string { return s; } +/** + * Native dependencies the scaffold pulls in (transitively) that need their + * build scripts to run at install time. pnpm 10+ blocks dependency build + * scripts by default; without this allowlist `better-sqlite3` (used by the + * default standalone SQLite store, via knex) ships uncompiled and `serve` + * fails with "Could not locate the bindings file". + * + * Current pnpm reads this from `pnpm-workspace.yaml`, NOT the `pnpm` field in + * package.json (that field is now ignored and emits a deprecation warning). + * npm/yarn/bun build native modules by default and ignore this file. + */ +export const SCAFFOLD_BUILT_DEPENDENCIES = ['better-sqlite3', 'esbuild']; + +/** + * Render the `pnpm-workspace.yaml` that allowlists native build scripts. + * Kept minimal (no `packages:` key) so it acts purely as a settings file for + * the single-package scaffold rather than declaring a workspace. + */ +export function renderPnpmWorkspaceYaml(builtDeps: string[] = SCAFFOLD_BUILT_DEPENDENCIES): string { + return [ + '# Allowlist native dependency build scripts so `pnpm install` compiles', + '# them (pnpm 10+ blocks build scripts by default). Without this,', + '# better-sqlite3 ships uncompiled and `objectstack serve` fails with', + '# "Could not locate the bindings file".', + 'onlyBuiltDependencies:', + ...builtDeps.map((d) => ` - ${d}`), + '', + ].join('\n'); +} + export const TEMPLATES: Record; @@ -88,7 +118,10 @@ export const TEMPLATES: Record { }); }); +describe('native build allowlist (pnpm-workspace.yaml)', () => { + // pnpm 10+ blocks dependency build scripts by default. Without an allowlist, + // the scaffold installs but `serve` crashes with "Could not locate the + // bindings file" because `better-sqlite3` shipped uncompiled. Current pnpm + // reads the allowlist from pnpm-workspace.yaml, not the package.json `pnpm` + // field (which it now ignores). + it('includes the native deps the standalone store needs', () => { + expect(SCAFFOLD_BUILT_DEPENDENCIES).toContain('better-sqlite3'); + }); + + it('does NOT put the allowlist in package.json (current pnpm ignores it)', () => { + const t = TEMPLATES.app; + // Mirror init.ts's package.json construction. + const pkgJson: Record = { + name: 'my-app', + version: '0.1.0', + private: true, + type: 'module', + scripts: t.scripts, + dependencies: t.dependencies, + devDependencies: t.devDependencies, + }; + expect(pkgJson.pnpm).toBeUndefined(); + }); + + it('renders a pnpm-workspace.yaml that allowlists better-sqlite3', () => { + const yaml = renderPnpmWorkspaceYaml(); + expect(yaml).toMatch(/^onlyBuiltDependencies:/m); + expect(yaml).toMatch(/^ {2}- better-sqlite3$/m); + // No `packages:` key — this is a settings file, not a workspace declaration. + expect(yaml).not.toMatch(/^packages:/m); + }); +}); + describe('sanitizeNamespace', () => { const NS_RE = /^[a-z][a-z0-9_]{1,19}$/; From b55160e85e8e071d76c8026b449f079706d25002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=91=A8=E6=B6=9B?= Date: Tue, 9 Jun 2026 14:29:33 +0800 Subject: [PATCH 2/2] fix(cli): write pnpm-workspace.yaml atomically (avoid TOCTOU) CodeQL flagged the exists()-then-writeFileSync pattern as a file-system race. Use an exclusive write (flag: 'wx') and ignore EEXIST so the "don't clobber an existing file" decision is atomic. --- packages/cli/src/commands/init.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 59ff67657..ca36a67c1 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -440,10 +440,14 @@ export default class Init extends Command { // 1b. Create pnpm-workspace.yaml so pnpm compiles the native deps the // standalone store needs (see SCAFFOLD_BUILT_DEPENDENCIES). Harmless for // npm/yarn/bun, which ignore this file and build native modules anyway. + // Use an exclusive write ('wx') rather than exists()-then-write so the + // "don't clobber an existing file" check is atomic (no TOCTOU race). const pnpmWorkspacePath = path.join(targetDir, 'pnpm-workspace.yaml'); - if (!fs.existsSync(pnpmWorkspacePath)) { - fs.writeFileSync(pnpmWorkspacePath, renderPnpmWorkspaceYaml()); + try { + fs.writeFileSync(pnpmWorkspacePath, renderPnpmWorkspaceYaml(), { flag: 'wx' }); createdFiles.push('pnpm-workspace.yaml'); + } catch (err: any) { + if (err?.code !== 'EEXIST') throw err; } // 2. Create objectstack.config.ts