@rebasepro/cli 0.18.1 → 0.19.1-canary.g5a92fa9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Give compiled output the relative-import extensions Node's ESM loader needs.
3
+ *
4
+ * ## Why a project needs this at all
5
+ *
6
+ * TypeScript does not rewrite module specifiers, deliberately and by long-stated
7
+ * policy: what you write is what it emits. Node's ESM loader, equally
8
+ * deliberately, resolves no extensions — `./authors` is not a module, only
9
+ * `./authors.js` is. Between those two positions sits every project that writes
10
+ * extensionless relative imports, which is most of them, and which
11
+ * `moduleResolution: "bundler"` — what this scaffold ships — is designed for.
12
+ *
13
+ * The usual escape is to write the *output* extension in the *source*:
14
+ * `import x from "./authors.js"` in a file called `authors.ts`. It works, and it
15
+ * is a tax on every import in the project to satisfy a step the toolchain could
16
+ * take itself. This is that step.
17
+ *
18
+ * ## Why it is a command
19
+ *
20
+ * `rebase build` has always done this to its own bundle — the specifiers in
21
+ * `dist-bundle` are complete because {@link normalizeEsmSpecifiers} completes
22
+ * them. Nothing did it for the plain `tsc` output that `rebase eject` runs
23
+ * (`node dist/backend/src/index.js`) or that a self-hoster runs after
24
+ * `pnpm build`. So the templates wrote `.js` in source, which is the tax, and
25
+ * removing it silently broke eject at `pnpm start` while `check:eject` — which
26
+ * typechecks with `noEmit` — stayed green.
27
+ *
28
+ * One implementation, on both paths, callable from a package.json script.
29
+ *
30
+ * Mechanical and verifiable: only relative specifiers are touched, and only when
31
+ * the target resolves to a file that exists. Idempotent, so a build that runs it
32
+ * twice is a build that runs it once.
33
+ */
34
+ export declare const NORMALIZE_IMPORTS_FLAGS: {
35
+ /** Report what would change without writing anything. */
36
+ readonly "--check": BooleanConstructor;
37
+ };
38
+ export declare function normalizeImportsCommand(rawArgs: string[]): Promise<void>;
@@ -28,6 +28,7 @@ var daemon_exports = /* @__PURE__ */ __exportAll({
28
28
  findRunningDaemon: () => findRunningDaemon,
29
29
  isDaemonAlive: () => isDaemonAlive,
30
30
  managedUrl: () => managedUrl,
31
+ pgliteLogTail: () => pgliteLogTail,
31
32
  resetManagedDatabase: () => resetManagedDatabase,
32
33
  resolveSpawn: () => resolveSpawn,
33
34
  stopManagedDatabase: () => stopManagedDatabase
@@ -60,6 +61,29 @@ var MANAGED_DATABASE = "postgres";
60
61
  * Putting it in the URL fixes every consumer at once — Atlas, `pg`, and anything
61
62
  * a driver shells out to later — rather than teaching each one about PGlite.
62
63
  */
64
+ /**
65
+ * The end of `pglite.log`, for an error message that has to carry its own reason.
66
+ *
67
+ * The daemon writes its output to a file because it is detached — it has to
68
+ * outlive the command that started it. That is right, and it means every
69
+ * startup failure used to be reported as a path: "see pglite.log for the
70
+ * reason". On a laptop that is a fair trade. In CI it is not a trade at all,
71
+ * because the runner discards the workspace: the log named in the message no
72
+ * longer exists by the time anybody reads the message, so a failed run says
73
+ * only that something failed.
74
+ *
75
+ * So the tail comes with the error. Bounded, because a log that has been
76
+ * appended to across many runs is not something to paste in full.
77
+ */
78
+ function pgliteLogTail(projectRoot, lines = 12) {
79
+ try {
80
+ const contents = fs.readFileSync(path.join(devDbDir(projectRoot), "pglite.log"), "utf8").trimEnd();
81
+ if (!contents) return " (pglite.log is empty — the daemon wrote nothing before exiting)";
82
+ return contents.split("\n").slice(-lines).map((line) => ` ${line}`).join("\n");
83
+ } catch {
84
+ return " (pglite.log could not be read)";
85
+ }
86
+ }
63
87
  function managedUrl(port) {
64
88
  return `postgresql://${MANAGED_USER}@127.0.0.1:${port}/${MANAGED_DATABASE}?sslmode=disable`;
65
89
  }
@@ -243,10 +267,10 @@ async function ensureManagedDatabase(projectRoot, options = {}) {
243
267
  announced = true;
244
268
  options.onProgress?.("Starting the development database…");
245
269
  }
246
- if (child.exitCode !== null && child.exitCode !== 0) throw new Error(`The development database failed to start (exit ${child.exitCode}).\n See ${path.join(devDbDir(projectRoot), "pglite.log")} for the reason.`);
270
+ if (child.exitCode !== null && child.exitCode !== 0) throw new Error(`The development database failed to start (exit ${child.exitCode}).\n ${path.join(devDbDir(projectRoot), "pglite.log")} ends:\n` + pgliteLogTail(projectRoot));
247
271
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
248
272
  }
249
- throw new Error(`The development database did not start within ${Math.round(START_TIMEOUT_MS / 1e3)}s.\n See ${path.join(devDbDir(projectRoot), "pglite.log")} for the reason.\n To use your own Postgres instead, set DATABASE_URL or pass --database-url.`);
273
+ throw new Error(`The development database did not start within ${Math.round(START_TIMEOUT_MS / 1e3)}s.\n ${path.join(devDbDir(projectRoot), "pglite.log")} ends:\n` + pgliteLogTail(projectRoot) + "\n To use your own Postgres instead, set DATABASE_URL or pass --database-url.");
250
274
  } finally {
251
275
  releaseStartLock(projectRoot);
252
276
  }
