From 9970b60f691245d8e8aa5e0fcecb62cc508d8a92 Mon Sep 17 00:00:00 2001 From: Tamishk Upadhyay Date: Mon, 6 Jul 2026 01:28:41 +0530 Subject: [PATCH] feat: use PGlite by default for generate so postgres isn't required Fixes #2. `pgstrap generate` (and therefore the scaffolded `bun run db:generate` script) now defaults to running migrations against an in-memory PGlite database, so generating zapatos types and structure dumps no longer requires a running Postgres server. Use `--no-pglite` to connect to a real Postgres via DATABASE_URL as before. Also: - await the generate handler and pass argv through yargs' factory invocation (hideBin) so the CLI runs under both the CJS build and direct ESM/bun execution - wrap the PGlite generate path in try/finally so DATABASE_URL is restored and the gateway server + PGlite instance are closed even when generation fails - load pgstrap.config.js via pathToFileURL so dynamic import works on Windows - add an end-to-end test that runs `pgstrap generate` with no flags in a scaffolded project with no reachable Postgres Co-Authored-By: Claude Fable 5 --- README.md | 5 +- src/cli.ts | 15 ++++-- src/generate.ts | 44 +++++++++-------- src/get-project-context.ts | 5 +- tests/cli-generate-default.test.ts | 78 ++++++++++++++++++++++++++++++ 5 files changed, 122 insertions(+), 25 deletions(-) create mode 100644 tests/cli-generate-default.test.ts diff --git a/README.md b/README.md index 20e3127..c5561b6 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,8 @@ npm install pgstrap --save-dev npm run db:migrate ``` -6. Generate types and structure: +6. Generate types and structure (no running Postgres server required — this + uses an in-memory PGlite database by default): ```bash npm run db:generate ``` @@ -55,7 +56,7 @@ npm install pgstrap --save-dev - `npm run db:migrate` - Run pending migrations - `npm run db:reset` - Drop and recreate the database, then run all migrations -- `npm run db:generate` - Generate types and structure dumps. Use `pgstrap generate --pglite` to run migrations against an in-memory PGlite instance. +- `npm run db:generate` - Generate types and structure dumps. By default this runs your migrations against an in-memory [PGlite](https://pglite.dev/) instance, so no running Postgres server is required. Use `pgstrap generate --no-pglite` to connect to a real Postgres database (via `DATABASE_URL`) instead. - `npm run db:create-migration` - Create a new migration file ### Configuration diff --git a/src/cli.ts b/src/cli.ts index 9a9bdec..114a6d6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,8 +1,9 @@ #!/usr/bin/env node import yargs from "yargs" +import { hideBin } from "yargs/helpers" import { migrate, reset, generate, createMigration, initPgstrap } from "./" import { getProjectContext } from "./get-project-context" -;(yargs as any) +;(yargs as any)(hideBin(process.argv)) .command("init", "initialize pgstrap", {}, async () => { await initPgstrap({ cwd: process.cwd(), @@ -35,10 +36,18 @@ import { getProjectContext } from "./get-project-context" "generate", "generate types and sql documentation from database", (yargs) => { - yargs.option("pglite", { type: "boolean", default: false }) + yargs.option("pglite", { + type: "boolean", + default: true, + describe: + "Run migrations against an in-memory PGlite database, so no running Postgres server is required. Use --no-pglite to connect to a real Postgres database instead.", + }) }, async (argv) => { - generate({ ...(await getProjectContext()), pglite: !!argv.pglite }) + await generate({ + ...(await getProjectContext()), + pglite: argv.pglite !== false, + }) }, ) .parse() diff --git a/src/generate.ts b/src/generate.ts index f337094..12c0218 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -12,7 +12,10 @@ export const generate = async ({ schemas, defaultDatabase, dbDir, - pglite = false, + // Defaults to an in-memory PGlite database so that a running Postgres + // server isn't required to generate types. Pass `pglite: false` to + // connect to a real Postgres database instead. + pglite = true, migrationsDir, }: Pick & { pglite?: boolean @@ -67,25 +70,28 @@ export const generate = async ({ const prevDbUrl = process.env.DATABASE_URL process.env.DATABASE_URL = connectionString - await zg.generate({ - db: { - connectionString, - }, - schemas: Object.fromEntries( - schemas.map((s) => [s, { include: "*", exclude: [] }]), - ), - outDir: dbDir, - }) - - await dumpTree({ - targetDir: path.join(dbDir, "structure"), - defaultDatabase: "postgres", - schemas, - }) + try { + await zg.generate({ + db: { + connectionString, + }, + schemas: Object.fromEntries( + schemas.map((s) => [s, { include: "*", exclude: [] }]), + ), + outDir: dbDir, + }) - server.close() - if (prevDbUrl === undefined) delete process.env.DATABASE_URL - else process.env.DATABASE_URL = prevDbUrl + await dumpTree({ + targetDir: path.join(dbDir, "structure"), + defaultDatabase: "postgres", + schemas, + }) + } finally { + server.close() + await db.close().catch(() => {}) + if (prevDbUrl === undefined) delete process.env.DATABASE_URL + else process.env.DATABASE_URL = prevDbUrl + } return } diff --git a/src/get-project-context.ts b/src/get-project-context.ts index 8d01d80..35680a8 100644 --- a/src/get-project-context.ts +++ b/src/get-project-context.ts @@ -1,5 +1,6 @@ import fs from "fs" import path from "path" +import { pathToFileURL } from "url" import { PgstrapConfig } from "./define-config" export interface Context extends PgstrapConfig { @@ -13,7 +14,9 @@ export const getProjectContext = async (): Promise => { ) } - const config = await import(path.join(process.cwd(), "pgstrap.config.js")) + const config = await import( + pathToFileURL(path.join(process.cwd(), "pgstrap.config.js")).href + ) return { cwd: process.cwd(), diff --git a/tests/cli-generate-default.test.ts b/tests/cli-generate-default.test.ts new file mode 100644 index 0000000..c005e55 --- /dev/null +++ b/tests/cli-generate-default.test.ts @@ -0,0 +1,78 @@ +import { test, expect } from "bun:test" +import { spawnSync } from "child_process" +import fs from "fs" +import os from "os" +import path from "path" + +const migrationFile = ` +exports.up = async (pgm) => { + pgm.createTable('widgets', { id: 'id', name: { type: 'text' } }) +} +exports.down = async (pgm) => { + pgm.dropTable('widgets') +} +` + +// Regression test for https://github.com/seveibar/pgstrap/issues/2 +// Running "pgstrap generate" (i.e. the scaffolded "db:generate" script) +// with no flags must succeed without a running Postgres server, because +// it defaults to an in-memory PGlite database. +test( + "pgstrap generate defaults to pglite and works without postgres", + async () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "pgstrap-cli-generate-")) + try { + fs.writeFileSync( + path.join(tmp, "pgstrap.config.js"), + `module.exports = ${JSON.stringify({ + defaultDatabase: "some_db_that_does_not_exist", + schemas: ["public"], + })}`, + ) + const migrationsDir = path.join(tmp, "src", "db", "migrations") + fs.mkdirSync(migrationsDir, { recursive: true }) + fs.writeFileSync( + path.join(migrationsDir, "001_create_widgets.js"), + migrationFile, + ) + + const env = { ...process.env } + // Make sure no real postgres connection info leaks into the test + delete env.DATABASE_URL + delete env.PGHOST + delete env.PGPORT + delete env.PGUSER + delete env.PGPASSWORD + delete env.PGDATABASE + // Point at a port where nothing is listening so a fallback to a real + // postgres connection would fail loudly + env.PGPORT = "54329" + + const result = spawnSync( + process.execPath, + [path.join(__dirname, "..", "src", "cli.ts"), "generate"], + { cwd: tmp, env, encoding: "utf8" }, + ) + + expect(result.status).toBe(0) + + const zapatosFile = path.join(tmp, "src", "db", "zapatos", "schema.d.ts") + const structureFile = path.join( + tmp, + "src", + "db", + "structure", + "public", + "tables", + "widgets", + "table.sql", + ) + expect(fs.existsSync(zapatosFile)).toBe(true) + expect(fs.existsSync(structureFile)).toBe(true) + expect(fs.readFileSync(zapatosFile, "utf8")).toContain("widgets") + } finally { + fs.rmSync(tmp, { recursive: true, force: true }) + } + }, + 60_000, +)