@rebasepro/cli 0.25.0 → 0.25.1
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.
- package/dist/{agent-setup-B5AKHOgt.js → agent-setup-BYb2Tbdz.js} +2 -2
- package/dist/{agent-setup-B5AKHOgt.js.map → agent-setup-BYb2Tbdz.js.map} +1 -1
- package/dist/{api-keys-Tm7zvAPe.js → api-keys-CSMbsmAn.js} +3 -4
- package/dist/{api-keys-Tm7zvAPe.js.map → api-keys-CSMbsmAn.js.map} +1 -1
- package/dist/{apps-BsU_ILbP.js → apps-iBXNZhVN.js} +6 -7
- package/dist/{apps-BsU_ILbP.js.map → apps-iBXNZhVN.js.map} +1 -1
- package/dist/{atlas-dev-database-OWtqnG7W.js → atlas-dev-database-BusbHP6n.js} +4 -4
- package/dist/{atlas-dev-database-OWtqnG7W.js.map → atlas-dev-database-BusbHP6n.js.map} +1 -1
- package/dist/{auth-Vl2hFKXP.js → auth-DRsTWPky.js} +6 -7
- package/dist/{auth-Vl2hFKXP.js.map → auth-DRsTWPky.js.map} +1 -1
- package/dist/{branch-pointer-9DnwkFJJ.js → branch-pointer-Bn_FjNld.js} +1 -1
- package/dist/{build-BBgT8EJZ.js → build-et7Rccxi.js} +6 -7
- package/dist/{build-BBgT8EJZ.js.map → build-et7Rccxi.js.map} +1 -1
- package/dist/{bundle-DtCCCr4w.js → bundle-DlqkDYSe.js} +3 -3
- package/dist/{bundle-DtCCCr4w.js.map → bundle-DlqkDYSe.js.map} +1 -1
- package/dist/{cloud-BpH0_bmV.js → cloud-B-ZIjHrA.js} +10 -11
- package/dist/{cloud-BpH0_bmV.js.map → cloud-B-ZIjHrA.js.map} +1 -1
- package/dist/commands/dev.d.ts +12 -0
- package/dist/commands/generate_sdk.d.ts +4 -2
- package/dist/commands/init.d.ts +20 -0
- package/dist/commands/start.d.ts +10 -0
- package/dist/commands/upgrade.d.ts +12 -0
- package/dist/{consent-ClRHmcxf.js → consent-BaB-eQKG.js} +2 -2
- package/dist/{consent-ClRHmcxf.js.map → consent-BaB-eQKG.js.map} +1 -1
- package/dist/{console-CEGm0XNc.js → console-BlD9kjFW.js} +4 -4
- package/dist/{console-CEGm0XNc.js.map → console-BlD9kjFW.js.map} +1 -1
- package/dist/{context-CyflUrKg.js → context-DHiiiJ5C.js} +3 -4
- package/dist/context-DHiiiJ5C.js.map +1 -0
- package/dist/{daemon-CknkDjd6.js → daemon-DDMRbz4T.js} +160 -5
- package/dist/daemon-DDMRbz4T.js.map +1 -0
- package/dist/{daemon-entry-CbLRrgzy.js → daemon-entry-zOC1revO.js} +6 -3
- package/dist/daemon-entry-zOC1revO.js.map +1 -0
- package/dist/{db-CRNpA5mH.js → db-9P6MnmdC.js} +90 -50
- package/dist/db-9P6MnmdC.js.map +1 -0
- package/dist/{derive-CsoFGAnK.js → derive-MTdClnkc.js} +2 -2
- package/dist/{derive-CsoFGAnK.js.map → derive-MTdClnkc.js.map} +1 -1
- package/dist/{dev-C0pbMmhS.js → dev-BDN_OKXA.js} +183 -23
- package/dist/dev-BDN_OKXA.js.map +1 -0
- package/dist/dev-db/daemon.d.ts +41 -0
- package/dist/dev-db/pull.d.ts +33 -5
- package/dist/dev-db/state.d.ts +9 -0
- package/dist/{dev-preflight-unHpiYO1.js → dev-preflight-FT9GbJVj.js} +73 -27
- package/dist/dev-preflight-FT9GbJVj.js.map +1 -0
- package/dist/{dev-server-B1UYifZJ.js → dev-server-CIZwAfZa.js} +14 -3
- package/dist/dev-server-CIZwAfZa.js.map +1 -0
- package/dist/{doctor-CATRdWm6.js → doctor-D8YXz7df.js} +6 -6
- package/dist/doctor-D8YXz7df.js.map +1 -0
- package/dist/{eject-DuasTr-F.js → eject-izG2KjMo.js} +3 -4
- package/dist/{eject-DuasTr-F.js.map → eject-izG2KjMo.js.map} +1 -1
- package/dist/{fold-static-hDHtDfTq.js → fold-static-CybSay_W.js} +3 -3
- package/dist/{fold-static-hDHtDfTq.js.map → fold-static-CybSay_W.js.map} +1 -1
- package/dist/{generate_sdk-Cvg5jjJF.js → generate_sdk-B4R65oUV.js} +21 -11
- package/dist/generate_sdk-B4R65oUV.js.map +1 -0
- package/dist/index.es.js +36 -37
- package/dist/index.es.js.map +1 -1
- package/dist/{init-BWAwHPtz.js → init-DYaeppEN.js} +50 -62
- package/dist/init-DYaeppEN.js.map +1 -0
- package/dist/invoked-command-CG_rIa_o.js +55 -0
- package/dist/invoked-command-CG_rIa_o.js.map +1 -0
- package/dist/last-dev-database-B_75fxZC.js +2 -0
- package/dist/{manifest-DRp0UVTj.js → manifest-nvPdLlJY.js} +2 -2
- package/dist/{manifest-DRp0UVTj.js.map → manifest-nvPdLlJY.js.map} +1 -1
- package/dist/{normalize-imports-BoBVh5tU.js → normalize-imports-BYw8ad87.js} +3 -3
- package/dist/{normalize-imports-BoBVh5tU.js.map → normalize-imports-BYw8ad87.js.map} +1 -1
- package/dist/{prepare-DdnU70z8.js → prepare-1rVNd2PF.js} +5 -4
- package/dist/{prepare-DdnU70z8.js.map → prepare-1rVNd2PF.js.map} +1 -1
- package/dist/project-g4K6kV1t.js +701 -0
- package/dist/project-g4K6kV1t.js.map +1 -0
- package/dist/{pull-C3PXduzl.js → pull-DDMpsZ5r.js} +131 -24
- package/dist/pull-DDMpsZ5r.js.map +1 -0
- package/dist/refusal-ReeeGP8a.js +26 -0
- package/dist/refusal-ReeeGP8a.js.map +1 -0
- package/dist/resolve-BQUAC3Wi.js +2 -0
- package/dist/{resources-BdNRDEAy.js → resources-CovP8AZ-.js} +4 -5
- package/dist/{resources-BdNRDEAy.js.map → resources-CovP8AZ-.js.map} +1 -1
- package/dist/{schema-B0-p6T1O.js → schema-nuxvvc50.js} +5 -6
- package/dist/{schema-B0-p6T1O.js.map → schema-nuxvvc50.js.map} +1 -1
- package/dist/{skills-DfnTeyRz.js → skills-DeUIsZcT.js} +3 -4
- package/dist/{skills-DfnTeyRz.js.map → skills-DeUIsZcT.js.map} +1 -1
- package/dist/{start-BmEZkS0n.js → start-RzZqDroW.js} +20 -7
- package/dist/start-RzZqDroW.js.map +1 -0
- package/dist/{state-CCB-FAQ9.js → state-CYdBvnwA.js} +3 -2
- package/dist/state-CYdBvnwA.js.map +1 -0
- package/dist/{status-BJ26yZOo.js → status-D9Zm88NZ.js} +8 -9
- package/dist/{status-BJ26yZOo.js.map → status-D9Zm88NZ.js.map} +1 -1
- package/dist/{storage-DKPUBTWG.js → storage-BB3umRfw.js} +3 -4
- package/dist/{storage-DKPUBTWG.js.map → storage-BB3umRfw.js.map} +1 -1
- package/dist/{telemetry-EMFRVRQi.js → telemetry-HfU5uAh1.js} +2 -2
- package/dist/{telemetry-EMFRVRQi.js.map → telemetry-HfU5uAh1.js.map} +1 -1
- package/dist/{telemetry-m9czTNGR.js → telemetry-TseNLk6X.js} +4 -4
- package/dist/{telemetry-m9czTNGR.js.map → telemetry-TseNLk6X.js.map} +1 -1
- package/dist/{upgrade-DjihWuZL.js → upgrade-apiPKKE_.js} +26 -4
- package/dist/{upgrade-DjihWuZL.js.map → upgrade-apiPKKE_.js.map} +1 -1
- package/dist/utils/args.d.ts +32 -3
- package/dist/utils/boot-retry.d.ts +41 -0
- package/dist/utils/dev-preflight.d.ts +34 -1
- package/dist/utils/dev-server.d.ts +11 -0
- package/dist/utils/project.d.ts +5 -1
- package/dist/utils/unknown-command.d.ts +4 -0
- package/package.json +7 -7
- package/templates/resource-graphs/blank.json +25 -0
- package/templates/resource-graphs/blog.json +28 -0
- package/templates/resource-graphs/ecommerce.json +28 -0
- package/templates/resource-graphs/headless.json +22 -0
- package/templates/template/.env.example +5 -4
- package/templates/template/frontend/src/App.tsx +10 -9
- package/dist/args-BX0YD14a.js +0 -266
- package/dist/args-BX0YD14a.js.map +0 -1
- package/dist/context-CyflUrKg.js.map +0 -1
- package/dist/daemon-CknkDjd6.js.map +0 -1
- package/dist/daemon-entry-CbLRrgzy.js.map +0 -1
- package/dist/db-CRNpA5mH.js.map +0 -1
- package/dist/dev-C0pbMmhS.js.map +0 -1
- package/dist/dev-preflight-unHpiYO1.js.map +0 -1
- package/dist/dev-server-B1UYifZJ.js.map +0 -1
- package/dist/doctor-CATRdWm6.js.map +0 -1
- package/dist/generate_sdk-Cvg5jjJF.js.map +0 -1
- package/dist/init-BWAwHPtz.js.map +0 -1
- package/dist/last-dev-database-c_dKz1S3.js +0 -2
- package/dist/project-Wp2zoyeN.js +0 -349
- package/dist/project-Wp2zoyeN.js.map +0 -1
- package/dist/pull-C3PXduzl.js.map +0 -1
- package/dist/resolve-CNQjsatH.js +0 -2
- package/dist/start-BmEZkS0n.js.map +0 -1
- package/dist/state-CCB-FAQ9.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"derive-CsoFGAnK.js","names":[],"sources":["../src/resources/derive.ts"],"sourcesContent":["/**\n * Deriving the resource graph from a project's config, and recording it.\n *\n * ## Why the graph is derived rather than authored\n *\n * A resource is declared once, in code. But a host has to know what a project\n * needs *before* it runs anything — that is how a console can say \"this project\n * wants a `media` bucket and has none\" on a first deploy, and it is the reason\n * storage topology used to be hand-written into `rebase.json` in the first\n * place. A `custom` runtime makes it sharper still: it emits no bundle\n * manifest, so without a committed artifact there is nothing to read at all.\n *\n * So the graph is derived by evaluating the config and written to\n * `rebase.resources.json`, which is committed. Same arrangement as\n * `schema.generated.ts`: authored in one place, generated into another, and a\n * gate fails if the two disagree. The file is an *output*, never an input —\n * which is what removes the old silent merge, because there is no longer a\n * second declaration for anything to be merged with.\n *\n * ## Why evaluation rather than static analysis\n *\n * Encore parses the source and builds the graph from the AST. Rebase already\n * evaluates config to load collections, and evaluation is exact where a parser\n * is approximate — a resource declared in a loop, or behind a helper, is\n * invisible to a parser and obvious to an interpreter. The cost is running user\n * code at build time, which this project already accepts everywhere else.\n */\nimport fs from \"fs\";\nimport path from \"path\";\nimport { createRequire } from \"module\";\nimport { pathToFileURL } from \"url\";\nimport {\n DEFAULT_RESOURCE_KEY,\n buildResourceGraph,\n declareFunction,\n declaredQueueConsumers,\n declaredResources,\n declaredSubscriptions,\n findEnvSuffixCollision,\n isResourceHandle,\n resetDeclaredQueueConsumers,\n resetDeclaredResources,\n resetDeclaredSubscriptions,\n resolveResourceRefs,\n resourceId,\n resourceKeyOf,\n type CollectionConfig,\n type RebaseBackendAppConfig,\n type ResourceDeclaration,\n type ResourceGraph\n} from \"@rebasepro/types\";\nimport { analyseFunctionsDirectory } from \"../function-portability\";\nimport { resolveBackendPaths } from \"../manifest\";\nimport { cliVersion } from \"../utils/version\";\n\n/** The committed, generated record of what a project needs. */\nexport const RESOURCE_GRAPH_FILENAME = \"rebase.resources.json\";\n\n/** A problem found while deriving, reported with the rest rather than thrown one at a time. */\nexport interface ResourceIssue {\n path: string;\n message: string;\n}\n\n/**\n * Which `@rebasepro/*` package a module error is about, when it is about one.\n *\n * Both directions of a version skew arrive as an error about the *user's* file:\n * `resources.ts The requested module '@rebasepro/types' does not provide an\n * export named 'queue'`. Nothing in that says which copy of `@rebasepro/types`\n * is installed, what version this CLI is, or that the two disagree — so the\n * reader goes looking for a typo in code they just copied out of the docs.\n *\n * Two shapes, because the two directions produce different errors: a named\n * export the installed copy is too old for, and a package the installed tree\n * does not have at all.\n */\nconst NAMED_EXPORT_MISSING = /does not provide an export named ['\"]([^'\"]+)['\"]/;\nconst SPECIFIER = /(@rebasepro\\/[a-z0-9-]+)/;\n\n/** The version of a package as resolved from the project, or null. */\nfunction installedVersion(specifier: string, from: string): string | null {\n try {\n const require = createRequire(path.join(from, \"noop.js\"));\n const manifest = require.resolve(`${specifier}/package.json`);\n const version: unknown = JSON.parse(fs.readFileSync(manifest, \"utf-8\")).version;\n return typeof version === \"string\" ? version : null;\n } catch {\n return null;\n }\n}\n\n/**\n * The message an issue carries, with the skew named when there is one.\n *\n * `main` runs 497 commits ahead of npm `latest` and the docs site publishes from\n * `main`, so the shape this exists for is routine: a scaffold pinned to the\n * published version, config code written against today's documentation, and an\n * error about the config file. The fix is one command and the error named none\n * of it — the same failure as `404 No PUT route on collection 'projects'`, one\n * layer down.\n *\n * Says nothing extra when the two versions agree, or when either is unreadable:\n * a sentence naming one version twice is worse than no sentence.\n */\nexport function describeIssue(err: unknown, projectRoot: string, ownVersion = cliVersion()): string {\n const message = err instanceof Error ? err.message : String(err);\n const code = (err as { code?: unknown } | null)?.code;\n const missingExport = NAMED_EXPORT_MISSING.exec(message);\n const isSkewShaped = missingExport !== null || code === \"ERR_MODULE_NOT_FOUND\";\n if (!isSkewShaped) return message;\n\n const specifier = SPECIFIER.exec(message)?.[1];\n if (!specifier) return message;\n\n // One line, not two: every caller renders an issue as `path message` on a\n // single row, so a newline here would land the useful half unindented under\n // the file name.\n const lead = /[.!?]$/.test(message.trim()) ? message.trim() : `${message.trim()}.`;\n\n const installed = installedVersion(specifier, projectRoot);\n if (installed === null) {\n return `${lead} ${specifier} is not installed in this project; this CLI is ${ownVersion}. `\n + `Run pnpm add ${specifier}@${ownVersion}`;\n }\n if (ownVersion === \"unknown\" || installed === ownVersion) return message;\n\n // Which side to move is the question the reader actually has, and it has a\n // right answer: the CLI is what evaluates this file, so the packages follow\n // it — unless the CLI is the older one, in which case following it would\n // move the project backwards.\n const cliIsNewer = compareVersions(ownVersion, installed) > 0;\n return cliIsNewer\n ? `${lead} ${specifier} ${installed} is installed; this CLI is ${ownVersion}. `\n + `Run pnpm add ${specifier}@${ownVersion}`\n : `${lead} ${specifier} ${installed} is installed; this CLI is ${ownVersion}, which is older. `\n + `Update the CLI to ${installed}, or pin ${specifier} back to ${ownVersion}`;\n}\n\n/** -1 / 0 / 1 on the numeric parts, ignoring any prerelease tail. */\nfunction compareVersions(a: string, b: string): number {\n const parts = (v: string): number[] => v.split(\"-\")[0].split(\".\").map(n => Number(n) || 0);\n const [x, y] = [parts(a), parts(b)];\n for (let i = 0; i < 3; i++) {\n if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) < (y[i] ?? 0) ? -1 : 1;\n }\n return 0;\n}\n\n/** Files that declare resources, in the order they are loaded. */\nconst RESOURCE_ENTRY_NAMES = [\"resources.ts\", \"resources.js\", \"resources/index.ts\", \"resources/index.js\"];\n\n/** Collection files, which may also declare a resource beside the collection using it. */\nfunction collectionFiles(configDir: string): string[] {\n const dir = path.join(configDir, \"collections\");\n if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return [];\n return fs.readdirSync(dir)\n .filter(f => /\\.(ts|js|mts|mjs)$/.test(f) && !/\\.(test|spec|d)\\./.test(f))\n .sort()\n .map(f => path.join(dir, f));\n}\n\n/**\n * Make the project's own TypeScript loader active in this process.\n *\n * `import()` below relies on tsx's resolver, and nothing had registered it:\n * `rebase build` runs from `node_modules/.bin/rebase`, which is plain Node.\n * On a modern Node that *looks* like it works — native type stripping imports\n * `collections/index.ts` happily — right up to the first relative specifier\n * inside it. The scaffolded template writes `import posts from \"./posts.js\"`,\n * which is the correct thing for TypeScript to emit and which native stripping\n * does not remap back to `posts.ts`. So `rebase build` failed on every\n * scaffolded project with `Cannot find module …/posts.js`, and the failure\n * looked like a broken template rather than a missing loader.\n *\n * Resolved from the project, not from this package, exactly as `resolveTsx`\n * does for the schema-generation subprocess: tsx is the project's dependency\n * (the scaffold declares it), and taking it from anywhere else would mean\n * evaluating a project's config with a loader it did not choose.\n *\n * Best-effort by design. A project with no tsx keeps the previous behaviour —\n * which is correct for one whose config is plain JavaScript, or whose imports\n * carry no extension — rather than failing on a dependency it never needed.\n * Registered once per process; `register` is cheap but not free, and deriving\n * twice in one process is an ordinary thing for a watch mode to do.\n */\nlet tsxRegistered = false;\n\nasync function registerProjectTsx(configDir: string): Promise<void> {\n if (tsxRegistered) return;\n tsxRegistered = true;\n\n // Where tsx might be, in the order most likely to find it. The scaffold\n // declares tsx in the *backend* workspace, and pnpm's isolated layout puts\n // it in `backend/node_modules` rather than hoisting it — so resolving only\n // from the config directory finds nothing on exactly the project shape this\n // exists to serve.\n const projectRoot = path.dirname(configDir);\n const bases = [configDir, projectRoot, path.join(projectRoot, \"backend\")];\n\n for (const base of bases) {\n try {\n // Resolved against a file *inside* the project so Node walks that\n // project's `node_modules`, not this package's.\n const requireFromProject = createRequire(path.join(base, \"noop.js\"));\n const api = await import(pathToFileURL(requireFromProject.resolve(\"tsx/esm/api\")).href) as\n { register?: () => unknown; default?: { register?: () => unknown } };\n\n // tsx's own `register`, not `module.register`. `tsx/esm/api` is the\n // API module; the loader is `tsx/esm`. Handing the API module to\n // `module.register` registers something with no hooks and reports\n // success — a worse failure than throwing, because the import below\n // then resolves exactly as it did before and the bug looks unfixed.\n const register = api.register ?? api.default?.register;\n if (typeof register === \"function\") {\n register();\n return;\n }\n } catch {\n // Not here. Try the next base.\n }\n }\n\n // No tsx anywhere in the project. The import below still runs; it simply\n // resolves the way Node alone would, which is correct for a config written\n // in plain JavaScript or whose imports carry no extension.\n}\n\n/**\n * Import a module for its side effects.\n *\n * Plain `import()` so tsx's loader hooks resolve `.ts` and workspace\n * specifiers, exactly as the collection loader does.\n */\nasync function evaluate(filePath: string): Promise<Record<string, unknown>> {\n return await import(pathToFileURL(filePath).href) as Record<string, unknown>;\n}\n\nexport interface DeriveOptions {\n /** Absolute path to the project's config directory. */\n configDir: string;\n /**\n * Whether to evaluate collection files too. Resources are conventionally\n * declared in `resources.ts`, but nothing stops a bucket being declared\n * beside the collection that stores into it, and a graph that missed it\n * would under-report what the project needs.\n */\n includeCollections?: boolean;\n /**\n * Absolute path to the crons directory. Each file is evaluated through the\n * same loader the runtime uses, which declares the cron under the same id\n * the scheduler runs it as. Absent, or missing on disk: no crons.\n */\n cronsDir?: string;\n /**\n * Absolute path to the functions directory. Read statically — the\n * bundler's portability analysis — never evaluated: a function's module\n * scope belongs to a running backend, not a build.\n */\n functionsDir?: string;\n /** The project root, for the paths a graph records. Defaults to the config directory's parent. */\n projectRoot?: string;\n}\n\n/** The derive options for a backend app, from its manifest entry. */\nexport function deriveOptionsFor(projectRoot: string, app: RebaseBackendAppConfig): DeriveOptions {\n const paths = resolveBackendPaths(app, projectRoot);\n return {\n projectRoot,\n configDir: path.join(projectRoot, paths.config),\n cronsDir: path.join(projectRoot, paths.crons),\n functionsDir: path.join(projectRoot, paths.functions)\n };\n}\n\n/** Whether a module's default export looks like a collection. */\nfunction isCollectionLike(value: unknown): value is CollectionConfig {\n return typeof value === \"object\" && value !== null\n && typeof (value as { slug?: unknown }).slug === \"string\"\n && typeof (value as { properties?: unknown }).properties === \"object\";\n}\n\n/**\n * The resources a collection reaches: its database, and every bucket a\n * storage property stores into. Only resources the graph declares get an\n * edge — the implicit default database is not in the graph, so a collection\n * that names no `dataSource` records nothing.\n */\nfunction collectionEdges(collection: CollectionConfig, declared: Set<string>): Array<[string, string]> {\n const edges: Array<[string, string]> = [];\n const from = `collection:${collection.slug}`;\n const databaseId = resourceId(\"database\", collection.dataSource ?? DEFAULT_RESOURCE_KEY);\n if (declared.has(databaseId)) edges.push([databaseId, from]);\n\n const walk = (properties: Record<string, unknown> | undefined, prefix: string): void => {\n for (const [name, raw] of Object.entries(properties ?? {})) {\n const property = raw as {\n storage?: { storageSource?: unknown };\n of?: unknown;\n properties?: Record<string, unknown>;\n };\n if (!property || typeof property !== \"object\") continue;\n if (property.storage) {\n const key = property.storage.storageSource === undefined\n ? DEFAULT_RESOURCE_KEY\n : resourceKeyOf(property.storage.storageSource as string);\n const bucketId = resourceId(\"bucket\", key);\n if (declared.has(bucketId)) edges.push([bucketId, `property:${collection.slug}.${prefix}${name}`]);\n }\n if (property.properties) walk(property.properties, `${prefix}${name}.`);\n if (property.of && typeof property.of === \"object\") walk({ [name]: property.of }, `${prefix}`);\n }\n };\n walk(collection.properties as Record<string, unknown>, \"\");\n return edges;\n}\n\n/**\n * A cron file that would not import, said in terms somebody can act on.\n *\n * The graph is derived by evaluating each cron module, in a process with none\n * of the deployment's environment — so a cron that reaches a module validating\n * `DATABASE_URL` at import fails here and works perfectly at boot. The raw\n * message is a Zod error about a variable, which reads as \"the environment is\n * wrong\" when the environment is not the point.\n */\nfunction cronLoadIssue(problem: string): string {\n return `${summariseCronProblem(problem)}\\n` +\n \" `rebase resources` evaluates each cron file to read its schedule, and it is a build \" +\n \"step: no .env, no secrets. Move work that needs the deployment's environment inside the \" +\n \"handler — `const { x } = await import(\\\"…\\\")` — so the module scope imports nothing that \" +\n \"reads configuration.\";\n}\n\n/**\n * The cron loader's `<file> (threw: <message>)`, as one line.\n *\n * The message is whatever the module threw, and the common case is a schema\n * validator: `loadEnv()` at module scope with no `.env` present throws a\n * `ZodError`, whose `.message` is the JSON array of its issues. That arrived\n * here verbatim — ten lines of `{ \"expected\": \"string\", \"code\":\n * \"invalid_type\", … }` — with the sentence that says what to do underneath it,\n * so the reader met \"Invalid input: expected string, received undefined\" twice\n * before reaching the explanation.\n *\n * A validator's own sentences are the useful half of that object, so they are\n * kept and the serialisation is dropped.\n */\nfunction summariseCronProblem(problem: string): string {\n const match = problem.match(/^(.*) \\(threw: ([\\s\\S]*)\\)$/);\n if (!match) return problem.split(\"\\n\")[0];\n const [, file, thrown] = match;\n return `${file}: ${summariseThrownMessage(thrown)}`;\n}\n\n/** One sentence out of a thrown message, whatever shape it arrived in. */\nfunction summariseThrownMessage(message: string): string {\n const trimmed = message.trim();\n if (trimmed.startsWith(\"[\") || trimmed.startsWith(\"{\")) {\n try {\n const parsed: unknown = JSON.parse(trimmed);\n const issues = Array.isArray(parsed) ? parsed : [parsed];\n const sentences = [...new Set(\n issues\n .map(issue => (issue as { message?: unknown } | null)?.message)\n .filter((m): m is string => typeof m === \"string\" && m.length > 0)\n )];\n if (sentences.length > 0) return sentences.join(\"; \");\n } catch {\n // Not JSON after all — fall through to the first line.\n }\n }\n return trimmed.split(\"\\n\")[0];\n}\n\n/**\n * Evaluate a project's config and return the graph it declares.\n *\n * Clears any previously registered declarations first, so deriving twice in one\n * process — a watch mode, a test — describes the project rather than the union\n * of every project seen so far.\n */\nexport async function deriveResourceGraph(options: DeriveOptions): Promise<{ graph: ResourceGraph; issues: ResourceIssue[] }> {\n const { configDir, includeCollections = true, cronsDir, functionsDir } = options;\n const projectRoot = options.projectRoot ?? path.dirname(configDir);\n // Before anything is evaluated: the loader has to be in place for the very\n // first import, and the first import is a collection file.\n await registerProjectTsx(configDir);\n\n // All three registries, and the side lists are the easy ones to forget:\n // resetting only the resources leaves one project's handlers attached\n // while its topics are gone — every one of them then reads as orphaned\n // against the *next* project. Caught by deriving twice in one process,\n // which is what a watch mode does.\n resetDeclaredResources();\n resetDeclaredSubscriptions();\n resetDeclaredQueueConsumers();\n\n const issues: ResourceIssue[] = [];\n const usedBy = new Map<string, string[]>();\n // `recordUse`, not `use`: React 19 made `use` a hook name, and\n // `react-hooks/rules-of-hooks` refuses a call to one from a plain function\n // — a lint error on a file with no React in it.\n const recordUse = (resource: string, by: string): void => {\n const list = usedBy.get(resource) ?? [];\n if (!list.includes(by)) list.push(by);\n usedBy.set(resource, list);\n };\n\n // ── Declarations ─────────────────────────────────────────────────────\n // Which export name is which resource, so a function's `import { media }`\n // can be recorded as reaching `bucket:media` without evaluating the\n // function. The export name and the key are usually the same word and\n // nothing requires them to be.\n const handleExports = new Map<string, string>();\n for (const name of RESOURCE_ENTRY_NAMES) {\n const candidate = path.join(configDir, name);\n if (!fs.existsSync(candidate)) continue;\n try {\n const mod = await evaluate(candidate);\n for (const [exportName, value] of Object.entries(mod)) {\n if (isResourceHandle(value)) handleExports.set(exportName, resourceId(value.kind, value.key));\n }\n } catch (err) {\n issues.push({ path: path.relative(configDir, candidate), message: describeIssue(err, projectRoot) });\n }\n break;\n }\n\n // ── Collections ──────────────────────────────────────────────────────\n const collections: CollectionConfig[] = [];\n if (includeCollections) {\n for (const entry of collectionFiles(configDir)) {\n try {\n const mod = await evaluate(entry);\n // As the loader sees it at boot: a handle written where a key\n // belongs is its key here too.\n if (isCollectionLike(mod.default)) collections.push(resolveResourceRefs(mod.default));\n } catch (err) {\n // Named individually: \"could not derive the graph\" with no file in\n // it sends somebody reading every module in the directory.\n issues.push({\n path: path.relative(configDir, entry),\n message: describeIssue(err, projectRoot)\n });\n }\n }\n }\n\n // ── Crons ────────────────────────────────────────────────────────────\n // Through the runtime's own loader, so the id the graph records is the id\n // the scheduler runs — the filename — and a cron that would not load at\n // boot does not load here either.\n //\n // Which means the module is EVALUATED, in a build step that has none of the\n // deployment's environment. A cron whose module scope reaches something that\n // validates `DATABASE_URL` at import therefore derives on a laptop with a\n // `.env` and nowhere else. That is worth an explicit sentence rather than a\n // stack, so `cronLoadIssue` adds one.\n if (cronsDir && fs.existsSync(cronsDir)) {\n // The runtime's loader logs each job as it loads, to stdout, as JSON\n // — which is also where `rebase resources --json` writes the graph.\n // Its problems come back as a list, so the log adds nothing here.\n const previousLevel = process.env.LOG_LEVEL;\n process.env.LOG_LEVEL = \"error\";\n try {\n const { loadCrons, declareCrons } = await import(\"@rebasepro/server\");\n const reportProblem = (problem: string) => {\n issues.push({ path: path.relative(projectRoot, cronsDir), message: cronLoadIssue(problem) });\n };\n // This process's own `import()`, not the loader's `new Function`\n // one: tsx's hooks are registered here, and a test runner's module\n // sandbox answers the other with \"a dynamic import callback was\n // not specified\" — the same trap the split-roles e2e records.\n const crons = await loadCrons(cronsDir, { importModule: url => import(url), onSkip: reportProblem });\n // One at a time, so a cron the graph refuses is reported by name\n // and the others are still declared.\n for (const [id, definition] of Object.entries(crons)) {\n try {\n declareCrons({ [id]: definition });\n } catch (err) {\n reportProblem(`${id} (threw: ${err instanceof Error ? err.message : String(err)})`);\n }\n }\n } catch (err) {\n issues.push({\n path: path.relative(projectRoot, cronsDir),\n message: cronLoadIssue(err instanceof Error ? err.message : String(err))\n });\n } finally {\n if (previousLevel === undefined) delete process.env.LOG_LEVEL;\n else process.env.LOG_LEVEL = previousLevel;\n }\n }\n\n // ── Functions ────────────────────────────────────────────────────────\n if (functionsDir && fs.existsSync(functionsDir)) {\n for (const report of analyseFunctionsDirectory(functionsDir, projectRoot)) {\n const requires = [...new Set(\n report.issues\n .filter(issue => issue.kind === \"node-builtin\" || issue.kind === \"node-only-package\")\n .map(issue => issue.message)\n )];\n try {\n declareFunction(report.name, {\n file: report.file,\n portable: report.portable,\n ...(requires.length > 0 ? { requires } : {})\n });\n } catch (err) {\n issues.push({ path: report.file, message: describeIssue(err, projectRoot) });\n }\n for (const imported of report.resourceImports) {\n const resource = handleExports.get(imported);\n if (resource) recordUse(resource, `function:${report.name}`);\n }\n }\n }\n\n // ── Edges ────────────────────────────────────────────────────────────\n const declared = new Set(declaredResources().map(r => resourceId(r.kind, r.key)));\n for (const collection of collections) {\n for (const [resource, by] of collectionEdges(collection, declared)) recordUse(resource, by);\n }\n for (const sub of declaredSubscriptions()) {\n const id = resourceId(\"topic\", sub.topic);\n if (declared.has(id)) recordUse(id, `subscription:${sub.topic}.${sub.name}`);\n }\n for (const consumer of declaredQueueConsumers()) {\n const id = resourceId(\"queue\", consumer.queue);\n if (declared.has(id)) recordUse(id, `handler:${consumer.queue}`);\n }\n\n const graph = buildResourceGraph(usedBy);\n\n // Two resources of a kind whose env suffixes collide would silently read\n // each other's configuration, which is indistinguishable from a\n // misconfigured deployment.\n const byKind = new Map<string, string[]>();\n for (const r of graph.resources) {\n byKind.set(r.kind, [...(byKind.get(r.kind) ?? []), r.key]);\n }\n for (const [kind, keys] of byKind) {\n const collision = findEnvSuffixCollision(keys);\n if (collision) {\n issues.push({\n path: `${kind}.${collision.b}`,\n message:\n `\"${collision.a}\" and \"${collision.b}\" both bind from ${collision.suffix}, so one would ` +\n \"read the other's configuration. Rename one.\"\n });\n }\n }\n\n // A subscription on a topic nobody declared is a handler that will never\n // run, and nothing else would ever say so.\n const topics = new Set(graph.resources.filter(r => r.kind === \"topic\").map(r => r.key));\n for (const sub of declaredSubscriptions()) {\n if (!topics.has(sub.topic)) {\n issues.push({\n path: `topic.${sub.topic}`,\n message:\n `Subscription \"${sub.name}\" is declared on topic \"${sub.topic}\", which nothing declares. ` +\n \"The handler would never run.\"\n });\n }\n }\n\n // A collection routed to a database nothing declares boots into a\n // refusal, and a property stored in a bucket nothing declares answers 501\n // on its first upload. Both are cheaper to hear about here.\n const databases = new Set(graph.resources.filter(r => r.kind === \"database\").map(r => r.key));\n const buckets = new Set(graph.resources.filter(r => r.kind === \"bucket\").map(r => r.key));\n for (const collection of collections) {\n if (collection.dataSource && collection.dataSource !== DEFAULT_RESOURCE_KEY && !databases.has(collection.dataSource)) {\n issues.push({\n path: `collection.${collection.slug}`,\n message:\n `routes to dataSource \"${collection.dataSource}\", which nothing declares. ` +\n `Add database(\"${collection.dataSource}\") to resources.ts, or import the handle and use it here.`\n });\n }\n for (const [name, raw] of Object.entries(collection.properties ?? {})) {\n const key = (raw as { storage?: { storageSource?: unknown } })?.storage?.storageSource;\n if (typeof key === \"string\" && key !== DEFAULT_RESOURCE_KEY && buckets.size > 0 && !buckets.has(key)) {\n issues.push({\n path: `collection.${collection.slug}.${name}`,\n message: `stores into bucket \"${key}\", which nothing declares. Add bucket(\"${key}\") to resources.ts.`\n });\n }\n }\n }\n\n return { graph, issues };\n}\n\n/** The graph as it is written to disk: stable key order, trailing newline. */\nexport function serializeResourceGraph(graph: ResourceGraph): string {\n const resources = graph.resources.map(r => ({\n kind: r.kind,\n key: r.key,\n engine: r.engine,\n transport: r.transport,\n ...(r.label !== undefined ? { label: r.label } : {}),\n ...(Object.keys(r.options).length > 0 ? { options: r.options } : {}),\n ...(r.usedBy && r.usedBy.length > 0 ? { usedBy: r.usedBy } : {})\n }));\n return JSON.stringify({\n $generated: `Generated from config by \\`rebase resources\\`. Edit the declarations, not this file.`,\n version: graph.version,\n resources\n }, null, 4) + \"\\n\";\n}\n\n/** Read the committed graph, or null when a project has none yet. */\nexport function readResourceGraphFile(projectRoot: string): string | null {\n const file = path.join(projectRoot, RESOURCE_GRAPH_FILENAME);\n return fs.existsSync(file) ? fs.readFileSync(file, \"utf-8\") : null;\n}\n\n/**\n * Write the graph, and say whether the file changed.\n *\n * The boolean is what a `--check` mode reports on: a project whose committed\n * graph disagrees with its config has a host reading one thing and a runtime\n * doing another.\n */\nexport function writeResourceGraphFile(projectRoot: string, graph: ResourceGraph): { changed: boolean; file: string } {\n const file = path.join(projectRoot, RESOURCE_GRAPH_FILENAME);\n const next = serializeResourceGraph(graph);\n const previous = fs.existsSync(file) ? fs.readFileSync(file, \"utf-8\") : null;\n if (previous === next) return { changed: false, file };\n fs.writeFileSync(file, next);\n return { changed: true, file };\n}\n\n/** Parse a committed graph file, tolerating the `$generated` banner. */\nexport function parseResourceGraph(contents: string): ResourceGraph {\n const raw = JSON.parse(contents) as { version?: number; resources?: unknown };\n if (raw.version !== 1) {\n throw new Error(\n `Unsupported resource graph version ${String(raw.version)}. ` +\n \"This project was generated by a newer Rebase; upgrade rather than provisioning half of it.\"\n );\n }\n const entries = Array.isArray(raw.resources) ? raw.resources as Record<string, unknown>[] : [];\n // `options` is omitted from the file when empty; a reader of the graph\n // expects it present, so it is restored here.\n return { version: 1, resources: entries.map(r => ({ options: {}, ...r })) as ResourceGraph[\"resources\"] };\n}\n\n/**\n * Add the resources a project has without declaring them.\n *\n * A backend has a database whether or not anyone said so, and a project that\n * declares no buckets still gets one default storage source from the plain\n * unsuffixed variables. Both are load-bearing defaults and both are invisible\n * in the graph, so a status view built only from declarations would show an\n * empty screen to the majority of projects — the ones that most need to be told\n * which variable their one database reads.\n *\n * Declaring a *named* one leaves the default where it is, as the server does\n * (`resolveDataSources` and `resolveStorageSources` in `@rebasepro/server`):\n * `bucket(\"media\")` adds a source and never moves the files the default holds,\n * so both are listed. The default is displaced only by declaring it — or, for a\n * bucket, by marking exactly one server-side bucket `default: true`, which then\n * serves the uploads that name no `storageSource`. This used to drop the\n * default once a bucket was declared, and `rebase status` and `rebase\n * resources` stopped listing a source the server was still serving.\n */\nexport function withImplicitDefaults(graph: ResourceGraph): {\n declaration: ResourceDeclaration;\n implicit: boolean;\n}[] {\n const entries = graph.resources.map(declaration => ({ declaration, implicit: false }));\n const declaresDefault = (kind: string) => graph.resources.some(r => r.kind === kind && r.key === DEFAULT_RESOURCE_KEY);\n const markedDefault = graph.resources.filter(r =>\n r.kind === \"bucket\" && (r.transport ?? \"server\") === \"server\" && r.options.default === true);\n\n if (!declaresDefault(\"database\")) {\n entries.unshift({\n declaration: {\n kind: \"database\",\n key: DEFAULT_RESOURCE_KEY,\n engine: \"postgres\",\n transport: \"server\",\n options: {}\n },\n implicit: true\n });\n }\n if (!declaresDefault(\"bucket\") && markedDefault.length !== 1) {\n const implicitBucket = {\n declaration: {\n kind: \"bucket\",\n key: DEFAULT_RESOURCE_KEY,\n // `local` is what an unconfigured default source resolves to,\n // and naming it here keeps the row honest about what a project\n // with no S3 variables actually gets: a directory that a\n // container erases on restart, which production then drops.\n engine: \"local\",\n transport: \"server\" as const,\n options: {}\n },\n implicit: true\n };\n // First among the buckets, as the database default is first among the\n // databases; last when there are none.\n const firstBucket = entries.findIndex(entry => entry.declaration.kind === \"bucket\");\n if (firstBucket === -1) entries.push(implicitBucket);\n else entries.splice(firstBucket, 0, implicitBucket);\n }\n return entries;\n}\n\n/**\n * The resources a project *has*, which is what a person is asking about.\n *\n * One projection, because two of them disagreed. `rebase status` built its\n * rows from {@link withImplicitDefaults} and listed `buckets ✓ (default) local\n * · implicit`; `rebase resources` and `rebase resources --json` built theirs\n * from the raw graph and listed only the database and the function. Two\n * commands whose whole job is to answer \"what does this project need\", on the\n * same stock scaffold, giving different answers.\n *\n * The implicit entries are marked rather than hidden, because the distinction\n * is real and load-bearing: a declared resource is recorded in\n * `rebase.resources.json` for a host to provision, and an implicit one is a\n * default the runtime supplies whether or not anyone wrote it down. That file\n * still holds declarations only — it is a wire contract a host reads, and\n * putting defaults in it would ask for something to be provisioned that\n * nobody declared.\n */\nexport type ProjectedResource = ResourceDeclaration & { implicit?: true };\n\nexport function projectResourceGraph(graph: ResourceGraph): ProjectedResource[] {\n return withImplicitDefaults(graph).map(({ declaration, implicit }) =>\n (implicit ? { ...declaration, implicit: true as const } : declaration));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"derive-MTdClnkc.js","names":[],"sources":["../src/resources/derive.ts"],"sourcesContent":["/**\n * Deriving the resource graph from a project's config, and recording it.\n *\n * ## Why the graph is derived rather than authored\n *\n * A resource is declared once, in code. But a host has to know what a project\n * needs *before* it runs anything — that is how a console can say \"this project\n * wants a `media` bucket and has none\" on a first deploy, and it is the reason\n * storage topology used to be hand-written into `rebase.json` in the first\n * place. A `custom` runtime makes it sharper still: it emits no bundle\n * manifest, so without a committed artifact there is nothing to read at all.\n *\n * So the graph is derived by evaluating the config and written to\n * `rebase.resources.json`, which is committed. Same arrangement as\n * `schema.generated.ts`: authored in one place, generated into another, and a\n * gate fails if the two disagree. The file is an *output*, never an input —\n * which is what removes the old silent merge, because there is no longer a\n * second declaration for anything to be merged with.\n *\n * ## Why evaluation rather than static analysis\n *\n * Encore parses the source and builds the graph from the AST. Rebase already\n * evaluates config to load collections, and evaluation is exact where a parser\n * is approximate — a resource declared in a loop, or behind a helper, is\n * invisible to a parser and obvious to an interpreter. The cost is running user\n * code at build time, which this project already accepts everywhere else.\n */\nimport fs from \"fs\";\nimport path from \"path\";\nimport { createRequire } from \"module\";\nimport { pathToFileURL } from \"url\";\nimport {\n DEFAULT_RESOURCE_KEY,\n buildResourceGraph,\n declareFunction,\n declaredQueueConsumers,\n declaredResources,\n declaredSubscriptions,\n findEnvSuffixCollision,\n isResourceHandle,\n resetDeclaredQueueConsumers,\n resetDeclaredResources,\n resetDeclaredSubscriptions,\n resolveResourceRefs,\n resourceId,\n resourceKeyOf,\n type CollectionConfig,\n type RebaseBackendAppConfig,\n type ResourceDeclaration,\n type ResourceGraph\n} from \"@rebasepro/types\";\nimport { analyseFunctionsDirectory } from \"../function-portability\";\nimport { resolveBackendPaths } from \"../manifest\";\nimport { cliVersion } from \"../utils/version\";\n\n/** The committed, generated record of what a project needs. */\nexport const RESOURCE_GRAPH_FILENAME = \"rebase.resources.json\";\n\n/** A problem found while deriving, reported with the rest rather than thrown one at a time. */\nexport interface ResourceIssue {\n path: string;\n message: string;\n}\n\n/**\n * Which `@rebasepro/*` package a module error is about, when it is about one.\n *\n * Both directions of a version skew arrive as an error about the *user's* file:\n * `resources.ts The requested module '@rebasepro/types' does not provide an\n * export named 'queue'`. Nothing in that says which copy of `@rebasepro/types`\n * is installed, what version this CLI is, or that the two disagree — so the\n * reader goes looking for a typo in code they just copied out of the docs.\n *\n * Two shapes, because the two directions produce different errors: a named\n * export the installed copy is too old for, and a package the installed tree\n * does not have at all.\n */\nconst NAMED_EXPORT_MISSING = /does not provide an export named ['\"]([^'\"]+)['\"]/;\nconst SPECIFIER = /(@rebasepro\\/[a-z0-9-]+)/;\n\n/** The version of a package as resolved from the project, or null. */\nfunction installedVersion(specifier: string, from: string): string | null {\n try {\n const require = createRequire(path.join(from, \"noop.js\"));\n const manifest = require.resolve(`${specifier}/package.json`);\n const version: unknown = JSON.parse(fs.readFileSync(manifest, \"utf-8\")).version;\n return typeof version === \"string\" ? version : null;\n } catch {\n return null;\n }\n}\n\n/**\n * The message an issue carries, with the skew named when there is one.\n *\n * `main` runs 497 commits ahead of npm `latest` and the docs site publishes from\n * `main`, so the shape this exists for is routine: a scaffold pinned to the\n * published version, config code written against today's documentation, and an\n * error about the config file. The fix is one command and the error named none\n * of it — the same failure as `404 No PUT route on collection 'projects'`, one\n * layer down.\n *\n * Says nothing extra when the two versions agree, or when either is unreadable:\n * a sentence naming one version twice is worse than no sentence.\n */\nexport function describeIssue(err: unknown, projectRoot: string, ownVersion = cliVersion()): string {\n const message = err instanceof Error ? err.message : String(err);\n const code = (err as { code?: unknown } | null)?.code;\n const missingExport = NAMED_EXPORT_MISSING.exec(message);\n const isSkewShaped = missingExport !== null || code === \"ERR_MODULE_NOT_FOUND\";\n if (!isSkewShaped) return message;\n\n const specifier = SPECIFIER.exec(message)?.[1];\n if (!specifier) return message;\n\n // One line, not two: every caller renders an issue as `path message` on a\n // single row, so a newline here would land the useful half unindented under\n // the file name.\n const lead = /[.!?]$/.test(message.trim()) ? message.trim() : `${message.trim()}.`;\n\n const installed = installedVersion(specifier, projectRoot);\n if (installed === null) {\n return `${lead} ${specifier} is not installed in this project; this CLI is ${ownVersion}. `\n + `Run pnpm add ${specifier}@${ownVersion}`;\n }\n if (ownVersion === \"unknown\" || installed === ownVersion) return message;\n\n // Which side to move is the question the reader actually has, and it has a\n // right answer: the CLI is what evaluates this file, so the packages follow\n // it — unless the CLI is the older one, in which case following it would\n // move the project backwards.\n const cliIsNewer = compareVersions(ownVersion, installed) > 0;\n return cliIsNewer\n ? `${lead} ${specifier} ${installed} is installed; this CLI is ${ownVersion}. `\n + `Run pnpm add ${specifier}@${ownVersion}`\n : `${lead} ${specifier} ${installed} is installed; this CLI is ${ownVersion}, which is older. `\n + `Update the CLI to ${installed}, or pin ${specifier} back to ${ownVersion}`;\n}\n\n/** -1 / 0 / 1 on the numeric parts, ignoring any prerelease tail. */\nfunction compareVersions(a: string, b: string): number {\n const parts = (v: string): number[] => v.split(\"-\")[0].split(\".\").map(n => Number(n) || 0);\n const [x, y] = [parts(a), parts(b)];\n for (let i = 0; i < 3; i++) {\n if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) < (y[i] ?? 0) ? -1 : 1;\n }\n return 0;\n}\n\n/** Files that declare resources, in the order they are loaded. */\nconst RESOURCE_ENTRY_NAMES = [\"resources.ts\", \"resources.js\", \"resources/index.ts\", \"resources/index.js\"];\n\n/** Collection files, which may also declare a resource beside the collection using it. */\nfunction collectionFiles(configDir: string): string[] {\n const dir = path.join(configDir, \"collections\");\n if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return [];\n return fs.readdirSync(dir)\n .filter(f => /\\.(ts|js|mts|mjs)$/.test(f) && !/\\.(test|spec|d)\\./.test(f))\n .sort()\n .map(f => path.join(dir, f));\n}\n\n/**\n * Make the project's own TypeScript loader active in this process.\n *\n * `import()` below relies on tsx's resolver, and nothing had registered it:\n * `rebase build` runs from `node_modules/.bin/rebase`, which is plain Node.\n * On a modern Node that *looks* like it works — native type stripping imports\n * `collections/index.ts` happily — right up to the first relative specifier\n * inside it. The scaffolded template writes `import posts from \"./posts.js\"`,\n * which is the correct thing for TypeScript to emit and which native stripping\n * does not remap back to `posts.ts`. So `rebase build` failed on every\n * scaffolded project with `Cannot find module …/posts.js`, and the failure\n * looked like a broken template rather than a missing loader.\n *\n * Resolved from the project, not from this package, exactly as `resolveTsx`\n * does for the schema-generation subprocess: tsx is the project's dependency\n * (the scaffold declares it), and taking it from anywhere else would mean\n * evaluating a project's config with a loader it did not choose.\n *\n * Best-effort by design. A project with no tsx keeps the previous behaviour —\n * which is correct for one whose config is plain JavaScript, or whose imports\n * carry no extension — rather than failing on a dependency it never needed.\n * Registered once per process; `register` is cheap but not free, and deriving\n * twice in one process is an ordinary thing for a watch mode to do.\n */\nlet tsxRegistered = false;\n\nasync function registerProjectTsx(configDir: string): Promise<void> {\n if (tsxRegistered) return;\n tsxRegistered = true;\n\n // Where tsx might be, in the order most likely to find it. The scaffold\n // declares tsx in the *backend* workspace, and pnpm's isolated layout puts\n // it in `backend/node_modules` rather than hoisting it — so resolving only\n // from the config directory finds nothing on exactly the project shape this\n // exists to serve.\n const projectRoot = path.dirname(configDir);\n const bases = [configDir, projectRoot, path.join(projectRoot, \"backend\")];\n\n for (const base of bases) {\n try {\n // Resolved against a file *inside* the project so Node walks that\n // project's `node_modules`, not this package's.\n const requireFromProject = createRequire(path.join(base, \"noop.js\"));\n const api = await import(pathToFileURL(requireFromProject.resolve(\"tsx/esm/api\")).href) as\n { register?: () => unknown; default?: { register?: () => unknown } };\n\n // tsx's own `register`, not `module.register`. `tsx/esm/api` is the\n // API module; the loader is `tsx/esm`. Handing the API module to\n // `module.register` registers something with no hooks and reports\n // success — a worse failure than throwing, because the import below\n // then resolves exactly as it did before and the bug looks unfixed.\n const register = api.register ?? api.default?.register;\n if (typeof register === \"function\") {\n register();\n return;\n }\n } catch {\n // Not here. Try the next base.\n }\n }\n\n // No tsx anywhere in the project. The import below still runs; it simply\n // resolves the way Node alone would, which is correct for a config written\n // in plain JavaScript or whose imports carry no extension.\n}\n\n/**\n * Import a module for its side effects.\n *\n * Plain `import()` so tsx's loader hooks resolve `.ts` and workspace\n * specifiers, exactly as the collection loader does.\n */\nasync function evaluate(filePath: string): Promise<Record<string, unknown>> {\n return await import(pathToFileURL(filePath).href) as Record<string, unknown>;\n}\n\nexport interface DeriveOptions {\n /** Absolute path to the project's config directory. */\n configDir: string;\n /**\n * Whether to evaluate collection files too. Resources are conventionally\n * declared in `resources.ts`, but nothing stops a bucket being declared\n * beside the collection that stores into it, and a graph that missed it\n * would under-report what the project needs.\n */\n includeCollections?: boolean;\n /**\n * Absolute path to the crons directory. Each file is evaluated through the\n * same loader the runtime uses, which declares the cron under the same id\n * the scheduler runs it as. Absent, or missing on disk: no crons.\n */\n cronsDir?: string;\n /**\n * Absolute path to the functions directory. Read statically — the\n * bundler's portability analysis — never evaluated: a function's module\n * scope belongs to a running backend, not a build.\n */\n functionsDir?: string;\n /** The project root, for the paths a graph records. Defaults to the config directory's parent. */\n projectRoot?: string;\n}\n\n/** The derive options for a backend app, from its manifest entry. */\nexport function deriveOptionsFor(projectRoot: string, app: RebaseBackendAppConfig): DeriveOptions {\n const paths = resolveBackendPaths(app, projectRoot);\n return {\n projectRoot,\n configDir: path.join(projectRoot, paths.config),\n cronsDir: path.join(projectRoot, paths.crons),\n functionsDir: path.join(projectRoot, paths.functions)\n };\n}\n\n/** Whether a module's default export looks like a collection. */\nfunction isCollectionLike(value: unknown): value is CollectionConfig {\n return typeof value === \"object\" && value !== null\n && typeof (value as { slug?: unknown }).slug === \"string\"\n && typeof (value as { properties?: unknown }).properties === \"object\";\n}\n\n/**\n * The resources a collection reaches: its database, and every bucket a\n * storage property stores into. Only resources the graph declares get an\n * edge — the implicit default database is not in the graph, so a collection\n * that names no `dataSource` records nothing.\n */\nfunction collectionEdges(collection: CollectionConfig, declared: Set<string>): Array<[string, string]> {\n const edges: Array<[string, string]> = [];\n const from = `collection:${collection.slug}`;\n const databaseId = resourceId(\"database\", collection.dataSource ?? DEFAULT_RESOURCE_KEY);\n if (declared.has(databaseId)) edges.push([databaseId, from]);\n\n const walk = (properties: Record<string, unknown> | undefined, prefix: string): void => {\n for (const [name, raw] of Object.entries(properties ?? {})) {\n const property = raw as {\n storage?: { storageSource?: unknown };\n of?: unknown;\n properties?: Record<string, unknown>;\n };\n if (!property || typeof property !== \"object\") continue;\n if (property.storage) {\n const key = property.storage.storageSource === undefined\n ? DEFAULT_RESOURCE_KEY\n : resourceKeyOf(property.storage.storageSource as string);\n const bucketId = resourceId(\"bucket\", key);\n if (declared.has(bucketId)) edges.push([bucketId, `property:${collection.slug}.${prefix}${name}`]);\n }\n if (property.properties) walk(property.properties, `${prefix}${name}.`);\n if (property.of && typeof property.of === \"object\") walk({ [name]: property.of }, `${prefix}`);\n }\n };\n walk(collection.properties as Record<string, unknown>, \"\");\n return edges;\n}\n\n/**\n * A cron file that would not import, said in terms somebody can act on.\n *\n * The graph is derived by evaluating each cron module, in a process with none\n * of the deployment's environment — so a cron that reaches a module validating\n * `DATABASE_URL` at import fails here and works perfectly at boot. The raw\n * message is a Zod error about a variable, which reads as \"the environment is\n * wrong\" when the environment is not the point.\n */\nfunction cronLoadIssue(problem: string): string {\n return `${summariseCronProblem(problem)}\\n` +\n \" `rebase resources` evaluates each cron file to read its schedule, and it is a build \" +\n \"step: no .env, no secrets. Move work that needs the deployment's environment inside the \" +\n \"handler — `const { x } = await import(\\\"…\\\")` — so the module scope imports nothing that \" +\n \"reads configuration.\";\n}\n\n/**\n * The cron loader's `<file> (threw: <message>)`, as one line.\n *\n * The message is whatever the module threw, and the common case is a schema\n * validator: `loadEnv()` at module scope with no `.env` present throws a\n * `ZodError`, whose `.message` is the JSON array of its issues. That arrived\n * here verbatim — ten lines of `{ \"expected\": \"string\", \"code\":\n * \"invalid_type\", … }` — with the sentence that says what to do underneath it,\n * so the reader met \"Invalid input: expected string, received undefined\" twice\n * before reaching the explanation.\n *\n * A validator's own sentences are the useful half of that object, so they are\n * kept and the serialisation is dropped.\n */\nfunction summariseCronProblem(problem: string): string {\n const match = problem.match(/^(.*) \\(threw: ([\\s\\S]*)\\)$/);\n if (!match) return problem.split(\"\\n\")[0];\n const [, file, thrown] = match;\n return `${file}: ${summariseThrownMessage(thrown)}`;\n}\n\n/** One sentence out of a thrown message, whatever shape it arrived in. */\nfunction summariseThrownMessage(message: string): string {\n const trimmed = message.trim();\n if (trimmed.startsWith(\"[\") || trimmed.startsWith(\"{\")) {\n try {\n const parsed: unknown = JSON.parse(trimmed);\n const issues = Array.isArray(parsed) ? parsed : [parsed];\n const sentences = [...new Set(\n issues\n .map(issue => (issue as { message?: unknown } | null)?.message)\n .filter((m): m is string => typeof m === \"string\" && m.length > 0)\n )];\n if (sentences.length > 0) return sentences.join(\"; \");\n } catch {\n // Not JSON after all — fall through to the first line.\n }\n }\n return trimmed.split(\"\\n\")[0];\n}\n\n/**\n * Evaluate a project's config and return the graph it declares.\n *\n * Clears any previously registered declarations first, so deriving twice in one\n * process — a watch mode, a test — describes the project rather than the union\n * of every project seen so far.\n */\nexport async function deriveResourceGraph(options: DeriveOptions): Promise<{ graph: ResourceGraph; issues: ResourceIssue[] }> {\n const { configDir, includeCollections = true, cronsDir, functionsDir } = options;\n const projectRoot = options.projectRoot ?? path.dirname(configDir);\n // Before anything is evaluated: the loader has to be in place for the very\n // first import, and the first import is a collection file.\n await registerProjectTsx(configDir);\n\n // All three registries, and the side lists are the easy ones to forget:\n // resetting only the resources leaves one project's handlers attached\n // while its topics are gone — every one of them then reads as orphaned\n // against the *next* project. Caught by deriving twice in one process,\n // which is what a watch mode does.\n resetDeclaredResources();\n resetDeclaredSubscriptions();\n resetDeclaredQueueConsumers();\n\n const issues: ResourceIssue[] = [];\n const usedBy = new Map<string, string[]>();\n // `recordUse`, not `use`: React 19 made `use` a hook name, and\n // `react-hooks/rules-of-hooks` refuses a call to one from a plain function\n // — a lint error on a file with no React in it.\n const recordUse = (resource: string, by: string): void => {\n const list = usedBy.get(resource) ?? [];\n if (!list.includes(by)) list.push(by);\n usedBy.set(resource, list);\n };\n\n // ── Declarations ─────────────────────────────────────────────────────\n // Which export name is which resource, so a function's `import { media }`\n // can be recorded as reaching `bucket:media` without evaluating the\n // function. The export name and the key are usually the same word and\n // nothing requires them to be.\n const handleExports = new Map<string, string>();\n for (const name of RESOURCE_ENTRY_NAMES) {\n const candidate = path.join(configDir, name);\n if (!fs.existsSync(candidate)) continue;\n try {\n const mod = await evaluate(candidate);\n for (const [exportName, value] of Object.entries(mod)) {\n if (isResourceHandle(value)) handleExports.set(exportName, resourceId(value.kind, value.key));\n }\n } catch (err) {\n issues.push({ path: path.relative(configDir, candidate), message: describeIssue(err, projectRoot) });\n }\n break;\n }\n\n // ── Collections ──────────────────────────────────────────────────────\n const collections: CollectionConfig[] = [];\n if (includeCollections) {\n for (const entry of collectionFiles(configDir)) {\n try {\n const mod = await evaluate(entry);\n // As the loader sees it at boot: a handle written where a key\n // belongs is its key here too.\n if (isCollectionLike(mod.default)) collections.push(resolveResourceRefs(mod.default));\n } catch (err) {\n // Named individually: \"could not derive the graph\" with no file in\n // it sends somebody reading every module in the directory.\n issues.push({\n path: path.relative(configDir, entry),\n message: describeIssue(err, projectRoot)\n });\n }\n }\n }\n\n // ── Crons ────────────────────────────────────────────────────────────\n // Through the runtime's own loader, so the id the graph records is the id\n // the scheduler runs — the filename — and a cron that would not load at\n // boot does not load here either.\n //\n // Which means the module is EVALUATED, in a build step that has none of the\n // deployment's environment. A cron whose module scope reaches something that\n // validates `DATABASE_URL` at import therefore derives on a laptop with a\n // `.env` and nowhere else. That is worth an explicit sentence rather than a\n // stack, so `cronLoadIssue` adds one.\n if (cronsDir && fs.existsSync(cronsDir)) {\n // The runtime's loader logs each job as it loads, to stdout, as JSON\n // — which is also where `rebase resources --json` writes the graph.\n // Its problems come back as a list, so the log adds nothing here.\n const previousLevel = process.env.LOG_LEVEL;\n process.env.LOG_LEVEL = \"error\";\n try {\n const { loadCrons, declareCrons } = await import(\"@rebasepro/server\");\n const reportProblem = (problem: string) => {\n issues.push({ path: path.relative(projectRoot, cronsDir), message: cronLoadIssue(problem) });\n };\n // This process's own `import()`, not the loader's `new Function`\n // one: tsx's hooks are registered here, and a test runner's module\n // sandbox answers the other with \"a dynamic import callback was\n // not specified\" — the same trap the split-roles e2e records.\n const crons = await loadCrons(cronsDir, { importModule: url => import(url), onSkip: reportProblem });\n // One at a time, so a cron the graph refuses is reported by name\n // and the others are still declared.\n for (const [id, definition] of Object.entries(crons)) {\n try {\n declareCrons({ [id]: definition });\n } catch (err) {\n reportProblem(`${id} (threw: ${err instanceof Error ? err.message : String(err)})`);\n }\n }\n } catch (err) {\n issues.push({\n path: path.relative(projectRoot, cronsDir),\n message: cronLoadIssue(err instanceof Error ? err.message : String(err))\n });\n } finally {\n if (previousLevel === undefined) delete process.env.LOG_LEVEL;\n else process.env.LOG_LEVEL = previousLevel;\n }\n }\n\n // ── Functions ────────────────────────────────────────────────────────\n if (functionsDir && fs.existsSync(functionsDir)) {\n for (const report of analyseFunctionsDirectory(functionsDir, projectRoot)) {\n const requires = [...new Set(\n report.issues\n .filter(issue => issue.kind === \"node-builtin\" || issue.kind === \"node-only-package\")\n .map(issue => issue.message)\n )];\n try {\n declareFunction(report.name, {\n file: report.file,\n portable: report.portable,\n ...(requires.length > 0 ? { requires } : {})\n });\n } catch (err) {\n issues.push({ path: report.file, message: describeIssue(err, projectRoot) });\n }\n for (const imported of report.resourceImports) {\n const resource = handleExports.get(imported);\n if (resource) recordUse(resource, `function:${report.name}`);\n }\n }\n }\n\n // ── Edges ────────────────────────────────────────────────────────────\n const declared = new Set(declaredResources().map(r => resourceId(r.kind, r.key)));\n for (const collection of collections) {\n for (const [resource, by] of collectionEdges(collection, declared)) recordUse(resource, by);\n }\n for (const sub of declaredSubscriptions()) {\n const id = resourceId(\"topic\", sub.topic);\n if (declared.has(id)) recordUse(id, `subscription:${sub.topic}.${sub.name}`);\n }\n for (const consumer of declaredQueueConsumers()) {\n const id = resourceId(\"queue\", consumer.queue);\n if (declared.has(id)) recordUse(id, `handler:${consumer.queue}`);\n }\n\n const graph = buildResourceGraph(usedBy);\n\n // Two resources of a kind whose env suffixes collide would silently read\n // each other's configuration, which is indistinguishable from a\n // misconfigured deployment.\n const byKind = new Map<string, string[]>();\n for (const r of graph.resources) {\n byKind.set(r.kind, [...(byKind.get(r.kind) ?? []), r.key]);\n }\n for (const [kind, keys] of byKind) {\n const collision = findEnvSuffixCollision(keys);\n if (collision) {\n issues.push({\n path: `${kind}.${collision.b}`,\n message:\n `\"${collision.a}\" and \"${collision.b}\" both bind from ${collision.suffix}, so one would ` +\n \"read the other's configuration. Rename one.\"\n });\n }\n }\n\n // A subscription on a topic nobody declared is a handler that will never\n // run, and nothing else would ever say so.\n const topics = new Set(graph.resources.filter(r => r.kind === \"topic\").map(r => r.key));\n for (const sub of declaredSubscriptions()) {\n if (!topics.has(sub.topic)) {\n issues.push({\n path: `topic.${sub.topic}`,\n message:\n `Subscription \"${sub.name}\" is declared on topic \"${sub.topic}\", which nothing declares. ` +\n \"The handler would never run.\"\n });\n }\n }\n\n // A collection routed to a database nothing declares boots into a\n // refusal, and a property stored in a bucket nothing declares answers 501\n // on its first upload. Both are cheaper to hear about here.\n const databases = new Set(graph.resources.filter(r => r.kind === \"database\").map(r => r.key));\n const buckets = new Set(graph.resources.filter(r => r.kind === \"bucket\").map(r => r.key));\n for (const collection of collections) {\n if (collection.dataSource && collection.dataSource !== DEFAULT_RESOURCE_KEY && !databases.has(collection.dataSource)) {\n issues.push({\n path: `collection.${collection.slug}`,\n message:\n `routes to dataSource \"${collection.dataSource}\", which nothing declares. ` +\n `Add database(\"${collection.dataSource}\") to resources.ts, or import the handle and use it here.`\n });\n }\n for (const [name, raw] of Object.entries(collection.properties ?? {})) {\n const key = (raw as { storage?: { storageSource?: unknown } })?.storage?.storageSource;\n if (typeof key === \"string\" && key !== DEFAULT_RESOURCE_KEY && buckets.size > 0 && !buckets.has(key)) {\n issues.push({\n path: `collection.${collection.slug}.${name}`,\n message: `stores into bucket \"${key}\", which nothing declares. Add bucket(\"${key}\") to resources.ts.`\n });\n }\n }\n }\n\n return { graph, issues };\n}\n\n/** The graph as it is written to disk: stable key order, trailing newline. */\nexport function serializeResourceGraph(graph: ResourceGraph): string {\n const resources = graph.resources.map(r => ({\n kind: r.kind,\n key: r.key,\n engine: r.engine,\n transport: r.transport,\n ...(r.label !== undefined ? { label: r.label } : {}),\n ...(Object.keys(r.options).length > 0 ? { options: r.options } : {}),\n ...(r.usedBy && r.usedBy.length > 0 ? { usedBy: r.usedBy } : {})\n }));\n return JSON.stringify({\n $generated: `Generated from config by \\`rebase resources\\`. Edit the declarations, not this file.`,\n version: graph.version,\n resources\n }, null, 4) + \"\\n\";\n}\n\n/** Read the committed graph, or null when a project has none yet. */\nexport function readResourceGraphFile(projectRoot: string): string | null {\n const file = path.join(projectRoot, RESOURCE_GRAPH_FILENAME);\n return fs.existsSync(file) ? fs.readFileSync(file, \"utf-8\") : null;\n}\n\n/**\n * Write the graph, and say whether the file changed.\n *\n * The boolean is what a `--check` mode reports on: a project whose committed\n * graph disagrees with its config has a host reading one thing and a runtime\n * doing another.\n */\nexport function writeResourceGraphFile(projectRoot: string, graph: ResourceGraph): { changed: boolean; file: string } {\n const file = path.join(projectRoot, RESOURCE_GRAPH_FILENAME);\n const next = serializeResourceGraph(graph);\n const previous = fs.existsSync(file) ? fs.readFileSync(file, \"utf-8\") : null;\n if (previous === next) return { changed: false, file };\n fs.writeFileSync(file, next);\n return { changed: true, file };\n}\n\n/** Parse a committed graph file, tolerating the `$generated` banner. */\nexport function parseResourceGraph(contents: string): ResourceGraph {\n const raw = JSON.parse(contents) as { version?: number; resources?: unknown };\n if (raw.version !== 1) {\n throw new Error(\n `Unsupported resource graph version ${String(raw.version)}. ` +\n \"This project was generated by a newer Rebase; upgrade rather than provisioning half of it.\"\n );\n }\n const entries = Array.isArray(raw.resources) ? raw.resources as Record<string, unknown>[] : [];\n // `options` is omitted from the file when empty; a reader of the graph\n // expects it present, so it is restored here.\n return { version: 1, resources: entries.map(r => ({ options: {}, ...r })) as ResourceGraph[\"resources\"] };\n}\n\n/**\n * Add the resources a project has without declaring them.\n *\n * A backend has a database whether or not anyone said so, and a project that\n * declares no buckets still gets one default storage source from the plain\n * unsuffixed variables. Both are load-bearing defaults and both are invisible\n * in the graph, so a status view built only from declarations would show an\n * empty screen to the majority of projects — the ones that most need to be told\n * which variable their one database reads.\n *\n * Declaring a *named* one leaves the default where it is, as the server does\n * (`resolveDataSources` and `resolveStorageSources` in `@rebasepro/server`):\n * `bucket(\"media\")` adds a source and never moves the files the default holds,\n * so both are listed. The default is displaced only by declaring it — or, for a\n * bucket, by marking exactly one server-side bucket `default: true`, which then\n * serves the uploads that name no `storageSource`. This used to drop the\n * default once a bucket was declared, and `rebase status` and `rebase\n * resources` stopped listing a source the server was still serving.\n */\nexport function withImplicitDefaults(graph: ResourceGraph): {\n declaration: ResourceDeclaration;\n implicit: boolean;\n}[] {\n const entries = graph.resources.map(declaration => ({ declaration, implicit: false }));\n const declaresDefault = (kind: string) => graph.resources.some(r => r.kind === kind && r.key === DEFAULT_RESOURCE_KEY);\n const markedDefault = graph.resources.filter(r =>\n r.kind === \"bucket\" && (r.transport ?? \"server\") === \"server\" && r.options.default === true);\n\n if (!declaresDefault(\"database\")) {\n entries.unshift({\n declaration: {\n kind: \"database\",\n key: DEFAULT_RESOURCE_KEY,\n engine: \"postgres\",\n transport: \"server\",\n options: {}\n },\n implicit: true\n });\n }\n if (!declaresDefault(\"bucket\") && markedDefault.length !== 1) {\n const implicitBucket = {\n declaration: {\n kind: \"bucket\",\n key: DEFAULT_RESOURCE_KEY,\n // `local` is what an unconfigured default source resolves to,\n // and naming it here keeps the row honest about what a project\n // with no S3 variables actually gets: a directory that a\n // container erases on restart, which production then drops.\n engine: \"local\",\n transport: \"server\" as const,\n options: {}\n },\n implicit: true\n };\n // First among the buckets, as the database default is first among the\n // databases; last when there are none.\n const firstBucket = entries.findIndex(entry => entry.declaration.kind === \"bucket\");\n if (firstBucket === -1) entries.push(implicitBucket);\n else entries.splice(firstBucket, 0, implicitBucket);\n }\n return entries;\n}\n\n/**\n * The resources a project *has*, which is what a person is asking about.\n *\n * One projection, because two of them disagreed. `rebase status` built its\n * rows from {@link withImplicitDefaults} and listed `buckets ✓ (default) local\n * · implicit`; `rebase resources` and `rebase resources --json` built theirs\n * from the raw graph and listed only the database and the function. Two\n * commands whose whole job is to answer \"what does this project need\", on the\n * same stock scaffold, giving different answers.\n *\n * The implicit entries are marked rather than hidden, because the distinction\n * is real and load-bearing: a declared resource is recorded in\n * `rebase.resources.json` for a host to provision, and an implicit one is a\n * default the runtime supplies whether or not anyone wrote it down. That file\n * still holds declarations only — it is a wire contract a host reads, and\n * putting defaults in it would ask for something to be provisioned that\n * nobody declared.\n */\nexport type ProjectedResource = ResourceDeclaration & { implicit?: true };\n\nexport function projectResourceGraph(graph: ResourceGraph): ProjectedResource[] {\n return withImplicitDefaults(graph).map(({ declaration, implicit }) =>\n (implicit ? { ...declaration, implicit: true as const } : declaration));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,IAAa,0BAA0B;;;;;;;;;;;;;;AAqBvC,IAAM,uBAAuB;AAC7B,IAAM,YAAY;;AAGlB,SAAS,iBAAiB,WAAmB,MAA6B;CACtE,IAAI;EAEA,MAAM,WADU,cAAc,KAAK,KAAK,MAAM,SAAS,CACtC,CAAA,CAAQ,QAAQ,GAAG,UAAU,cAAc;EAC5D,MAAM,UAAmB,KAAK,MAAM,GAAG,aAAa,UAAU,OAAO,CAAC,CAAC,CAAC;EACxE,OAAO,OAAO,YAAY,WAAW,UAAU;CACnD,QAAQ;EACJ,OAAO;CACX;AACJ;;;;;;;;;;;;;;AAeA,SAAgB,cAAc,KAAc,aAAqB,aAAa,WAAW,GAAW;CAChG,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC/D,MAAM,OAAQ,KAAmC;CAGjD,IAAI,EAFkB,qBAAqB,KAAK,OAC3B,MAAkB,QAAQ,SAAS,yBACrC,OAAO;CAE1B,MAAM,YAAY,UAAU,KAAK,OAAO,CAAC,GAAG;CAC5C,IAAI,CAAC,WAAW,OAAO;CAKvB,MAAM,OAAO,SAAS,KAAK,QAAQ,KAAK,CAAC,IAAI,QAAQ,KAAK,IAAI,GAAG,QAAQ,KAAK,EAAE;CAEhF,MAAM,YAAY,iBAAiB,WAAW,WAAW;CACzD,IAAI,cAAc,MACd,OAAO,GAAG,KAAK,GAAG,UAAU,iDAAiD,WAAW,iBAClE,UAAU,GAAG;CAEvC,IAAI,eAAe,aAAa,cAAc,YAAY,OAAO;CAOjE,OADmB,gBAAgB,YAAY,SAAS,IAAI,IAEtD,GAAG,KAAK,GAAG,UAAU,GAAG,UAAU,6BAA6B,WAAW,iBACtD,UAAU,GAAG,eACjC,GAAG,KAAK,GAAG,UAAU,GAAG,UAAU,6BAA6B,WAAW,sCACjD,UAAU,WAAW,UAAU,WAAW;AAC7E;;AAGA,SAAS,gBAAgB,GAAW,GAAmB;CACnD,MAAM,SAAS,MAAwB,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,KAAI,MAAK,OAAO,CAAC,KAAK,CAAC;CACzF,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC;CAClC,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KACnB,KAAK,EAAE,MAAM,QAAQ,EAAE,MAAM,IAAI,QAAQ,EAAE,MAAM,MAAM,EAAE,MAAM,KAAK,KAAK;CAE7E,OAAO;AACX;;AAGA,IAAM,uBAAuB;CAAC;CAAgB;CAAgB;CAAsB;AAAoB;;AAGxG,SAAS,gBAAgB,WAA6B;CAClD,MAAM,MAAM,KAAK,KAAK,WAAW,aAAa;CAC9C,IAAI,CAAC,GAAG,WAAW,GAAG,KAAK,CAAC,GAAG,SAAS,GAAG,CAAC,CAAC,YAAY,GAAG,OAAO,CAAC;CACpE,OAAO,GAAG,YAAY,GAAG,CAAC,CACrB,QAAO,MAAK,qBAAqB,KAAK,CAAC,KAAK,CAAC,oBAAoB,KAAK,CAAC,CAAC,CAAC,CACzE,KAAK,CAAC,CACN,KAAI,MAAK,KAAK,KAAK,KAAK,CAAC,CAAC;AACnC;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,IAAI,gBAAgB;AAEpB,eAAe,mBAAmB,WAAkC;CAChE,IAAI,eAAe;CACnB,gBAAgB;CAOhB,MAAM,cAAc,KAAK,QAAQ,SAAS;CAC1C,MAAM,QAAQ;EAAC;EAAW;EAAa,KAAK,KAAK,aAAa,SAAS;CAAC;CAExE,KAAK,MAAM,QAAQ,OACf,IAAI;EAIA,MAAM,MAAM,MAAM,OAAO,cADE,cAAc,KAAK,KAAK,MAAM,SAAS,CAC3B,CAAA,CAAmB,QAAQ,aAAa,CAAC,CAAC,CAAC;EAQlF,MAAM,WAAW,IAAI,YAAY,IAAI,SAAS;EAC9C,IAAI,OAAO,aAAa,YAAY;GAChC,SAAS;GACT;EACJ;CACJ,QAAQ,CAER;AAMR;;;;;;;AAQA,eAAe,SAAS,UAAoD;CACxE,OAAO,MAAM,OAAO,cAAc,QAAQ,CAAC,CAAC;AAChD;;AA6BA,SAAgB,iBAAiB,aAAqB,KAA4C;CAC9F,MAAM,QAAQ,oBAAoB,KAAK,WAAW;CAClD,OAAO;EACH;EACA,WAAW,KAAK,KAAK,aAAa,MAAM,MAAM;EAC9C,UAAU,KAAK,KAAK,aAAa,MAAM,KAAK;EAC5C,cAAc,KAAK,KAAK,aAAa,MAAM,SAAS;CACxD;AACJ;;AAGA,SAAS,iBAAiB,OAA2C;CACjE,OAAO,OAAO,UAAU,YAAY,UAAU,QACvC,OAAQ,MAA6B,SAAS,YAC9C,OAAQ,MAAmC,eAAe;AACrE;;;;;;;AAQA,SAAS,gBAAgB,YAA8B,UAAgD;CACnG,MAAM,QAAiC,CAAC;CACxC,MAAM,OAAO,cAAc,WAAW;CACtC,MAAM,aAAa,WAAW,YAAY,WAAW,cAAc,oBAAoB;CACvF,IAAI,SAAS,IAAI,UAAU,GAAG,MAAM,KAAK,CAAC,YAAY,IAAI,CAAC;CAE3D,MAAM,QAAQ,YAAiD,WAAyB;EACpF,KAAK,MAAM,CAAC,MAAM,QAAQ,OAAO,QAAQ,cAAc,CAAC,CAAC,GAAG;GACxD,MAAM,WAAW;GAKjB,IAAI,CAAC,YAAY,OAAO,aAAa,UAAU;GAC/C,IAAI,SAAS,SAAS;IAIlB,MAAM,WAAW,WAAW,UAHhB,SAAS,QAAQ,kBAAkB,KAAA,IACzC,uBACA,cAAc,SAAS,QAAQ,aAAuB,CACnB;IACzC,IAAI,SAAS,IAAI,QAAQ,GAAG,MAAM,KAAK,CAAC,UAAU,YAAY,WAAW,KAAK,GAAG,SAAS,MAAM,CAAC;GACrG;GACA,IAAI,SAAS,YAAY,KAAK,SAAS,YAAY,GAAG,SAAS,KAAK,EAAE;GACtE,IAAI,SAAS,MAAM,OAAO,SAAS,OAAO,UAAU,KAAK,GAAG,OAAO,SAAS,GAAG,GAAG,GAAG,QAAQ;EACjG;CACJ;CACA,KAAK,WAAW,YAAuC,EAAE;CACzD,OAAO;AACX;;;;;;;;;;AAWA,SAAS,cAAc,SAAyB;CAC5C,OAAO,GAAG,qBAAqB,OAAO,EAAE;AAK5C;;;;;;;;;;;;;;;AAgBA,SAAS,qBAAqB,SAAyB;CACnD,MAAM,QAAQ,QAAQ,MAAM,6BAA6B;CACzD,IAAI,CAAC,OAAO,OAAO,QAAQ,MAAM,IAAI,CAAC,CAAC;CACvC,MAAM,GAAG,MAAM,UAAU;CACzB,OAAO,GAAG,KAAK,IAAI,uBAAuB,MAAM;AACpD;;AAGA,SAAS,uBAAuB,SAAyB;CACrD,MAAM,UAAU,QAAQ,KAAK;CAC7B,IAAI,QAAQ,WAAW,GAAG,KAAK,QAAQ,WAAW,GAAG,GACjD,IAAI;EACA,MAAM,SAAkB,KAAK,MAAM,OAAO;EAE1C,MAAM,YAAY,CAAC,GAAG,IAAI,KADX,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,MAAM,EAAA,CAG9C,KAAI,UAAU,OAAwC,OAAO,CAAC,CAC9D,QAAQ,MAAmB,OAAO,MAAM,YAAY,EAAE,SAAS,CAAC,CACzE,CAAC;EACD,IAAI,UAAU,SAAS,GAAG,OAAO,UAAU,KAAK,IAAI;CACxD,QAAQ,CAER;CAEJ,OAAO,QAAQ,MAAM,IAAI,CAAC,CAAC;AAC/B;;;;;;;;AASA,eAAsB,oBAAoB,SAAoF;CAC1H,MAAM,EAAE,WAAW,qBAAqB,MAAM,UAAU,iBAAiB;CACzE,MAAM,cAAc,QAAQ,eAAe,KAAK,QAAQ,SAAS;CAGjE,MAAM,mBAAmB,SAAS;CAOlC,uBAAuB;CACvB,2BAA2B;CAC3B,4BAA4B;CAE5B,MAAM,SAA0B,CAAC;CACjC,MAAM,yBAAS,IAAI,IAAsB;CAIzC,MAAM,aAAa,UAAkB,OAAqB;EACtD,MAAM,OAAO,OAAO,IAAI,QAAQ,KAAK,CAAC;EACtC,IAAI,CAAC,KAAK,SAAS,EAAE,GAAG,KAAK,KAAK,EAAE;EACpC,OAAO,IAAI,UAAU,IAAI;CAC7B;CAOA,MAAM,gCAAgB,IAAI,IAAoB;CAC9C,KAAK,MAAM,QAAQ,sBAAsB;EACrC,MAAM,YAAY,KAAK,KAAK,WAAW,IAAI;EAC3C,IAAI,CAAC,GAAG,WAAW,SAAS,GAAG;EAC/B,IAAI;GACA,MAAM,MAAM,MAAM,SAAS,SAAS;GACpC,KAAK,MAAM,CAAC,YAAY,UAAU,OAAO,QAAQ,GAAG,GAChD,IAAI,iBAAiB,KAAK,GAAG,cAAc,IAAI,YAAY,WAAW,MAAM,MAAM,MAAM,GAAG,CAAC;EAEpG,SAAS,KAAK;GACV,OAAO,KAAK;IAAE,MAAM,KAAK,SAAS,WAAW,SAAS;IAAG,SAAS,cAAc,KAAK,WAAW;GAAE,CAAC;EACvG;EACA;CACJ;CAGA,MAAM,cAAkC,CAAC;CACzC,IAAI,oBACA,KAAK,MAAM,SAAS,gBAAgB,SAAS,GACzC,IAAI;EACA,MAAM,MAAM,MAAM,SAAS,KAAK;EAGhC,IAAI,iBAAiB,IAAI,OAAO,GAAG,YAAY,KAAK,oBAAoB,IAAI,OAAO,CAAC;CACxF,SAAS,KAAK;EAGV,OAAO,KAAK;GACR,MAAM,KAAK,SAAS,WAAW,KAAK;GACpC,SAAS,cAAc,KAAK,WAAW;EAC3C,CAAC;CACL;CAcR,IAAI,YAAY,GAAG,WAAW,QAAQ,GAAG;EAIrC,MAAM,gBAAgB,QAAQ,IAAI;EAClC,QAAQ,IAAI,YAAY;EACxB,IAAI;GACA,MAAM,EAAE,WAAW,iBAAiB,MAAM,OAAO;GACjD,MAAM,iBAAiB,YAAoB;IACvC,OAAO,KAAK;KAAE,MAAM,KAAK,SAAS,aAAa,QAAQ;KAAG,SAAS,cAAc,OAAO;IAAE,CAAC;GAC/F;GAKA,MAAM,QAAQ,MAAM,UAAU,UAAU;IAAE,eAAc,QAAO,OAAO;IAAM,QAAQ;GAAc,CAAC;GAGnG,KAAK,MAAM,CAAC,IAAI,eAAe,OAAO,QAAQ,KAAK,GAC/C,IAAI;IACA,aAAa,GAAG,KAAK,WAAW,CAAC;GACrC,SAAS,KAAK;IACV,cAAc,GAAG,GAAG,WAAW,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,EAAE;GACtF;EAER,SAAS,KAAK;GACV,OAAO,KAAK;IACR,MAAM,KAAK,SAAS,aAAa,QAAQ;IACzC,SAAS,cAAc,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;GAC3E,CAAC;EACL,UAAU;GACN,IAAI,kBAAkB,KAAA,GAAW,OAAO,QAAQ,IAAI;QAC/C,QAAQ,IAAI,YAAY;EACjC;CACJ;CAGA,IAAI,gBAAgB,GAAG,WAAW,YAAY,GAC1C,KAAK,MAAM,UAAU,0BAA0B,cAAc,WAAW,GAAG;EACvE,MAAM,WAAW,CAAC,GAAG,IAAI,IACrB,OAAO,OACF,QAAO,UAAS,MAAM,SAAS,kBAAkB,MAAM,SAAS,mBAAmB,CAAC,CACpF,KAAI,UAAS,MAAM,OAAO,CACnC,CAAC;EACD,IAAI;GACA,gBAAgB,OAAO,MAAM;IACzB,MAAM,OAAO;IACb,UAAU,OAAO;IACjB,GAAI,SAAS,SAAS,IAAI,EAAE,SAAS,IAAI,CAAC;GAC9C,CAAC;EACL,SAAS,KAAK;GACV,OAAO,KAAK;IAAE,MAAM,OAAO;IAAM,SAAS,cAAc,KAAK,WAAW;GAAE,CAAC;EAC/E;EACA,KAAK,MAAM,YAAY,OAAO,iBAAiB;GAC3C,MAAM,WAAW,cAAc,IAAI,QAAQ;GAC3C,IAAI,UAAU,UAAU,UAAU,YAAY,OAAO,MAAM;EAC/D;CACJ;CAIJ,MAAM,WAAW,IAAI,IAAI,kBAAkB,CAAC,CAAC,KAAI,MAAK,WAAW,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;CAChF,KAAK,MAAM,cAAc,aACrB,KAAK,MAAM,CAAC,UAAU,OAAO,gBAAgB,YAAY,QAAQ,GAAG,UAAU,UAAU,EAAE;CAE9F,KAAK,MAAM,OAAO,sBAAsB,GAAG;EACvC,MAAM,KAAK,WAAW,SAAS,IAAI,KAAK;EACxC,IAAI,SAAS,IAAI,EAAE,GAAG,UAAU,IAAI,gBAAgB,IAAI,MAAM,GAAG,IAAI,MAAM;CAC/E;CACA,KAAK,MAAM,YAAY,uBAAuB,GAAG;EAC7C,MAAM,KAAK,WAAW,SAAS,SAAS,KAAK;EAC7C,IAAI,SAAS,IAAI,EAAE,GAAG,UAAU,IAAI,WAAW,SAAS,OAAO;CACnE;CAEA,MAAM,QAAQ,mBAAmB,MAAM;CAKvC,MAAM,yBAAS,IAAI,IAAsB;CACzC,KAAK,MAAM,KAAK,MAAM,WAClB,OAAO,IAAI,EAAE,MAAM,CAAC,GAAI,OAAO,IAAI,EAAE,IAAI,KAAK,CAAC,GAAI,EAAE,GAAG,CAAC;CAE7D,KAAK,MAAM,CAAC,MAAM,SAAS,QAAQ;EAC/B,MAAM,YAAY,uBAAuB,IAAI;EAC7C,IAAI,WACA,OAAO,KAAK;GACR,MAAM,GAAG,KAAK,GAAG,UAAU;GAC3B,SACI,IAAI,UAAU,EAAE,SAAS,UAAU,EAAE,mBAAmB,UAAU,OAAO;EAEjF,CAAC;CAET;CAIA,MAAM,SAAS,IAAI,IAAI,MAAM,UAAU,QAAO,MAAK,EAAE,SAAS,OAAO,CAAC,CAAC,KAAI,MAAK,EAAE,GAAG,CAAC;CACtF,KAAK,MAAM,OAAO,sBAAsB,GACpC,IAAI,CAAC,OAAO,IAAI,IAAI,KAAK,GACrB,OAAO,KAAK;EACR,MAAM,SAAS,IAAI;EACnB,SACI,iBAAiB,IAAI,KAAK,0BAA0B,IAAI,MAAM;CAEtE,CAAC;CAOT,MAAM,YAAY,IAAI,IAAI,MAAM,UAAU,QAAO,MAAK,EAAE,SAAS,UAAU,CAAC,CAAC,KAAI,MAAK,EAAE,GAAG,CAAC;CAC5F,MAAM,UAAU,IAAI,IAAI,MAAM,UAAU,QAAO,MAAK,EAAE,SAAS,QAAQ,CAAC,CAAC,KAAI,MAAK,EAAE,GAAG,CAAC;CACxF,KAAK,MAAM,cAAc,aAAa;EAClC,IAAI,WAAW,cAAc,WAAW,eAAe,wBAAwB,CAAC,UAAU,IAAI,WAAW,UAAU,GAC/G,OAAO,KAAK;GACR,MAAM,cAAc,WAAW;GAC/B,SACI,yBAAyB,WAAW,WAAW,2CAC9B,WAAW,WAAW;EAC/C,CAAC;EAEL,KAAK,MAAM,CAAC,MAAM,QAAQ,OAAO,QAAQ,WAAW,cAAc,CAAC,CAAC,GAAG;GACnE,MAAM,MAAO,KAAmD,SAAS;GACzE,IAAI,OAAO,QAAQ,YAAY,QAAQ,wBAAwB,QAAQ,OAAO,KAAK,CAAC,QAAQ,IAAI,GAAG,GAC/F,OAAO,KAAK;IACR,MAAM,cAAc,WAAW,KAAK,GAAG;IACvC,SAAS,uBAAuB,IAAI,yCAAyC,IAAI;GACrF,CAAC;EAET;CACJ;CAEA,OAAO;EAAE;EAAO;CAAO;AAC3B;;AAGA,SAAgB,uBAAuB,OAA8B;CACjE,MAAM,YAAY,MAAM,UAAU,KAAI,OAAM;EACxC,MAAM,EAAE;EACR,KAAK,EAAE;EACP,QAAQ,EAAE;EACV,WAAW,EAAE;EACb,GAAI,EAAE,UAAU,KAAA,IAAY,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;EAClD,GAAI,OAAO,KAAK,EAAE,OAAO,CAAC,CAAC,SAAS,IAAI,EAAE,SAAS,EAAE,QAAQ,IAAI,CAAC;EAClE,GAAI,EAAE,UAAU,EAAE,OAAO,SAAS,IAAI,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;CAClE,EAAE;CACF,OAAO,KAAK,UAAU;EAClB,YAAY;EACZ,SAAS,MAAM;EACf;CACJ,GAAG,MAAM,CAAC,IAAI;AAClB;;AAGA,SAAgB,sBAAsB,aAAoC;CACtE,MAAM,OAAO,KAAK,KAAK,aAAa,uBAAuB;CAC3D,OAAO,GAAG,WAAW,IAAI,IAAI,GAAG,aAAa,MAAM,OAAO,IAAI;AAClE;;;;;;;;AASA,SAAgB,uBAAuB,aAAqB,OAA0D;CAClH,MAAM,OAAO,KAAK,KAAK,aAAa,uBAAuB;CAC3D,MAAM,OAAO,uBAAuB,KAAK;CAEzC,KADiB,GAAG,WAAW,IAAI,IAAI,GAAG,aAAa,MAAM,OAAO,IAAI,UACvD,MAAM,OAAO;EAAE,SAAS;EAAO;CAAK;CACrD,GAAG,cAAc,MAAM,IAAI;CAC3B,OAAO;EAAE,SAAS;EAAM;CAAK;AACjC;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,qBAAqB,OAGjC;CACA,MAAM,UAAU,MAAM,UAAU,KAAI,iBAAgB;EAAE;EAAa,UAAU;CAAM,EAAE;CACrF,MAAM,mBAAmB,SAAiB,MAAM,UAAU,MAAK,MAAK,EAAE,SAAS,QAAQ,EAAE,QAAQ,oBAAoB;CACrH,MAAM,gBAAgB,MAAM,UAAU,QAAO,MACzC,EAAE,SAAS,aAAa,EAAE,aAAa,cAAc,YAAY,EAAE,QAAQ,YAAY,IAAI;CAE/F,IAAI,CAAC,gBAAgB,UAAU,GAC3B,QAAQ,QAAQ;EACZ,aAAa;GACT,MAAM;GACN,KAAK;GACL,QAAQ;GACR,WAAW;GACX,SAAS,CAAC;EACd;EACA,UAAU;CACd,CAAC;CAEL,IAAI,CAAC,gBAAgB,QAAQ,KAAK,cAAc,WAAW,GAAG;EAC1D,MAAM,iBAAiB;GACnB,aAAa;IACT,MAAM;IACN,KAAK;IAKL,QAAQ;IACR,WAAW;IACX,SAAS,CAAC;GACd;GACA,UAAU;EACd;EAGA,MAAM,cAAc,QAAQ,WAAU,UAAS,MAAM,YAAY,SAAS,QAAQ;EAClF,IAAI,gBAAgB,IAAI,QAAQ,KAAK,cAAc;OAC9C,QAAQ,OAAO,aAAa,GAAG,cAAc;CACtD;CACA,OAAO;AACX;AAsBA,SAAgB,qBAAqB,OAA2C;CAC5E,OAAO,qBAAqB,KAAK,CAAC,CAAC,KAAK,EAAE,aAAa,eAClD,WAAW;EAAE,GAAG;EAAa,UAAU;CAAc,IAAI,WAAY;AAC9E"}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { i as recordEvent } from "./telemetry-
|
|
1
|
+
import { i as recordEvent } from "./telemetry-HfU5uAh1.js";
|
|
2
2
|
import { n as getPMCommands, t as detectPackageManager } from "./package-manager-DSEOwTqM.js";
|
|
3
|
-
import { a as findBackendDir, g as resolveTsx, h as resolvePluginCliScript, l as getActiveBackendPlugin, n as dependenciesNotInstalled, o as findEnvFile, p as requireProjectRoot, s as findFrontendDir, v as validateTsxInstallation } from "./project-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { i as ensureDevDatabase, o as probeTcp } from "./dev-preflight-
|
|
3
|
+
import { C as wantsHelp, S as switchOn, a as findBackendDir, b as parseCommandArgs, g as resolveTsx, h as resolvePluginCliScript, l as getActiveBackendPlugin, n as dependenciesNotInstalled, o as findEnvFile, p as requireProjectRoot, s as findFrontendDir, v as validateTsxInstallation, y as UsageError } from "./project-g4K6kV1t.js";
|
|
4
|
+
import { d as findBackendApp, f as formatAppAddress, g as resolveBackendPaths, l as cmsMountOf, p as loadManifest, v as staticAppAddress } from "./manifest-nvPdLlJY.js";
|
|
5
|
+
import { r as getProjectPort, t as DEV_BACKEND_URL_ENV } from "./dev-server-CIZwAfZa.js";
|
|
6
|
+
import { t as Refusal } from "./refusal-ReeeGP8a.js";
|
|
7
|
+
import { i as ensureDevDatabase, o as probeTcp } from "./dev-preflight-FT9GbJVj.js";
|
|
8
8
|
import { t as MANAGED_IDLE_TIMEOUT_MS } from "./constraints-CtiylsGf.js";
|
|
9
|
-
import { a as resolveCliEntry, o as resolveSpawn } from "./daemon-
|
|
10
|
-
import { DEV_DATABASE_KIND_ENV, i as lastDevDatabaseNotice, managedNotices, prepareDatabaseEnv, resolveProjectDatabase, s as recordLastDevDatabase } from "./prepare-
|
|
11
|
-
import { runDriverDbCommand, runDriverSchemaCommand } from "./db-
|
|
9
|
+
import { a as resolveCliEntry, o as resolveSpawn } from "./daemon-DDMRbz4T.js";
|
|
10
|
+
import { DEV_DATABASE_KIND_ENV, i as lastDevDatabaseNotice, managedNotices, prepareDatabaseEnv, resolveProjectDatabase, s as recordLastDevDatabase } from "./prepare-1rVNd2PF.js";
|
|
11
|
+
import { runDriverDbCommand, runDriverSchemaCommand } from "./db-9P6MnmdC.js";
|
|
12
12
|
import { t as affectsSqlSchema } from "./collection-drift-BKj_BoMq.js";
|
|
13
13
|
import chalk from "chalk";
|
|
14
14
|
import fs from "fs";
|
|
@@ -20,6 +20,80 @@ import dotenv from "dotenv";
|
|
|
20
20
|
import { DEFAULT_RESOURCE_KEY, parseEnvBoolean } from "@rebasepro/types";
|
|
21
21
|
import { execa, execaCommandSync } from "execa";
|
|
22
22
|
import { StringDecoder } from "string_decoder";
|
|
23
|
+
//#region src/utils/boot-retry.ts
|
|
24
|
+
/**
|
|
25
|
+
* A backend boot that lost its database part-way, which booting again fixes.
|
|
26
|
+
*
|
|
27
|
+
* `rebase db branch create --force` closes the database it copies: it ends
|
|
28
|
+
* every connection to it and refuses new ones until the copy is done. A
|
|
29
|
+
* backend booting at that moment failed with `Failed to start the Rebase
|
|
30
|
+
* runtime … terminating connection due to administrator command (57P01)`, and
|
|
31
|
+
* `rebase dev` then said "Fix the error above; the watcher restarts it on the
|
|
32
|
+
* next change", about an error nobody could fix and that the next boot would
|
|
33
|
+
* not have. The same holds for a database that is restarting.
|
|
34
|
+
*
|
|
35
|
+
* This reads the backend's output and says when a failed boot is worth another
|
|
36
|
+
* try, and after how long: a few tries, each waiting twice as long as the one
|
|
37
|
+
* before, and none once the backend has failed for a reason of its own.
|
|
38
|
+
*/
|
|
39
|
+
/** The sentence that starts a boot failure, as `reportBootFailure` in `@rebasepro/server` writes it. */
|
|
40
|
+
var BOOT_FAILED = /Failed to start the Rebase runtime/;
|
|
41
|
+
/**
|
|
42
|
+
* Why a boot failed that a later boot does not, by what PostgreSQL said:
|
|
43
|
+
* the server ended the connection (57P01 `pg_terminate_backend`, 57P02, 57P03
|
|
44
|
+
* a server starting or stopping), or the database is closed to new
|
|
45
|
+
* connections (55000, what a forced branch create does while it copies).
|
|
46
|
+
* A connection the server ended while it sat idle is reported by `pg` on the
|
|
47
|
+
* next query, in its own words and with no code: "Client has encountered a
|
|
48
|
+
* connection error and is not queryable".
|
|
49
|
+
*/
|
|
50
|
+
var PASSING_CAUSES = [
|
|
51
|
+
/terminating connection due to administrator command/i,
|
|
52
|
+
/Client has encountered a connection error and is not queryable/i,
|
|
53
|
+
/\(57P0[123]\)/,
|
|
54
|
+
/is not currently accepting connections/i,
|
|
55
|
+
/the database system is (?:starting up|shutting down|in recovery mode)/i
|
|
56
|
+
];
|
|
57
|
+
var BootRetry = class {
|
|
58
|
+
maxAttempts;
|
|
59
|
+
firstDelayMs;
|
|
60
|
+
failing = false;
|
|
61
|
+
planned = false;
|
|
62
|
+
attempts = 0;
|
|
63
|
+
constructor(maxAttempts = 3, firstDelayMs = 1e3) {
|
|
64
|
+
this.maxAttempts = maxAttempts;
|
|
65
|
+
this.firstDelayMs = firstDelayMs;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Read one line of the backend's output, colour removed. Returns a plan
|
|
69
|
+
* the first time a failing boot names a passing cause, and null otherwise,
|
|
70
|
+
* including once the tries are spent.
|
|
71
|
+
*/
|
|
72
|
+
line(text) {
|
|
73
|
+
if (BOOT_FAILED.test(text)) {
|
|
74
|
+
this.failing = true;
|
|
75
|
+
this.planned = false;
|
|
76
|
+
}
|
|
77
|
+
if (!this.failing || this.planned) return null;
|
|
78
|
+
if (!PASSING_CAUSES.some((cause) => cause.test(text))) return null;
|
|
79
|
+
this.planned = true;
|
|
80
|
+
if (this.attempts >= this.maxAttempts) return null;
|
|
81
|
+
this.attempts += 1;
|
|
82
|
+
return {
|
|
83
|
+
attempt: this.attempts,
|
|
84
|
+
of: this.maxAttempts,
|
|
85
|
+
delayMs: this.firstDelayMs * 2 ** (this.attempts - 1),
|
|
86
|
+
reason: text.trim()
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** The backend is serving: a later failure is a new one, with every try left. */
|
|
90
|
+
up() {
|
|
91
|
+
this.failing = false;
|
|
92
|
+
this.planned = false;
|
|
93
|
+
this.attempts = 0;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
//#endregion
|
|
23
97
|
//#region src/commands/dev.ts
|
|
24
98
|
/**
|
|
25
99
|
* CLI command: rebase dev
|
|
@@ -233,6 +307,47 @@ function devCollectionsDir(projectRoot) {
|
|
|
233
307
|
return path.join(projectRoot, "config", "collections");
|
|
234
308
|
}
|
|
235
309
|
/**
|
|
310
|
+
* Call `onAppear` once, when `target` exists, and return what stops waiting.
|
|
311
|
+
*
|
|
312
|
+
* Watches the nearest directory above `target` that exists (no higher than
|
|
313
|
+
* `root`), and follows the path down as each part of it appears — `fs.watch`
|
|
314
|
+
* cannot watch a directory that is not there yet. A headless project has no
|
|
315
|
+
* `config/collections` until `rebase schema introspect` writes one, and dev
|
|
316
|
+
* watched it only when it was there at start: the backend kept serving the
|
|
317
|
+
* API it derived from the database, and edits to the new files did nothing,
|
|
318
|
+
* until dev was restarted by hand.
|
|
319
|
+
*/
|
|
320
|
+
function whenDirectoryAppears(target, root, onAppear) {
|
|
321
|
+
let watcher = null;
|
|
322
|
+
let watching = null;
|
|
323
|
+
let done = false;
|
|
324
|
+
const stop = () => {
|
|
325
|
+
done = true;
|
|
326
|
+
watcher?.close();
|
|
327
|
+
watcher = null;
|
|
328
|
+
};
|
|
329
|
+
const arm = () => {
|
|
330
|
+
if (done) return;
|
|
331
|
+
if (fs.existsSync(target)) {
|
|
332
|
+
stop();
|
|
333
|
+
onAppear();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
let dir = path.dirname(target);
|
|
337
|
+
while (!fs.existsSync(dir) && dir !== root && path.dirname(dir) !== dir) dir = path.dirname(dir);
|
|
338
|
+
if (dir === watching || !fs.existsSync(dir)) return;
|
|
339
|
+
watcher?.close();
|
|
340
|
+
watching = dir;
|
|
341
|
+
try {
|
|
342
|
+
watcher = fs.watch(dir, () => arm());
|
|
343
|
+
} catch {
|
|
344
|
+
watcher = null;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
arm();
|
|
348
|
+
return stop;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
236
351
|
* The file dev writes to restart the backend, or `null` when tsx would not see it.
|
|
237
352
|
*
|
|
238
353
|
* Outside the project, so nothing appears in the developer's tree, and keyed by
|
|
@@ -323,7 +438,7 @@ async function declaredAdditionalDatabases(projectRoot) {
|
|
|
323
438
|
try {
|
|
324
439
|
const backend = findBackendApp(loadManifest(projectRoot).manifest);
|
|
325
440
|
if (!backend) return [];
|
|
326
|
-
const { deriveResourceGraph, deriveOptionsFor } = await import("./derive-
|
|
441
|
+
const { deriveResourceGraph, deriveOptionsFor } = await import("./derive-MTdClnkc.js").then((n) => n.i);
|
|
327
442
|
const { graph } = await deriveResourceGraph({
|
|
328
443
|
...deriveOptionsFor(projectRoot, backend.app),
|
|
329
444
|
cronsDir: void 0,
|
|
@@ -763,8 +878,12 @@ function needsStaleSchemaPreflight(generation) {
|
|
|
763
878
|
* hand, so it is captured, and on failure only the lines that say what went
|
|
764
879
|
* wrong are shown — not the stack under them. Like the schema, a failure never
|
|
765
880
|
* stops `dev`.
|
|
881
|
+
*
|
|
882
|
+
* `backendPort` is where the backend is, or is about to be: the README's
|
|
883
|
+
* example names it. The first run is before the backend binds, so it is told
|
|
884
|
+
* rather than left to read `.rebase/state.json`.
|
|
766
885
|
*/
|
|
767
|
-
async function regenerateSdk(projectRoot) {
|
|
886
|
+
async function regenerateSdk(projectRoot, backendPort) {
|
|
768
887
|
if (!projectHasCollections(projectRoot)) return;
|
|
769
888
|
const entry = resolveCliEntry();
|
|
770
889
|
const { execPath, prefixArgs } = resolveSpawn(entry);
|
|
@@ -775,7 +894,8 @@ async function regenerateSdk(projectRoot) {
|
|
|
775
894
|
"generate-sdk"
|
|
776
895
|
], {
|
|
777
896
|
cwd: projectRoot,
|
|
778
|
-
all: true
|
|
897
|
+
all: true,
|
|
898
|
+
...backendPort !== null ? { env: { [DEV_BACKEND_URL_ENV]: `http://localhost:${backendPort}` } } : {}
|
|
779
899
|
});
|
|
780
900
|
} catch (error) {
|
|
781
901
|
const cause = (typeof error === "object" && error !== null && "all" in error && typeof error.all === "string" ? error.all : "").split("\n").filter((line) => line.trim() && !/^\s+at /.test(line)).slice(-6);
|
|
@@ -982,7 +1102,7 @@ async function devCommand(rawArgs) {
|
|
|
982
1102
|
printDevHelp();
|
|
983
1103
|
return;
|
|
984
1104
|
}
|
|
985
|
-
if (rawArgs
|
|
1105
|
+
if (switchOn(rawArgs, "--generate") || rawArgs.includes("-g")) throw new UsageError("`rebase dev --generate` is gone: dev now regenerates the schema and the SDK types on every start and every collection save. Drop the flag.");
|
|
986
1106
|
const { flags: args } = parseCommandArgs({
|
|
987
1107
|
spec: DEV_FLAGS,
|
|
988
1108
|
rawArgs,
|
|
@@ -1009,7 +1129,7 @@ async function devCommand(rawArgs) {
|
|
|
1009
1129
|
const { port: startPort, source: startPortSource } = resolveStartPort(projectRoot, args["--port"]);
|
|
1010
1130
|
const portIsPinned = args["--port"] !== void 0;
|
|
1011
1131
|
const pinnedFrontendPort = process.env.REBASE_FRONTEND_PORT;
|
|
1012
|
-
if (pinnedFrontendPort && !/^\d+$/.test(pinnedFrontendPort)) throw new
|
|
1132
|
+
if (pinnedFrontendPort && !/^\d+$/.test(pinnedFrontendPort)) throw new Refusal(`REBASE_FRONTEND_PORT must be a number, got ${JSON.stringify(pinnedFrontendPort)}.`);
|
|
1013
1133
|
const frontendPort = pinnedFrontendPort ? Number(pinnedFrontendPort) : getProjectFrontendPort(projectRoot);
|
|
1014
1134
|
if (!frontendOnly) {
|
|
1015
1135
|
const refusal = await pinnedPortRefusal(startPort, portIsPinned);
|
|
@@ -1046,6 +1166,15 @@ async function devCommand(rawArgs) {
|
|
|
1046
1166
|
* which is the failure the flag exists to produce.
|
|
1047
1167
|
*/
|
|
1048
1168
|
let prepared = null;
|
|
1169
|
+
/**
|
|
1170
|
+
* The backend port the SDK README was last generated for: the one the
|
|
1171
|
+
* backend is told at boot, then each one it announces it bound.
|
|
1172
|
+
*/
|
|
1173
|
+
let sdkBackendPort = startsBackend ? startPort : null;
|
|
1174
|
+
const regenerateSdkFor = (port) => {
|
|
1175
|
+
sdkBackendPort = port;
|
|
1176
|
+
return regenerateSdk(projectRoot, port);
|
|
1177
|
+
};
|
|
1049
1178
|
if (!frontendOnly) {
|
|
1050
1179
|
const resolved = args["--docker"] ? resolveProjectDatabase(projectRoot, {
|
|
1051
1180
|
flagUrl,
|
|
@@ -1054,7 +1183,7 @@ async function devCommand(rawArgs) {
|
|
|
1054
1183
|
}) : null;
|
|
1055
1184
|
const dockerUrl = resolved?.kind === "docker" ? resolved.url ?? void 0 : void 0;
|
|
1056
1185
|
({schemaGeneration, database: prepared} = await overlapDevStartup({
|
|
1057
|
-
generate: async () => (await Promise.all([ensureGeneratedSchema(projectRoot),
|
|
1186
|
+
generate: async () => (await Promise.all([ensureGeneratedSchema(projectRoot), regenerateSdkFor(sdkBackendPort)]))[0],
|
|
1058
1187
|
preflight: () => runDatabasePreflight({
|
|
1059
1188
|
projectRoot,
|
|
1060
1189
|
disabled: noDb || flagUrl !== null,
|
|
@@ -1345,13 +1474,12 @@ async function devCommand(rawArgs) {
|
|
|
1345
1474
|
}
|
|
1346
1475
|
};
|
|
1347
1476
|
const collectionsDir = devCollectionsDir(projectRoot);
|
|
1348
|
-
|
|
1477
|
+
{
|
|
1349
1478
|
let debounce = null;
|
|
1350
1479
|
let sqlSchemaAffected = false;
|
|
1351
1480
|
let changed = "";
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
sqlSchemaAffected = sqlSchemaAffected || affectsSqlSchema(collectionsDir, filename);
|
|
1481
|
+
const collectionsChanged = (filename, affectsSchema) => {
|
|
1482
|
+
sqlSchemaAffected = sqlSchemaAffected || affectsSchema;
|
|
1355
1483
|
changed = filename;
|
|
1356
1484
|
if (debounce) clearTimeout(debounce);
|
|
1357
1485
|
debounce = setTimeout(() => {
|
|
@@ -1359,7 +1487,7 @@ async function devCommand(rawArgs) {
|
|
|
1359
1487
|
sqlSchemaAffected = false;
|
|
1360
1488
|
(async () => {
|
|
1361
1489
|
if (!regenerateSchema) {
|
|
1362
|
-
await
|
|
1490
|
+
await regenerateSdkFor(resolvedBackendPort ?? startPort);
|
|
1363
1491
|
restartBackend();
|
|
1364
1492
|
return;
|
|
1365
1493
|
}
|
|
@@ -1372,8 +1500,8 @@ async function devCommand(rawArgs) {
|
|
|
1372
1500
|
chalk.yellow(" └──────────────────────────────────────────────────────────────┘")
|
|
1373
1501
|
].join("\n"));
|
|
1374
1502
|
const schemaBefore = generatedSchemaStamp();
|
|
1375
|
-
await Promise.all([ensureGeneratedSchema(projectRoot),
|
|
1376
|
-
if (!(usesStockRuntime && generatedSchemaStamp() !== schemaBefore)) restartBackend();
|
|
1503
|
+
await Promise.all([ensureGeneratedSchema(projectRoot), regenerateSdkFor(resolvedBackendPort ?? startPort)]);
|
|
1504
|
+
if (!(usesStockRuntime && schemaBefore !== null && generatedSchemaStamp() !== schemaBefore)) restartBackend();
|
|
1377
1505
|
console.log([
|
|
1378
1506
|
chalk.green(" ✓ Schema and SDK types regenerated. The backend restarts and boot"),
|
|
1379
1507
|
chalk.green(" creates what is missing — a new collection, a new property."),
|
|
@@ -1383,6 +1511,17 @@ async function devCommand(rawArgs) {
|
|
|
1383
1511
|
].join("\n"));
|
|
1384
1512
|
})();
|
|
1385
1513
|
}, 500);
|
|
1514
|
+
};
|
|
1515
|
+
const watchCollections = () => {
|
|
1516
|
+
fs.watch(collectionsDir, { recursive: true }, (_eventType, filename) => {
|
|
1517
|
+
if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
|
|
1518
|
+
collectionsChanged(filename, affectsSqlSchema(collectionsDir, filename));
|
|
1519
|
+
});
|
|
1520
|
+
};
|
|
1521
|
+
if (fs.existsSync(collectionsDir)) watchCollections();
|
|
1522
|
+
else whenDirectoryAppears(collectionsDir, projectRoot, () => {
|
|
1523
|
+
watchCollections();
|
|
1524
|
+
collectionsChanged(`${path.basename(collectionsDir)}/`, true);
|
|
1386
1525
|
});
|
|
1387
1526
|
}
|
|
1388
1527
|
if (!backendOnly && frontendDir && await frontendCanStartWithBackend({
|
|
@@ -1415,6 +1554,23 @@ async function devCommand(rawArgs) {
|
|
|
1415
1554
|
* after the entry crashes, so nothing exits to contradict it.
|
|
1416
1555
|
*/
|
|
1417
1556
|
let backendAnnounced = false;
|
|
1557
|
+
/**
|
|
1558
|
+
* A boot that lost its database part-way is booted again, a few
|
|
1559
|
+
* times, instead of waiting for a file change. See utils/boot-retry.ts:
|
|
1560
|
+
* a `rebase db branch create --force` that lands while the backend
|
|
1561
|
+
* boots ends its connections, and nothing in the project is wrong.
|
|
1562
|
+
*/
|
|
1563
|
+
const bootRetry = new BootRetry();
|
|
1564
|
+
const retryPassingBootFailure = (line) => {
|
|
1565
|
+
if (!usableTrigger || shuttingDown) return;
|
|
1566
|
+
const plan = bootRetry.line(stripAnsi(line));
|
|
1567
|
+
if (!plan) return;
|
|
1568
|
+
setTimeout(() => {
|
|
1569
|
+
if (shuttingDown || backendAnnounced) return;
|
|
1570
|
+
console.log(chalk.gray(` ↻ The backend lost its database while it was starting; starting it again (${plan.attempt} of ${plan.of}).`));
|
|
1571
|
+
restartBackend();
|
|
1572
|
+
}, plan.delayMs).unref?.();
|
|
1573
|
+
};
|
|
1418
1574
|
/** The verdict, printed once per failure rather than once per line. */
|
|
1419
1575
|
let backendDownReported = false;
|
|
1420
1576
|
const reportBackendDown = (headline) => {
|
|
@@ -1537,6 +1693,7 @@ async function devCommand(rawArgs) {
|
|
|
1537
1693
|
console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
|
|
1538
1694
|
noticeWatcherRestart(line);
|
|
1539
1695
|
noticePortMoved(line);
|
|
1696
|
+
retryPassingBootFailure(line);
|
|
1540
1697
|
const cleanLine = stripAnsi(line);
|
|
1541
1698
|
const swaggerMatch = cleanLine.match(/Swagger UI available.*"path":"([^"]+)"/);
|
|
1542
1699
|
if (swaggerMatch) swaggerPath = swaggerMatch[1];
|
|
@@ -1545,6 +1702,7 @@ async function devCommand(rawArgs) {
|
|
|
1545
1702
|
backendAnnounced = true;
|
|
1546
1703
|
backendDownReported = false;
|
|
1547
1704
|
missedProbes = 0;
|
|
1705
|
+
bootRetry.up();
|
|
1548
1706
|
if (backendOnly) process.exitCode = 0;
|
|
1549
1707
|
disarmReadyDeadline();
|
|
1550
1708
|
resolvedBackendPort = parseInt(serverMatch[1], 10);
|
|
@@ -1561,6 +1719,7 @@ async function devCommand(rawArgs) {
|
|
|
1561
1719
|
fs.writeFileSync(urlFile, `http://localhost:${resolvedBackendPort}`, "utf-8");
|
|
1562
1720
|
const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
|
|
1563
1721
|
fs.writeFileSync(portFile, devPortFileContents(resolvedBackendPort, startPort), "utf-8");
|
|
1722
|
+
if (sdkBackendPort !== null && sdkBackendPort !== resolvedBackendPort) regenerateSdkFor(resolvedBackendPort);
|
|
1564
1723
|
}
|
|
1565
1724
|
});
|
|
1566
1725
|
/** Whether we've already shown a corrupted-modules recovery hint. */
|
|
@@ -1569,6 +1728,7 @@ async function devCommand(rawArgs) {
|
|
|
1569
1728
|
console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
|
|
1570
1729
|
noticeWatcherRestart(line);
|
|
1571
1730
|
noticePortMoved(line);
|
|
1731
|
+
retryPassingBootFailure(line);
|
|
1572
1732
|
if (!corruptedModulesWarned) {
|
|
1573
1733
|
const cleanLine = stripAnsi(line);
|
|
1574
1734
|
if (cleanLine.includes("Cannot find module") && cleanLine.includes("node_modules/.pnpm/")) {
|
|
@@ -1690,4 +1850,4 @@ ${chalk.green.bold("Description")}
|
|
|
1690
1850
|
//#endregion
|
|
1691
1851
|
export { devCommand };
|
|
1692
1852
|
|
|
1693
|
-
//# sourceMappingURL=dev-
|
|
1853
|
+
//# sourceMappingURL=dev-BDN_OKXA.js.map
|