@@ -328,4 +352,4 @@ function ensureGitignore(projectRoot) {
328
352
  //#endregion
329
353
  export { managedUrl as i, ensureManagedDatabase as n, findRunningDaemon as r, daemon_exports as t };
330
354
 
331
- //# sourceMappingURL=daemon-YDZK9NZ1.js.map
355
+ //# sourceMappingURL=daemon-DR9Tk0La.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-DR9Tk0La.js","names":[],"sources":["../src/dev-db/daemon.ts"],"sourcesContent":["/**\n * Starting, finding and stopping the managed database, from the caller's side.\n *\n * Every command that needs Postgres calls {@link ensureManagedDatabase} and\n * gets a connection string back. Whether that started a process or found one\n * already running is not the caller's business, which is the point: `rebase\n * db push`, `rebase dev` and `rebase studio` in three terminals must all reach\n * the same database without coordinating, because two processes opening one\n * PGlite data directory would corrupt it.\n *\n * The hard part is not starting the daemon; it is deciding whether the one the\n * state file describes is still there. A pid can be recycled after a reboot and\n * a port can be taken by a stranger, so believing either on its own would let\n * Rebase send a migration somewhere unintended. {@link isDaemonAlive} asks the\n * daemon to identify itself instead.\n */\n\nimport { randomBytes } from \"crypto\";\nimport { type ChildProcess, spawn } from \"child_process\";\nimport fs from \"fs\";\nimport net from \"net\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nimport {\n acquireStartLock,\n clearState,\n type DaemonState,\n dataDir,\n devDbDir,\n findFreePort,\n pidRunning,\n readState,\n releaseStartLock,\n stateFile\n} from \"./state\";\n\n/** How long to wait for a first boot. PGlite runs initdb on an empty data dir. */\nconst START_TIMEOUT_MS = 60_000;\n/** Poll interval while waiting for the daemon to publish its state file. */\nconst POLL_INTERVAL_MS = 150;\n/** How long the identity handshake may take before we call it dead. */\nconst IDENTITY_TIMEOUT_MS = 1_500;\n\nexport interface ManagedDatabase {\n /** Connection string for this project's managed database. */\n url: string;\n /** Where the data lives, for diagnostics and `--reset`. */\n dataDir: string;\n port: number;\n pid: number;\n /** True when this call started the daemon rather than finding it. */\n started: boolean;\n /**\n * Connection strings for the additional databases the caller asked for,\n * by declared key — `database(\"analytics\")` → `analytics`. Each is its\n * own PGlite instance behind the same daemon.\n */\n additional: Record<string, string>;\n}\n\n/**\n * PGlite's built-in superuser and database.\n *\n * Fixed by PGlite rather than chosen here, and asserted by the socket spike:\n * `session_user` comes back as `postgres`.\n */\nconst MANAGED_USER = \"postgres\";\nconst MANAGED_DATABASE = \"postgres\";\n\n/**\n * The DSN for the managed development database.\n *\n * **`sslmode=disable` is not decoration.** PGlite's socket server speaks no TLS,\n * and libpq-family clients try SSL first — so Atlas, which `rebase db push`\n * shells out to, failed the whole push with\n * `pq: SSL is not enabled on the server`. Its remedy box then told the reader to\n * append `sslmode=disable` to `DATABASE_URL`, which on this path is not set at\n * all: `rebase init` leaves it commented out precisely so the managed database\n * is used. The advice was correct and unfollowable.\n *\n * Putting it in the URL fixes every consumer at once — Atlas, `pg`, and anything\n * a driver shells out to later — rather than teaching each one about PGlite.\n */\n\n/**\n * The end of `pglite.log`, for an error message that has to carry its own reason.\n *\n * The daemon writes its output to a file because it is detached — it has to\n * outlive the command that started it. That is right, and it means every\n * startup failure used to be reported as a path: \"see pglite.log for the\n * reason\". On a laptop that is a fair trade. In CI it is not a trade at all,\n * because the runner discards the workspace: the log named in the message no\n * longer exists by the time anybody reads the message, so a failed run says\n * only that something failed.\n *\n * So the tail comes with the error. Bounded, because a log that has been\n * appended to across many runs is not something to paste in full.\n */\nexport function pgliteLogTail(projectRoot: string, lines = 12): string {\n try {\n const contents = fs.readFileSync(path.join(devDbDir(projectRoot), \"pglite.log\"), \"utf8\").trimEnd();\n if (!contents) return \" (pglite.log is empty — the daemon wrote nothing before exiting)\";\n return contents.split(\"\\n\").slice(-lines).map(line => ` ${line}`).join(\"\\n\");\n } catch {\n return \" (pglite.log could not be read)\";\n }\n}\n\nexport function managedUrl(port: number): string {\n return `postgresql://${MANAGED_USER}@127.0.0.1:${port}/${MANAGED_DATABASE}?sslmode=disable`;\n}\n\n/**\n * Ask the process behind a state record to prove it is the one we wrote down.\n *\n * A pid check alone answers \"is *a* process running\", and a port check alone\n * answers \"is *something* listening\" — after a reboot both say yes about\n * strangers. The daemon answers with the token from its own state file, so a\n * match is the only evidence accepted.\n */\nexport function isDaemonAlive(state: DaemonState): Promise<boolean> {\n if (!pidRunning(state.pid)) return Promise.resolve(false);\n\n return new Promise((resolve) => {\n const socket = new net.Socket();\n let answer = \"\";\n const settle = (alive: boolean) => {\n socket.removeAllListeners();\n socket.destroy();\n resolve(alive);\n };\n socket.setTimeout(IDENTITY_TIMEOUT_MS);\n socket.once(\"timeout\", () => settle(false));\n socket.once(\"error\", () => settle(false));\n socket.on(\"data\", (chunk) => {\n answer += chunk.toString(\"utf8\");\n if (answer.includes(\"\\n\")) settle(answer.trim() === `rebase-dev-db ${state.token}`);\n });\n socket.once(\"close\", () => settle(answer.trim() === `rebase-dev-db ${state.token}`));\n socket.connect(state.identityPort, \"127.0.0.1\");\n });\n}\n\n/** The running daemon for this project, or `null`. Never starts anything. */\nexport async function findRunningDaemon(projectRoot: string): Promise<DaemonState | null> {\n const state = readState(projectRoot);\n if (!state) return null;\n if (await isDaemonAlive(state)) return state;\n\n // The record describes something that is not there. Clearing it is the\n // whole recovery: the next ensure call starts a fresh daemon, and leaving\n // it would make every subsequent check pay the identity timeout.\n clearState(projectRoot);\n\n return null;\n}\n\n/**\n * Resolve the CLI entry point, so the daemon can be spawned as ourselves.\n *\n * A hidden subcommand rather than a second build entry: under `tsx` this file\n * is `src/dev-db/daemon.ts` and in a published CLI it is bundled into\n * `dist/index.js`, and in both cases the executable to re-invoke is the one\n * already running.\n */\nfunction resolveCliEntry(): string {\n // `process.argv[1]` is `bin/rebase.js` for a real invocation, which is\n // exactly what should be re-run. Under a test runner it is the runner, so\n // fall back to this module's own directory.\n const argvEntry = process.argv[1];\n if (argvEntry && fs.existsSync(argvEntry)) return argvEntry;\n\n return fileURLToPath(new URL(\"../index.js\", import.meta.url));\n}\n\n/**\n * How to re-invoke ourselves, which differs between a published CLI and this\n * repository.\n *\n * A published `rebase` is `node bin/rebase.js`, and re-running that is trivial.\n * Inside the monorepo the entry is TypeScript, which plain `node` cannot load —\n * so the daemon has to be started through the same loader that is running now.\n * Getting this wrong fails as a spawn that exits instantly with a syntax error,\n * which is why the caller reads `pglite.log` on failure.\n */\nexport function resolveSpawn(entry: string): { execPath: string; prefixArgs: string[] } {\n if (!/\\.[cm]?ts$/.test(entry)) return { execPath: process.execPath, prefixArgs: [] };\n\n // Node can load TypeScript when tsx is registered as an import hook. tsx is\n // already a dependency of this package, because `rebase dev` runs the\n // backend through it.\n return { execPath: process.execPath, prefixArgs: [\"--import\", \"tsx\"] };\n}\n\nexport interface EnsureOptions {\n /** Silence the \"starting…\" progress line. */\n quiet?: boolean;\n /** Milliseconds of inactivity before the daemon exits. 0 disables. */\n idleTimeoutMs?: number;\n /** Where progress goes. Injected for tests. */\n onProgress?: (message: string) => void;\n /** Override how the daemon process is launched. For tests. */\n spawn?: { execPath: string; prefixArgs: string[] };\n /**\n * Override the CLI entry to re-invoke. For tests.\n *\n * Necessary because under a test runner `process.argv[1]` is the runner\n * itself, which exists and is therefore accepted by {@link resolveCliEntry}\n * — spawning vitest with `__dev-db-daemon` rather than the CLI.\n */\n entry?: string;\n /**\n * Keys of the additional databases the project declares, beyond the\n * default. Each is served by the daemon on request, so a project that\n * adds `database(\"analytics\")` while the daemon is running gets it\n * without a restart.\n */\n additionalKeys?: readonly string[];\n}\n\n/**\n * Ask the running daemon for one additional database, and get its port.\n *\n * The identity socket doubles as the command channel: the daemon answers\n * with its token first, which is how the caller knows it is talking to the\n * daemon the state file describes and not to a stranger on a recycled port.\n */\nfunction ensureAdditionalDatabase(state: DaemonState, key: string): Promise<number> {\n return new Promise((resolve, reject) => {\n const socket = new net.Socket();\n let answer = \"\";\n let identified = false;\n const fail = (message: string) => {\n socket.removeAllListeners();\n socket.destroy();\n reject(new Error(message));\n };\n socket.setTimeout(START_TIMEOUT_MS);\n socket.once(\"timeout\", () => fail(`The development database did not answer for \"${key}\" within ${Math.round(START_TIMEOUT_MS / 1000)}s.`));\n socket.once(\"error\", (err) => fail(`Could not reach the development database for \"${key}\": ${err.message}`));\n socket.on(\"data\", (chunk) => {\n answer += chunk.toString(\"utf8\");\n let newline = answer.indexOf(\"\\n\");\n while (newline !== -1) {\n const line = answer.slice(0, newline).trim();\n answer = answer.slice(newline + 1);\n if (!identified) {\n if (line !== `rebase-dev-db ${state.token}`) return fail(\"The process on the development database's port is not the daemon.\");\n identified = true;\n socket.write(`ensure ${key}\\n`);\n } else if (line.startsWith(\"ok \")) {\n const port = Number(line.slice(3));\n socket.removeAllListeners();\n socket.destroy();\n if (!Number.isInteger(port) || port <= 0) return reject(new Error(`The development database answered with an invalid port for \"${key}\": ${line}`));\n return resolve(port);\n } else {\n return fail(`The development database could not serve \"${key}\": ${line.replace(/^error\\s*/, \"\")}`);\n }\n newline = answer.indexOf(\"\\n\");\n }\n });\n socket.connect(state.identityPort, \"127.0.0.1\");\n });\n}\n\n/** Every additional database the caller asked for, as connection strings. */\nasync function adoptWithAdditional(state: DaemonState, started: boolean, keys: readonly string[]): Promise<ManagedDatabase> {\n const additional: Record<string, string> = {};\n for (const key of keys) {\n const known = state.databases?.[key];\n const port = known ? known.port : await ensureAdditionalDatabase(state, key);\n additional[key] = managedUrl(port);\n }\n return { ...adopt(state, started), additional };\n}\n\n/**\n * The project's managed database, started if it is not already running.\n *\n * Safe to call concurrently from several commands: the loser of the race finds\n * the winner's state file during its poll and adopts it rather than starting a\n * second daemon.\n */\nexport async function ensureManagedDatabase(\n projectRoot: string,\n options: EnsureOptions = {}\n): Promise<ManagedDatabase> {\n const keys = options.additionalKeys ?? [];\n const existing = await findRunningDaemon(projectRoot);\n if (existing) return adoptWithAdditional(existing, false, keys);\n\n fs.mkdirSync(devDbDir(projectRoot), { recursive: true });\n ensureGitignore(projectRoot);\n\n // Exactly one process may spawn a daemon. Without this, `rebase dev` and\n // `rebase db push` started in the same second both see no state file, both\n // spawn, and two processes open one PGlite data directory — the corruption\n // the single-daemon design exists to prevent.\n if (!acquireStartLock(projectRoot, START_TIMEOUT_MS)) {\n const adopted = await waitForDaemon(projectRoot, START_TIMEOUT_MS);\n if (adopted) return adoptWithAdditional(adopted, false, keys);\n\n // The holder gave up or died without publishing. Falling through to\n // start one ourselves is better than failing: the lock is stale by now,\n // so the next acquire will break it.\n if (!acquireStartLock(projectRoot, 0)) {\n throw new Error(\n \"Another process is starting the development database and did not finish.\\n\" +\n ` Remove ${path.join(devDbDir(projectRoot), \"starting.lock\")} if nothing is running.`\n );\n }\n }\n\n const port = await findFreePort();\n const token = randomBytes(16).toString(\"hex\");\n const entry = options.entry ?? resolveCliEntry();\n\n const spawnPlan = options.spawn ?? resolveSpawn(entry);\n const args = [\n ...spawnPlan.prefixArgs,\n entry,\n \"__dev-db-daemon\",\n \"--project\", projectRoot,\n \"--port\", String(port),\n \"--token\", token\n ];\n if (options.idleTimeoutMs !== undefined) {\n args.push(\"--idle-timeout\", String(options.idleTimeoutMs));\n }\n\n const log = fs.openSync(path.join(devDbDir(projectRoot), \"pglite.log\"), \"a\");\n const child: ChildProcess = spawn(spawnPlan.execPath, args, {\n // Detached with no stdin and its output on a file: the daemon must\n // survive the command that started it, and must not hold the terminal\n // open when that command exits.\n detached: true,\n stdio: [\"ignore\", log, log]\n });\n child.unref();\n\n try {\n const deadline = Date.now() + START_TIMEOUT_MS;\n let announced = false;\n while (Date.now() < deadline) {\n const state = readState(projectRoot);\n if (state && (await isDaemonAlive(state))) return adoptWithAdditional(state, true, keys);\n\n if (!announced && !options.quiet) {\n announced = true;\n options.onProgress?.(\"Starting the development database…\");\n }\n\n // A daemon that died on startup will never publish a state file, and\n // waiting the full minute for that is a bad way to learn it. The log\n // is the only place the reason exists.\n if (child.exitCode !== null && child.exitCode !== 0) {\n throw new Error(\n `The development database failed to start (exit ${child.exitCode}).\\n` +\n ` ${path.join(devDbDir(projectRoot), \"pglite.log\")} ends:\\n` +\n pgliteLogTail(projectRoot)\n );\n }\n\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n }\n\n throw new Error(\n `The development database did not start within ${Math.round(START_TIMEOUT_MS / 1000)}s.\\n` +\n ` ${path.join(devDbDir(projectRoot), \"pglite.log\")} ends:\\n` +\n pgliteLogTail(projectRoot) + \"\\n\" +\n \" To use your own Postgres instead, set DATABASE_URL or pass --database-url.\"\n );\n } finally {\n // Released whether we succeeded, timed out or threw. A lock left behind\n // by a crash is broken by age, but only after a full timeout — which a\n // developer should never have to wait out.\n releaseStartLock(projectRoot);\n }\n}\n\n/** Shape a live state record as the caller's result. */\nfunction adopt(state: DaemonState, started: boolean): ManagedDatabase {\n return {\n url: managedUrl(state.port),\n dataDir: state.dataDir,\n port: state.port,\n pid: state.pid,\n started,\n additional: {}\n };\n}\n\n/** Poll until another process publishes a live daemon, or give up. */\nasync function waitForDaemon(projectRoot: string, timeoutMs: number): Promise<DaemonState | null> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n const state = readState(projectRoot);\n if (state && (await isDaemonAlive(state))) return state;\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n }\n\n return null;\n}\n\n/** Stop the daemon. Returns false when there was nothing running. */\nexport async function stopManagedDatabase(projectRoot: string): Promise<boolean> {\n const state = await findRunningDaemon(projectRoot);\n if (!state) return false;\n\n try {\n process.kill(state.pid, \"SIGTERM\");\n } catch {\n clearState(projectRoot);\n\n return false;\n }\n\n // Wait for it to clear its own state file, which is how it says it closed\n // the data directory cleanly. A hard kill here would risk the WAL.\n const deadline = Date.now() + 10_000;\n while (Date.now() < deadline) {\n if (!fs.existsSync(stateFile(projectRoot))) return true;\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n\n clearState(projectRoot);\n\n return true;\n}\n\n/**\n * Stop the daemon and delete the data directory.\n *\n * Destructive and deliberately not clever: it removes the whole directory\n * rather than dropping schemas, because \"give me an empty database\" is the only\n * thing anyone means by it.\n */\nexport async function resetManagedDatabase(projectRoot: string): Promise<void> {\n await stopManagedDatabase(projectRoot);\n fs.rmSync(dataDir(projectRoot), { recursive: true, force: true });\n // Every additional database too: `pgdata__<key>` beside `pgdata`. \"Give\n // me an empty database\" means all of them — a reset that emptied the\n // default and kept the analytics rows would be a surprise on first read.\n const dir = devDbDir(projectRoot);\n if (fs.existsSync(dir)) {\n for (const entry of fs.readdirSync(dir)) {\n if (entry.startsWith(\"pgdata__\")) fs.rmSync(path.join(dir, entry), { recursive: true, force: true });\n }\n }\n clearState(projectRoot);\n}\n\n/**\n * Keep the generated directory out of git.\n *\n * `.rebase/` holds a Postgres data directory and a log. A project that commits\n * it will push hundreds of megabytes and a database that only makes sense on\n * one machine, so the ignore file is written next to the data rather than\n * relying on the project's root `.gitignore` having been updated.\n */\nfunction ensureGitignore(projectRoot: string): void {\n const target = path.join(devDbDir(projectRoot), \".gitignore\");\n if (fs.existsSync(target)) return;\n fs.writeFileSync(\n target,\n \"# Generated by `rebase dev` — a local Postgres data directory and its log.\\n\" +\n \"# Machine-specific and large; never commit it.\\n*\\n\",\n \"utf8\"\n );\n}\n"],"mappings":"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"}
@@ -45,6 +45,21 @@ export interface ManagedDatabase {
45
45
  * Putting it in the URL fixes every consumer at once — Atlas, `pg`, and anything
46
46
  * a driver shells out to later — rather than teaching each one about PGlite.
47
47
  */
48
+ /**
49
+ * The end of `pglite.log`, for an error message that has to carry its own reason.
50
+ *
51
+ * The daemon writes its output to a file because it is detached — it has to
52
+ * outlive the command that started it. That is right, and it means every
53
+ * startup failure used to be reported as a path: "see pglite.log for the
54
+ * reason". On a laptop that is a fair trade. In CI it is not a trade at all,
55
+ * because the runner discards the workspace: the log named in the message no
56
+ * longer exists by the time anybody reads the message, so a failed run says
57
+ * only that something failed.
58
+ *
59
+ * So the tail comes with the error. Bounded, because a log that has been
60
+ * appended to across many runs is not something to paste in full.
61
+ */
62
+ export declare function pgliteLogTail(projectRoot: string, lines?: number): string;
48
63
  export declare function managedUrl(port: number): string;
49
64
  /**
50
65
  * Ask the process behind a state record to prove it is the one we wrote down.
package/dist/index.es.js CHANGED
@@ -3,7 +3,7 @@ import { _ as validateTsxInstallation, a as findBackendDir, c as findProjectRoot
3
3
  import { i as probeTcp, r as ensureDevDatabase, t as composeDatabaseUrl } from "./dev-preflight-CNLl4rdo.js";
4
4
  import { r as readActiveBranch, t as branchUrl } from "./branch-pointer-dBiClJ0p.js";
5
5
  import { t as MANAGED_LIMITATIONS } from "./constraints-DKGbNfUW.js";
6
- import { i as managedUrl, n as ensureManagedDatabase, r as findRunningDaemon } from "./daemon-YDZK9NZ1.js";
6
+ import { i as managedUrl, n as ensureManagedDatabase, r as findRunningDaemon } from "./daemon-DR9Tk0La.js";
7
7
  import { n as resolveDevDatabase, t as describeDevDatabase } from "./resolve-Y56osuQH.js";
8
8
  import chalk from "chalk";
9
9
  import arg from "arg";
@@ -3205,15 +3205,27 @@ async function schemaCommand(subcommand, rawArgs) {
3205
3205
  if (envFile) env.DOTENV_CONFIG_PATH = envFile;
3206
3206
  env[DEV_DATABASE_KIND_ENV] = devDatabaseKind(projectRoot) ?? "";
3207
3207
  try {
3208
- if (pluginCli.endsWith(".ts")) {
3209
- const tsxBin = resolveTsx(projectRoot);
3210
- if (!tsxBin) exitDependenciesNotInstalled(projectRoot);
3211
- await execa(tsxBin, [pluginCli, ...argsFromCommand(rawArgs, "schema")], {
3212
- cwd: backendDir,
3213
- stdio: "inherit",
3214
- env
3215
- });
3216
- } else await execa("node", [pluginCli, ...argsFromCommand(rawArgs, "schema")], {
3208
+ /**
3209
+ * tsx, whether the driver CLI is source or built.
3210
+ *
3211
+ * This chose the interpreter from the file extension — tsx for a `.ts`
3212
+ * entry, `node` for a built one — which was right only for as long as
3213
+ * the driver shipped `src/`. Once it shipped `dist/cli.js`, the built
3214
+ * CLI ran under plain `node`, and some of its subcommands load the
3215
+ * PROJECT's collections in process. `schema stale` does: on the stock
3216
+ * scaffold, whose collections import each other, Node could not resolve
3217
+ * `./authors` onto `authors.ts` and the command reported "⏭ Not
3218
+ * checked" — answering "is the generated schema stale?" with nothing,
3219
+ * and exiting 0.
3220
+ *
3221
+ * The same rule lives in `execDriverCli` (commands/db.ts). Two copies of
3222
+ * one decision is how they came to disagree; this one is kept because
3223
+ * `schema` deliberately does NOT resolve a database, and `execDriverCli`
3224
+ * is reached through a path that does.
3225
+ */
3226
+ const tsxBin = resolveTsx(projectRoot);
3227
+ if (!tsxBin && pluginCli.endsWith(".ts")) exitDependenciesNotInstalled(projectRoot);
3228
+ await execa(tsxBin ?? "node", [pluginCli, ...argsFromCommand(rawArgs, "schema")], {
3217
3229
  cwd: backendDir,
3218
3230
  stdio: "inherit",
3219
3231
  env
@@ -3580,9 +3592,24 @@ async function resolveDriverCli() {
3580
3592
  async function execDriverCli(resolved, childArgs, options = {}) {
3581
3593
  const { projectRoot, backendDir, pluginCli, env } = resolved;
3582
3594
  const stdio = options.quiet ? "pipe" : "inherit";
3583
- if (pluginCli.endsWith(".ts")) {
3584
- const tsxBin = resolveTsx(projectRoot);
3585
- if (!tsxBin) throw new Error(dependenciesNotInstalled(projectRoot));
3595
+ /**
3596
+ * tsx, whether the driver CLI is source or built.
3597
+ *
3598
+ * It used to be tsx only for a `.ts` entry, which happened to be right for
3599
+ * as long as the driver shipped `src/`. Once it shipped `dist/cli.js`
3600
+ * instead, this ran the built CLI under plain `node` — and some of its
3601
+ * subcommands load the PROJECT's collections in process. `schema stale`
3602
+ * does, so on every scaffold whose collections import each other (the stock
3603
+ * one does) it failed to resolve them and reported "⏭ Not checked": a
3604
+ * command that answers "is the generated schema stale?" with nothing at all,
3605
+ * exiting 0, on the default project.
3606
+ *
3607
+ * Node cannot load a `.ts` file, and cannot resolve `./authors` or
3608
+ * `./authors.js` onto `authors.ts`. tsx does both. It is a devDependency of
3609
+ * every scaffold for exactly this reason.
3610
+ */
3611
+ const tsxBin = resolveTsx(projectRoot);
3612
+ if (tsxBin) {
3586
3613
  await execa(tsxBin, [pluginCli, ...childArgs], {
3587
3614
  cwd: backendDir,
3588
3615
  stdio,
@@ -3590,6 +3617,7 @@ async function execDriverCli(resolved, childArgs, options = {}) {
3590
3617
  });
3591
3618
  return;
3592
3619
  }
3620
+ if (pluginCli.endsWith(".ts")) throw new Error(dependenciesNotInstalled(projectRoot));
3593
3621
  await execa("node", [pluginCli, ...childArgs], {
3594
3622
  cwd: backendDir,
3595
3623
  stdio,
@@ -3749,7 +3777,7 @@ async function printDatabaseUrl(projectRoot, rawArgs) {
3749
3777
  * removing the directory cannot leave a half-dropped schema behind.
3750
3778
  */
3751
3779
  async function manageLocalDatabase(subcommand, projectRoot, rawArgs) {
3752
- const { resetManagedDatabase, stopManagedDatabase, findRunningDaemon } = await import("./daemon-YDZK9NZ1.js").then((n) => n.t);
3780
+ const { resetManagedDatabase, stopManagedDatabase, findRunningDaemon } = await import("./daemon-DR9Tk0La.js").then((n) => n.t);
3753
3781
  const { dataDir } = await import("./state-C59Elrnt.js").then((n) => n.d);
3754
3782
  const nothingHere = () => {
3755
3783
  const kind = devDatabaseKind(projectRoot);
@@ -10838,6 +10866,98 @@ async function statusCommand$1(rawArgs) {
10838
10866
  console.log("");
10839
10867
  }
10840
10868
  //#endregion
10869
+ //#region src/commands/normalize-imports.ts
10870
+ /**
10871
+ * Give compiled output the relative-import extensions Node's ESM loader needs.
10872
+ *
10873
+ * ## Why a project needs this at all
10874
+ *
10875
+ * TypeScript does not rewrite module specifiers, deliberately and by long-stated
10876
+ * policy: what you write is what it emits. Node's ESM loader, equally
10877
+ * deliberately, resolves no extensions — `./authors` is not a module, only
10878
+ * `./authors.js` is. Between those two positions sits every project that writes
10879
+ * extensionless relative imports, which is most of them, and which
10880
+ * `moduleResolution: "bundler"` — what this scaffold ships — is designed for.
10881
+ *
10882
+ * The usual escape is to write the *output* extension in the *source*:
10883
+ * `import x from "./authors.js"` in a file called `authors.ts`. It works, and it
10884
+ * is a tax on every import in the project to satisfy a step the toolchain could
10885
+ * take itself. This is that step.
10886
+ *
10887
+ * ## Why it is a command
10888
+ *
10889
+ * `rebase build` has always done this to its own bundle — the specifiers in
10890
+ * `dist-bundle` are complete because {@link normalizeEsmSpecifiers} completes
10891
+ * them. Nothing did it for the plain `tsc` output that `rebase eject` runs
10892
+ * (`node dist/backend/src/index.js`) or that a self-hoster runs after
10893
+ * `pnpm build`. So the templates wrote `.js` in source, which is the tax, and
10894
+ * removing it silently broke eject at `pnpm start` while `check:eject` — which
10895
+ * typechecks with `noEmit` — stayed green.
10896
+ *
10897
+ * One implementation, on both paths, callable from a package.json script.
10898
+ *
10899
+ * Mechanical and verifiable: only relative specifiers are touched, and only when
10900
+ * the target resolves to a file that exists. Idempotent, so a build that runs it
10901
+ * twice is a build that runs it once.
10902
+ */
10903
+ var NORMALIZE_IMPORTS_FLAGS = {
10904
+ /** Report what would change without writing anything. */
10905
+ "--check": Boolean };
10906
+ var HELP = `
10907
+ ${chalk.bold("rebase normalize-imports")} — complete the relative imports in compiled output
10908
+
10909
+ ${chalk.green.bold("Usage")}
10910
+ rebase normalize-imports ${chalk.blue("<dir>")} [--check]
10911
+
10912
+ ${chalk.green.bold("What it does")}
10913
+ TypeScript emits your module specifiers verbatim, and Node's ESM loader does
10914
+ not guess extensions — so \`./authors\` compiles fine and fails at run time.
10915
+ This rewrites each relative specifier in the emitted JavaScript to the file it
10916
+ actually resolves to, so your source can stay extensionless.
10917
+
10918
+ Only relative specifiers, only when the target exists on disk, and idempotent.
10919
+
10920
+ ${chalk.green.bold("Arguments")}
10921
+ ${chalk.blue("<dir>")} The directory tsc emitted into, e.g. ${chalk.cyan("dist")}
10922
+
10923
+ ${chalk.green.bold("Options")}
10924
+ ${chalk.blue("--check")} Report what would change and exit 1 if anything would
10925
+ ${chalk.gray("(for CI — the build itself should just run the rewrite)")}
10926
+
10927
+ ${chalk.green.bold("Where it runs")}
10928
+ The scaffold's ${chalk.cyan("config")} and ${chalk.cyan("backend")} build scripts, after ${chalk.cyan("tsc")}.
10929
+ ${chalk.cyan("rebase build")} already does the same to the bundle it writes.
10930
+ `;
10931
+ async function normalizeImportsCommand(rawArgs) {
10932
+ const { flags, positionals, help } = parseCommandArgs({
10933
+ spec: NORMALIZE_IMPORTS_FLAGS,
10934
+ rawArgs,
10935
+ commandWords: 1,
10936
+ command: "normalize-imports",
10937
+ maxPositionals: 1
10938
+ });
10939
+ if (help) {
10940
+ console.log(HELP);
10941
+ return;
10942
+ }
10943
+ const target = positionals[0] ?? "dist";
10944
+ const outDir = path.resolve(process.cwd(), target);
10945
+ if (!fs.existsSync(outDir)) throw new Error(`${target} does not exist — run your build first. This step rewrites what the compiler emitted; it does not compile.`);
10946
+ const { rewritten, unresolved } = normalizeEsmSpecifiers(outDir);
10947
+ if (unresolved.length > 0) {
10948
+ console.log(chalk.yellow(` ⚠ ${unresolved.length} relative import(s) resolve to no file:`));
10949
+ for (const specifier of unresolved.slice(0, 10)) console.log(chalk.dim(` ${specifier}`));
10950
+ if (unresolved.length > 10) console.log(chalk.dim(` …and ${unresolved.length - 10} more`));
10951
+ console.log(chalk.dim(" These will throw ERR_MODULE_NOT_FOUND at run time."));
10952
+ }
10953
+ if (flags["--check"]) {
10954
+ if (rewritten > 0 || unresolved.length > 0) throw new Error(`${rewritten} import(s) are incomplete for Node ESM. Run \`rebase normalize-imports ${target}\` as part of the build.`);
10955
+ console.log(chalk.green(`✓ every relative import in ${target} is complete`));
10956
+ return;
10957
+ }
10958
+ console.log(rewritten > 0 ? chalk.dim(` completed ${rewritten} relative import(s) in ${target}/ for Node ESM`) : chalk.dim(` ${target}/ — every relative import was already complete`));
10959
+ }
10960
+ //#endregion
10841
10961
  //#region src/commands/skills.ts
10842
10962
  var require = createRequire(import.meta.url);
10843
10963
  /**
@@ -19014,6 +19134,9 @@ async function entry(args) {
19014
19134
  case "dev":
19015
19135
  await devCommand(args);
19016
19136
  break;
19137
+ case "normalize-imports":
19138
+ await normalizeImportsCommand(args);
19139
+ break;
19017
19140
  case "build":
19018
19141
  await buildCommand(args);
19019
19142
  break;
@@ -19065,6 +19188,7 @@ ${chalk.green.bold("Commands")}
19065
19188
  ${chalk.blue.bold("init")} Create a new Rebase project
19066
19189
  ${chalk.blue.bold("dev")} Start the development server
19067
19190
  ${chalk.blue.bold("build")} Build the apps declared in rebase.json into a bundle
19191
+ ${chalk.blue.bold("normalize-imports")} Complete compiled output's relative imports for Node ESM
19068
19192
  ${chalk.blue.bold("start")} Start the backend server ${chalk.gray("(production)")}
19069
19193
  ${chalk.blue.bold("apps list")} Show the apps this repository declares
19070
19194