Skip to content

Commit 2c3c0cd

Browse files
authored
fix(cli): make scaffolded projects start cleanly under pnpm (#1669)
1 parent 7c00720 commit 2c3c0cd

2 files changed

Lines changed: 82 additions & 2 deletions

File tree

packages/cli/src/commands/init.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,36 @@ export function sanitizeNamespace(name: string): string {
6161
return s;
6262
}
6363

64+
/**
65+
* Native dependencies the scaffold pulls in (transitively) that need their
66+
* build scripts to run at install time. pnpm 10+ blocks dependency build
67+
* scripts by default; without this allowlist `better-sqlite3` (used by the
68+
* default standalone SQLite store, via knex) ships uncompiled and `serve`
69+
* fails with "Could not locate the bindings file".
70+
*
71+
* Current pnpm reads this from `pnpm-workspace.yaml`, NOT the `pnpm` field in
72+
* package.json (that field is now ignored and emits a deprecation warning).
73+
* npm/yarn/bun build native modules by default and ignore this file.
74+
*/
75+
export const SCAFFOLD_BUILT_DEPENDENCIES = ['better-sqlite3', 'esbuild'];
76+
77+
/**
78+
* Render the `pnpm-workspace.yaml` that allowlists native build scripts.
79+
* Kept minimal (no `packages:` key) so it acts purely as a settings file for
80+
* the single-package scaffold rather than declaring a workspace.
81+
*/
82+
export function renderPnpmWorkspaceYaml(builtDeps: string[] = SCAFFOLD_BUILT_DEPENDENCIES): string {
83+
return [
84+
'# Allowlist native dependency build scripts so `pnpm install` compiles',
85+
'# them (pnpm 10+ blocks build scripts by default). Without this,',
86+
'# better-sqlite3 ships uncompiled and `objectstack serve` fails with',
87+
'# "Could not locate the bindings file".',
88+
'onlyBuiltDependencies:',
89+
...builtDeps.map((d) => ` - ${d}`),
90+
'',
91+
].join('\n');
92+
}
93+
6494
export const TEMPLATES: Record<string, {
6595
description: string;
6696
dependencies: Record<string, string>;
@@ -88,7 +118,10 @@ export const TEMPLATES: Record<string, {
88118
},
89119
scripts: {
90120
dev: 'objectstack dev',
91-
start: 'objectstack serve',
121+
// `serve` is production mode and boots from the compiled artifact
122+
// (dist/objectstack.json). Compile first so `pnpm start` works straight
123+
// after `pnpm install` without a separate build step.
124+
start: 'objectstack compile && objectstack serve',
92125
build: 'objectstack compile',
93126
validate: 'objectstack validate',
94127
typecheck: 'tsc --noEmit',
@@ -404,6 +437,19 @@ export default class Init extends Command {
404437
printInfo('package.json already exists, skipping');
405438
}
406439

440+
// 1b. Create pnpm-workspace.yaml so pnpm compiles the native deps the
441+
// standalone store needs (see SCAFFOLD_BUILT_DEPENDENCIES). Harmless for
442+
// npm/yarn/bun, which ignore this file and build native modules anyway.
443+
// Use an exclusive write ('wx') rather than exists()-then-write so the
444+
// "don't clobber an existing file" check is atomic (no TOCTOU race).
445+
const pnpmWorkspacePath = path.join(targetDir, 'pnpm-workspace.yaml');
446+
try {
447+
fs.writeFileSync(pnpmWorkspacePath, renderPnpmWorkspaceYaml(), { flag: 'wx' });
448+
createdFiles.push('pnpm-workspace.yaml');
449+
} catch (err: any) {
450+
if (err?.code !== 'EEXIST') throw err;
451+
}
452+
407453
// 2. Create objectstack.config.ts
408454
const configContent = template.configContent(projectName, namespace);
409455
fs.writeFileSync(path.join(targetDir, 'objectstack.config.ts'), configContent);

packages/cli/test/init.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import fs from 'fs';
55
import os from 'os';
66
import path from 'path';
77
import { fileURLToPath } from 'url';
8-
import { TEMPLATES, getCliVersion, detectPackageManager, sanitizeNamespace } from '../src/commands/init';
8+
import { TEMPLATES, getCliVersion, detectPackageManager, sanitizeNamespace, SCAFFOLD_BUILT_DEPENDENCIES, renderPnpmWorkspaceYaml } from '../src/commands/init';
99

1010
const __dirname = path.dirname(fileURLToPath(import.meta.url));
1111
const pkg = JSON.parse(
@@ -51,6 +51,40 @@ describe('init command — published scaffold', () => {
5151
});
5252
});
5353

54+
describe('native build allowlist (pnpm-workspace.yaml)', () => {
55+
// pnpm 10+ blocks dependency build scripts by default. Without an allowlist,
56+
// the scaffold installs but `serve` crashes with "Could not locate the
57+
// bindings file" because `better-sqlite3` shipped uncompiled. Current pnpm
58+
// reads the allowlist from pnpm-workspace.yaml, not the package.json `pnpm`
59+
// field (which it now ignores).
60+
it('includes the native deps the standalone store needs', () => {
61+
expect(SCAFFOLD_BUILT_DEPENDENCIES).toContain('better-sqlite3');
62+
});
63+
64+
it('does NOT put the allowlist in package.json (current pnpm ignores it)', () => {
65+
const t = TEMPLATES.app;
66+
// Mirror init.ts's package.json construction.
67+
const pkgJson: Record<string, unknown> = {
68+
name: 'my-app',
69+
version: '0.1.0',
70+
private: true,
71+
type: 'module',
72+
scripts: t.scripts,
73+
dependencies: t.dependencies,
74+
devDependencies: t.devDependencies,
75+
};
76+
expect(pkgJson.pnpm).toBeUndefined();
77+
});
78+
79+
it('renders a pnpm-workspace.yaml that allowlists better-sqlite3', () => {
80+
const yaml = renderPnpmWorkspaceYaml();
81+
expect(yaml).toMatch(/^onlyBuiltDependencies:/m);
82+
expect(yaml).toMatch(/^ {2}- better-sqlite3$/m);
83+
// No `packages:` key — this is a settings file, not a workspace declaration.
84+
expect(yaml).not.toMatch(/^packages:/m);
85+
});
86+
});
87+
5488
describe('sanitizeNamespace', () => {
5589
const NS_RE = /^[a-z][a-z0-9_]{1,19}$/;
5690

0 commit comments

Comments
 (0)