Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, {
description: string;
dependencies: Record<string, string>;
Expand Down Expand Up @@ -88,7 +118,10 @@ export const TEMPLATES: Record<string, {
},
scripts: {
dev: 'objectstack dev',
start: 'objectstack serve',
// `serve` is production mode and boots from the compiled artifact
// (dist/objectstack.json). Compile first so `pnpm start` works straight
// after `pnpm install` without a separate build step.
start: 'objectstack compile && objectstack serve',
build: 'objectstack compile',
validate: 'objectstack validate',
typecheck: 'tsc --noEmit',
Expand Down Expand Up @@ -404,6 +437,19 @@ export default class Init extends Command {
printInfo('package.json already exists, skipping');
}

// 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');
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
const configContent = template.configContent(projectName, namespace);
fs.writeFileSync(path.join(targetDir, 'objectstack.config.ts'), configContent);
Expand Down
36 changes: 35 additions & 1 deletion packages/cli/test/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import fs from 'fs';
import os from 'os';
import path from 'path';
import { fileURLToPath } from 'url';
import { TEMPLATES, getCliVersion, detectPackageManager, sanitizeNamespace } from '../src/commands/init';
import { TEMPLATES, getCliVersion, detectPackageManager, sanitizeNamespace, SCAFFOLD_BUILT_DEPENDENCIES, renderPnpmWorkspaceYaml } from '../src/commands/init';

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

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<string, unknown> = {
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}$/;

Expand Down