@rebasepro/cli 0.24.0 → 0.24.1-canary.g19596ef

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.
Files changed (156) hide show
  1. package/bin/rebase.js +3 -0
  2. package/dist/agent-setup-B31F00DC.js +324 -0
  3. package/dist/agent-setup-B31F00DC.js.map +1 -0
  4. package/dist/api-keys-Bl1I9q7M.js +525 -0
  5. package/dist/api-keys-Bl1I9q7M.js.map +1 -0
  6. package/dist/apps-CNgQJjg5.js +340 -0
  7. package/dist/apps-CNgQJjg5.js.map +1 -0
  8. package/dist/args-BnmsEXvu.js +109 -0
  9. package/dist/args-BnmsEXvu.js.map +1 -0
  10. package/dist/atlas-dev-database-D_LP_Woq.js +62 -0
  11. package/dist/atlas-dev-database-D_LP_Woq.js.map +1 -0
  12. package/dist/auth-aySHEF45.js +414 -0
  13. package/dist/auth-aySHEF45.js.map +1 -0
  14. package/dist/branch-pointer-DvSHBjjz.js +2 -0
  15. package/dist/build-DIHWIqO4.js +336 -0
  16. package/dist/build-DIHWIqO4.js.map +1 -0
  17. package/dist/bundle-CqURVN-K.js +1621 -0
  18. package/dist/bundle-CqURVN-K.js.map +1 -0
  19. package/dist/bundle.d.ts +6 -1
  20. package/dist/cli.d.ts +2 -2
  21. package/dist/cloud-DjByQlkW.js +9667 -0
  22. package/dist/cloud-DjByQlkW.js.map +1 -0
  23. package/dist/collection-drift-BKj_BoMq.js +106 -0
  24. package/dist/collection-drift-BKj_BoMq.js.map +1 -0
  25. package/dist/commands/api-keys.d.ts +2 -0
  26. package/dist/commands/console.d.ts +12 -0
  27. package/dist/commands/db.d.ts +64 -24
  28. package/dist/commands/dev.d.ts +103 -6
  29. package/dist/commands/generate_sdk.d.ts +26 -0
  30. package/dist/commands/init.d.ts +58 -0
  31. package/dist/commands/skills.d.ts +22 -7
  32. package/dist/commands/start.d.ts +15 -0
  33. package/dist/commands/storage.d.ts +46 -0
  34. package/dist/commands/upgrade.d.ts +1 -0
  35. package/dist/consent-ClRHmcxf.js +76 -0
  36. package/dist/consent-ClRHmcxf.js.map +1 -0
  37. package/dist/console-D6SJf8k_.js +102 -0
  38. package/dist/console-D6SJf8k_.js.map +1 -0
  39. package/dist/context-BqhRydBg.js +1234 -0
  40. package/dist/context-BqhRydBg.js.map +1 -0
  41. package/dist/{daemon-entry-LTFKtpHy.js → daemon-entry-Da-D3rmH.js} +3 -4
  42. package/dist/daemon-entry-Da-D3rmH.js.map +1 -0
  43. package/dist/daemon-pzsQwDHa.js +2 -0
  44. package/dist/db-B7RuNuaQ.js +1312 -0
  45. package/dist/db-B7RuNuaQ.js.map +1 -0
  46. package/dist/derive-Dl0z2OWi.js +484 -0
  47. package/dist/derive-Dl0z2OWi.js.map +1 -0
  48. package/dist/dev-CYr_WXan.js +1599 -0
  49. package/dist/dev-CYr_WXan.js.map +1 -0
  50. package/dist/dev-db/atlas-dev-database.d.ts +32 -0
  51. package/dist/dev-db/daemon-entry.d.ts +13 -0
  52. package/dist/dev-db/last-dev-database.d.ts +41 -0
  53. package/dist/dev-db/prepare.d.ts +19 -3
  54. package/dist/dev-db/pull.d.ts +90 -5
  55. package/dist/dev-db/resolve.d.ts +26 -2
  56. package/dist/dev-preflight-B-LYyBf1.js +2 -0
  57. package/dist/dev-server-DG8Tt-uf.js +96 -0
  58. package/dist/dev-server-DG8Tt-uf.js.map +1 -0
  59. package/dist/doctor-BP8cZ-7F.js +771 -0
  60. package/dist/doctor-BP8cZ-7F.js.map +1 -0
  61. package/dist/eject-DizdoecV.js +335 -0
  62. package/dist/eject-DizdoecV.js.map +1 -0
  63. package/dist/fold-static-BC9CYicw.js +201 -0
  64. package/dist/fold-static-BC9CYicw.js.map +1 -0
  65. package/dist/function-portability-BIvlvR9q.js +265 -0
  66. package/dist/function-portability-BIvlvR9q.js.map +1 -0
  67. package/dist/generate_sdk-BPqdJc3Q.js +489 -0
  68. package/dist/generate_sdk-BPqdJc3Q.js.map +1 -0
  69. package/dist/index.es.js +151 -24674
  70. package/dist/index.es.js.map +1 -1
  71. package/dist/init-Utstvh2Q.js +1149 -0
  72. package/dist/init-Utstvh2Q.js.map +1 -0
  73. package/dist/last-dev-database-B58g89uA.js +2 -0
  74. package/dist/manifest-DRp0UVTj.js +880 -0
  75. package/dist/manifest-DRp0UVTj.js.map +1 -0
  76. package/dist/manifest.d.ts +15 -3
  77. package/dist/normalize-imports-X0Tgze9y.js +107 -0
  78. package/dist/normalize-imports-X0Tgze9y.js.map +1 -0
  79. package/dist/{package-manager-CXQevsE1.js → package-manager-DSEOwTqM.js} +2 -2
  80. package/dist/{package-manager-CXQevsE1.js.map → package-manager-DSEOwTqM.js.map} +1 -1
  81. package/dist/path-flags-BFgG9V6y.js +91 -0
  82. package/dist/path-flags-BFgG9V6y.js.map +1 -0
  83. package/dist/precompress.d.ts +17 -0
  84. package/dist/prepare-CDN6lSpz.js +1236 -0
  85. package/dist/prepare-CDN6lSpz.js.map +1 -0
  86. package/dist/{project-DmaGLmH2.js → project-Wp2zoyeN.js} +33 -4
  87. package/dist/project-Wp2zoyeN.js.map +1 -0
  88. package/dist/pull-CrGYMEsy.js +458 -0
  89. package/dist/pull-CrGYMEsy.js.map +1 -0
  90. package/dist/resolve-B2O-wKKk.js +2 -0
  91. package/dist/resources-FD7AfZx8.js +183 -0
  92. package/dist/resources-FD7AfZx8.js.map +1 -0
  93. package/dist/schema-DMdr431S.js +125 -0
  94. package/dist/schema-DMdr431S.js.map +1 -0
  95. package/dist/skills-DTFibT1m.js +642 -0
  96. package/dist/skills-DTFibT1m.js.map +1 -0
  97. package/dist/start-DEa-hOBh.js +267 -0
  98. package/dist/start-DEa-hOBh.js.map +1 -0
  99. package/dist/{state-C59Elrnt.js → state-Do7ZJYkF.js} +49 -4
  100. package/dist/state-Do7ZJYkF.js.map +1 -0
  101. package/dist/status-DxnJTekv.js +352 -0
  102. package/dist/status-DxnJTekv.js.map +1 -0
  103. package/dist/storage-DyVq-f_F.js +303 -0
  104. package/dist/storage-DyVq-f_F.js.map +1 -0
  105. package/dist/telemetry/identity.d.ts +10 -1
  106. package/dist/telemetry-C8Uq8phb.js +129 -0
  107. package/dist/telemetry-C8Uq8phb.js.map +1 -0
  108. package/dist/telemetry-EMFRVRQi.js +351 -0
  109. package/dist/telemetry-EMFRVRQi.js.map +1 -0
  110. package/dist/unknown-command-DNwDcX3w.js +98 -0
  111. package/dist/unknown-command-DNwDcX3w.js.map +1 -0
  112. package/dist/upgrade-CtZtRkTE.js +1332 -0
  113. package/dist/upgrade-CtZtRkTE.js.map +1 -0
  114. package/dist/upgrade.d.ts +24 -3
  115. package/dist/utils/dev-preflight.d.ts +47 -5
  116. package/dist/utils/dev-server.d.ts +30 -0
  117. package/dist/utils/path-flags.d.ts +15 -2
  118. package/dist/utils/project.d.ts +16 -0
  119. package/dist/utils/temp-archive.d.ts +9 -0
  120. package/dist/{version-DsgBmP22.js → version-Buyyo1ut.js} +2 -2
  121. package/dist/{version-DsgBmP22.js.map → version-Buyyo1ut.js.map} +1 -1
  122. package/package.json +7 -7
  123. package/templates/eject/Dockerfile +4 -2
  124. package/templates/eject/backend/src/env.ts +1 -1
  125. package/templates/eject/backend/src/index.ts +29 -4
  126. package/templates/eject/docker-compose.custom.yml +8 -4
  127. package/templates/generated-schemas/blank.ts +40 -0
  128. package/templates/generated-schemas/blog.ts +127 -0
  129. package/templates/generated-schemas/ecommerce.ts +93 -0
  130. package/templates/overlays/baas/config/package.json +1 -0
  131. package/templates/overlays/baas/package.json +1 -0
  132. package/templates/template/.env.example +10 -4
  133. package/templates/template/README.md +11 -4
  134. package/templates/template/ai-instructions.md +3 -2
  135. package/templates/template/config/resources.ts +7 -6
  136. package/templates/template/docker-compose.yml +9 -3
  137. package/templates/template/frontend/vite.config.ts +13 -3
  138. package/templates/template/package.json +1 -0
  139. package/templates/template/scripts/example.ts +2 -1
  140. package/templates/template/tsconfig.json +25 -0
  141. package/dist/branch-pointer-kk9F5FBO.js +0 -111
  142. package/dist/branch-pointer-kk9F5FBO.js.map +0 -1
  143. package/dist/constraints-DKGbNfUW.js +0 -48
  144. package/dist/constraints-DKGbNfUW.js.map +0 -1
  145. package/dist/daemon-C8p32i-n.js +0 -357
  146. package/dist/daemon-C8p32i-n.js.map +0 -1
  147. package/dist/daemon-entry-LTFKtpHy.js.map +0 -1
  148. package/dist/dev-preflight-CNLl4rdo.js +0 -367
  149. package/dist/dev-preflight-CNLl4rdo.js.map +0 -1
  150. package/dist/project-DmaGLmH2.js.map +0 -1
  151. package/dist/pull-Dywm5FWP.js +0 -232
  152. package/dist/pull-Dywm5FWP.js.map +0 -1
  153. package/dist/resolve-BAeRUBhg.js +0 -74
  154. package/dist/resolve-BAeRUBhg.js.map +0 -1
  155. package/dist/state-C59Elrnt.js.map +0 -1
  156. package/templates/template/backend/src/schema.generated.ts +0 -13
@@ -0,0 +1,1621 @@
1
+ import { g as resolveTsx, h as resolvePluginCliScript, l as getActiveBackendPlugin, m as resolveLocalBin } from "./project-Wp2zoyeN.js";
2
+ import { n as cliVersion } from "./version-Buyyo1ut.js";
3
+ import { g as resolveBackendPaths, u as compareMountOrder } from "./manifest-DRp0UVTj.js";
4
+ import { n as summarisePortability, t as analyseFunctionsDirectory } from "./function-portability-BIvlvR9q.js";
5
+ import chalk from "chalk";
6
+ import fs from "fs";
7
+ import path from "path";
8
+ import { createRequire } from "module";
9
+ import { spawnSync } from "child_process";
10
+ import { BUNDLE_FORMAT_VERSION, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, findStorageSuffixCollision, parseEnvBoolean } from "@rebasepro/types";
11
+ import zlib from "zlib";
12
+ //#region src/utils/tool-stdio.ts
13
+ /**
14
+ * The stdio a build tool the CLI runs in the foreground gets: a static app's
15
+ * build command, `tsc`, `tar`.
16
+ *
17
+ * Inherited, so its output streams as it happens. With `quietStdout` its stdout
18
+ * goes to stderr instead, for a command whose own stdout carries a result:
19
+ * `rebase cloud deploy` in JSON mode — every piped run — owes stdout exactly one
20
+ * JSON value, and a `vite v6 building…` line in front of it is a result no
21
+ * parser can read. The tool's output still reaches the person watching.
22
+ */
23
+ function toolStdio(quietStdout) {
24
+ return quietStdout ? [
25
+ "ignore",
26
+ 2,
27
+ "inherit"
28
+ ] : "inherit";
29
+ }
30
+ //#endregion
31
+ //#region src/precompress.ts
32
+ /**
33
+ * `.br` and `.gz` beside a static build's content-hashed assets, written when
34
+ * the build is folded into a bundle.
35
+ *
36
+ * The runtime serves a sibling when the browser accepts its encoding
37
+ * (`serveSPA` turns `precompressed` on at mount when the build holds any), so a
38
+ * hashed chunk costs no compression CPU at all, on any request — including the
39
+ * first one after a deploy, which otherwise gzips every chunk once per process
40
+ * — and the browsers that accept brotli get it. Without siblings the runtime
41
+ * only ever sends gzip.
42
+ *
43
+ * Which files: the ones the runtime caches as `immutable` — a content hash in
44
+ * the name, by the same rule as the server's `isContentHashed` — of a type worth
45
+ * compressing, and 1 KB or more (the runtime's own compression threshold). An
46
+ * unhashed file (`index.html`) is left to on-the-fly compression, where a
47
+ * rewrite of it can never be served stale from a sibling. A sibling is written
48
+ * only when it is smaller, and never over one the build already emitted.
49
+ *
50
+ * Brotli at quality 9, not 11. On the example app's 5.6 MB of JavaScript,
51
+ * quality 11 is 1.30 MB and took 20 s (8.7 s across the thread pool); quality
52
+ * 9 is 1.41 MB in 0.9 s. gzip at level 9 is 1.57 MB in 0.3 s. A deploy runs
53
+ * this every time, and a CI runner has fewer cores than a laptop.
54
+ */
55
+ /** Below this a compressed response saves less than its headers cost — the runtime's threshold. */
56
+ var MIN_BYTES = 1024;
57
+ /**
58
+ * Extensions whose content type the runtime compresses (`COMPRESSIBLE_CONTENT_TYPE_REGEX`).
59
+ * All of them, because a build with any sibling switches the runtime's cache of
60
+ * gzipped hashed files off: a hashed type left out here would be compressed on
61
+ * every request instead of once.
62
+ */
63
+ var COMPRESSIBLE = /* @__PURE__ */ new Set([
64
+ "js",
65
+ "mjs",
66
+ "cjs",
67
+ "css",
68
+ "html",
69
+ "htm",
70
+ "txt",
71
+ "csv",
72
+ "md",
73
+ "xml",
74
+ "json",
75
+ "webmanifest",
76
+ "svg",
77
+ "wasm",
78
+ "ttf",
79
+ "otf",
80
+ "eot",
81
+ "ico",
82
+ "bmp"
83
+ ]);
84
+ /**
85
+ * A content hash in the name, by the runtime's rule (`isContentHashed` in the
86
+ * server's `serve-spa.ts`): 8+ base64url characters after the last `-` with a
87
+ * digit or a capital in them. `index-vgugiqRO.js` is; `vendor-analytics.js` is
88
+ * not.
89
+ */
90
+ function isContentHashedName(fileName) {
91
+ const dot = fileName.lastIndexOf(".");
92
+ if (dot <= 0) return false;
93
+ const stem = fileName.slice(0, dot);
94
+ const dash = stem.lastIndexOf("-");
95
+ if (dash < 0) return false;
96
+ const suffix = stem.slice(dash + 1);
97
+ return suffix.length >= 8 && /^[A-Za-z0-9_-]+$/.test(suffix) && /[0-9A-Z]/.test(suffix);
98
+ }
99
+ /** Write `.br` and `.gz` siblings for every hashed, compressible asset under `dir`. */
100
+ function precompressStaticAssets(dir) {
101
+ const outcome = {
102
+ files: 0,
103
+ bytes: 0,
104
+ brotliBytes: 0,
105
+ gzipBytes: 0
106
+ };
107
+ const pending = [dir];
108
+ while (pending.length > 0) {
109
+ const current = pending.pop();
110
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
111
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
112
+ const file = path.join(current, entry.name);
113
+ if (entry.isDirectory()) {
114
+ pending.push(file);
115
+ continue;
116
+ }
117
+ if (!entry.isFile() || !worthCompressing(entry.name)) continue;
118
+ const source = fs.readFileSync(file);
119
+ if (source.byteLength < MIN_BYTES) continue;
120
+ const brotli = writeSibling(`${file}.br`, source, () => zlib.brotliCompressSync(source, { params: {
121
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 9,
122
+ [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
123
+ [zlib.constants.BROTLI_PARAM_SIZE_HINT]: source.byteLength
124
+ } }));
125
+ const gzip = writeSibling(`${file}.gz`, source, () => zlib.gzipSync(source, { level: 9 }));
126
+ if (brotli === 0 && gzip === 0) continue;
127
+ outcome.files++;
128
+ outcome.bytes += source.byteLength;
129
+ outcome.brotliBytes += brotli;
130
+ outcome.gzipBytes += gzip;
131
+ }
132
+ }
133
+ return outcome;
134
+ }
135
+ function worthCompressing(fileName) {
136
+ const extension = fileName.slice(fileName.lastIndexOf(".") + 1).toLowerCase();
137
+ return COMPRESSIBLE.has(extension) && isContentHashedName(fileName);
138
+ }
139
+ /** The sibling's size, or 0 when there already was one or it would not be smaller. */
140
+ function writeSibling(target, source, compress) {
141
+ if (fs.existsSync(target)) return 0;
142
+ const compressed = compress();
143
+ if (compressed.byteLength >= source.byteLength) return 0;
144
+ fs.writeFileSync(target, compressed);
145
+ return compressed.byteLength;
146
+ }
147
+ //#endregion
148
+ //#region src/bundle.ts
149
+ /**
150
+ * Building a project bundle.
151
+ *
152
+ * A bundle is the deployable form of a project: compiled collections, functions,
153
+ * crons and schema, plus a generated manifest describing exactly what it needs
154
+ * to run. It contains no Dockerfile and no repository — the runtime is supplied
155
+ * separately, which is what allows a project to be moved onto a patched runtime
156
+ * without being rebuilt.
157
+ *
158
+ * Compilation runs through a generated tsconfig rooted at the project directory,
159
+ * so the output mirrors the source layout (`config/…`, `backend/functions/…`)
160
+ * and every path in the manifest is predictable. Letting each workspace package
161
+ * emit into its own `dist/` would have meant guessing at three different
162
+ * layouts, since `rootDir` differs between the template flavours.
163
+ */
164
+ var DEFAULT_BUNDLE_DIR = "dist-bundle";
165
+ /** Packages whose presence means the bundle cannot run on a stock runtime image. */
166
+ var KNOWN_NATIVE_PACKAGES = /* @__PURE__ */ new Set([
167
+ "sharp",
168
+ "canvas",
169
+ "bcrypt",
170
+ "argon2",
171
+ "node-sass",
172
+ "sqlite3",
173
+ "better-sqlite3",
174
+ "grpc",
175
+ "@grpc/grpc-js-native",
176
+ "re2",
177
+ "sodium-native",
178
+ "libpq",
179
+ "pg-native"
180
+ ]);
181
+ /** Dependencies supplied by the runtime image itself, not by the bundle. */
182
+ var RUNTIME_PROVIDED = /* @__PURE__ */ new Set([
183
+ "@rebasepro/server",
184
+ "@rebasepro/types",
185
+ "@rebasepro/client",
186
+ "@rebasepro/common",
187
+ "@rebasepro/utils",
188
+ "hono",
189
+ "@hono/node-server",
190
+ "typescript",
191
+ "tsx",
192
+ "zod"
193
+ ]);
194
+ function log(options, message) {
195
+ (options.log ?? ((m) => console.log(m)))(message);
196
+ }
197
+ /**
198
+ * A line the build says to the person running it — a warning, or what it left
199
+ * out — which progress callbacks may drop and this does not: stdout, or stderr
200
+ * when stdout carries the caller's result.
201
+ */
202
+ function say(options, line) {
203
+ if (options.quietStdout) console.error(line);
204
+ else console.log(line);
205
+ }
206
+ /**
207
+ * Every `node_modules/@types` directory the project can see.
208
+ *
209
+ * Type roots normally resolve by walking up from the tsconfig's own directory,
210
+ * which breaks here for two reasons: the generated config lives in `.rebase/`,
211
+ * and a pnpm workspace puts `@types/node` inside the *package* that depends on
212
+ * it (`config/node_modules/@types`) rather than at the project root. Listing them
213
+ * explicitly, as absolute paths, sidesteps both.
214
+ */
215
+ function discoverTypeRoots(projectRoot) {
216
+ const candidates = [];
217
+ for (const relative of [
218
+ ".",
219
+ "config",
220
+ "backend",
221
+ "frontend"
222
+ ]) candidates.push(path.join(projectRoot, relative, "node_modules", "@types"));
223
+ let dir = projectRoot;
224
+ for (let i = 0; i < 4; i++) {
225
+ const parent = path.dirname(dir);
226
+ if (parent === dir) break;
227
+ candidates.push(path.join(parent, "node_modules", "@types"));
228
+ dir = parent;
229
+ }
230
+ return candidates.filter((candidate) => fs.existsSync(candidate));
231
+ }
232
+ /**
233
+ * Read a tsconfig's own `compilerOptions`.
234
+ *
235
+ * Parsed with the project's own TypeScript, because a tsconfig is not JSON: it
236
+ * permits comments and trailing commas. Hand-rolled comment stripping gets this
237
+ * wrong in a way that is easy to miss — a `paths` entry like
238
+ * `"@acme/types/*": ["src/*"]` contains the character sequence that opens a
239
+ * block comment, so a regex happily eats the rest of the file and the result
240
+ * parses as *something*, just not the config the developer wrote.
241
+ *
242
+ * One level only, and only `paths` is used from it.
243
+ */
244
+ async function readCompilerOptions(projectRoot, file) {
245
+ if (!fs.existsSync(file)) return void 0;
246
+ const text = fs.readFileSync(file, "utf8");
247
+ try {
248
+ const { config } = createRequire(path.join(projectRoot, "package.json"))("typescript").parseConfigFileTextToJson(file, text);
249
+ return config?.compilerOptions;
250
+ } catch {
251
+ try {
252
+ return JSON.parse(text.replace(/^\s*\/\/.*$/gm, "")).compilerOptions;
253
+ } catch {
254
+ return;
255
+ }
256
+ }
257
+ }
258
+ /**
259
+ * Drop path aliases that resolve outside the project.
260
+ *
261
+ * A monorepo commonly aliases its workspace packages to their **source**
262
+ * (`"@acme/types": ["packages/types/src/index.ts"]`) so editors jump to real
263
+ * files. That is right for developing the monorepo and wrong for building a
264
+ * bundle: it drags foreign `.ts` files into the program, none of which are under
265
+ * the project's `rootDir`, and the compile fails on files the developer never
266
+ * asked to build.
267
+ *
268
+ * A bundle is built against *installed packages*. Aliases pointing inside the
269
+ * project are kept, because those are the project's own code.
270
+ */
271
+ function filterProjectPaths(baseDir, projectRoot, paths, baseUrl) {
272
+ const kept = {};
273
+ const dropped = [];
274
+ for (const [alias, targets] of Object.entries(paths)) {
275
+ if (!Array.isArray(targets)) continue;
276
+ const resolved = targets.map((target) => path.resolve(baseUrl, target));
277
+ if (resolved.every((target) => {
278
+ const relative = path.relative(projectRoot, target);
279
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
280
+ })) kept[alias] = resolved.map((target) => {
281
+ return path.relative(baseDir, target).split(path.sep).join("/");
282
+ });
283
+ else dropped.push(alias);
284
+ }
285
+ return {
286
+ kept,
287
+ dropped
288
+ };
289
+ }
290
+ /**
291
+ * Compose the tsconfig used to compile the bundle.
292
+ *
293
+ * Extends the config package's own tsconfig when there is one, so the project's
294
+ * choices about target, JSX and strictness are respected. It has to be `extends`
295
+ * rather than a copy of `compilerOptions`: TypeScript resolves relative paths
296
+ * against the file they were written in, so copying a value like
297
+ * `baseUrl: "../../"` into a config in a different directory silently repoints
298
+ * it at the wrong place.
299
+ */
300
+ async function writeBundleTsconfig(projectRoot, outDir, includes, options) {
301
+ const skipTypeCheck = options.skipTypeCheck === true;
302
+ const tsconfigDir = path.join(projectRoot, ".rebase");
303
+ const fromTsconfig = (target) => {
304
+ return path.relative(tsconfigDir, path.resolve(projectRoot, target)).split(path.sep).join("/");
305
+ };
306
+ const configTsconfigPath = path.join(projectRoot, "config", "tsconfig.json");
307
+ const extendsFrom = fs.existsSync(configTsconfigPath) ? fromTsconfig(path.join("config", "tsconfig.json")) : void 0;
308
+ let pathOverrides = {};
309
+ const baseOptions = await readCompilerOptions(projectRoot, configTsconfigPath);
310
+ if (baseOptions?.paths && typeof baseOptions.paths === "object") {
311
+ const baseDir = path.dirname(configTsconfigPath);
312
+ const baseUrl = path.resolve(baseDir, typeof baseOptions.baseUrl === "string" ? baseOptions.baseUrl : ".");
313
+ const { kept, dropped } = filterProjectPaths(tsconfigDir, projectRoot, baseOptions.paths, baseUrl);
314
+ pathOverrides = {
315
+ baseUrl: fromTsconfig("."),
316
+ paths: kept
317
+ };
318
+ if (dropped.length > 0) say(options, chalk.dim(` ignoring ${dropped.length} path alias(es) pointing outside the project (${dropped.join(", ")}) — resolving those from node_modules instead`));
319
+ }
320
+ const compilerOptions = {
321
+ target: "ES2022",
322
+ module: "ESNext",
323
+ moduleResolution: "bundler",
324
+ lib: ["ES2022"],
325
+ jsx: "react-jsx",
326
+ allowSyntheticDefaultImports: true,
327
+ esModuleInterop: true,
328
+ resolveJsonModule: true,
329
+ forceConsistentCasingInFileNames: true,
330
+ rootDir: fromTsconfig("."),
331
+ outDir: fromTsconfig(path.relative(projectRoot, outDir) || "."),
332
+ typeRoots: discoverTypeRoots(projectRoot),
333
+ ...pathOverrides,
334
+ declaration: false,
335
+ declarationMap: false,
336
+ sourceMap: true,
337
+ noEmit: false,
338
+ skipLibCheck: true,
339
+ allowJs: true,
340
+ ...skipTypeCheck ? { noCheck: true } : {}
341
+ };
342
+ const tsconfig = {
343
+ ...extendsFrom ? { extends: extendsFrom } : {},
344
+ compilerOptions,
345
+ include: includes.map(fromTsconfig),
346
+ exclude: [
347
+ "node_modules",
348
+ "**/*.test.ts",
349
+ "**/*.spec.ts",
350
+ "**/dist/**",
351
+ DEFAULT_BUNDLE_DIR
352
+ ].map((pattern) => pattern.startsWith("**") ? pattern : fromTsconfig(pattern))
353
+ };
354
+ fs.mkdirSync(tsconfigDir, { recursive: true });
355
+ const tsconfigPath = path.join(tsconfigDir, "tsconfig.bundle.json");
356
+ fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2), "utf8");
357
+ return tsconfigPath;
358
+ }
359
+ /**
360
+ * Whether the compiled config package exports a `storageAuthorize` hook.
361
+ *
362
+ * Recorded in the manifest so a host can refuse a deploy that would enable file
363
+ * storage with no access model, rather than let the runtime's boot guard turn it
364
+ * into a crash loop the developer cannot read.
365
+ *
366
+ * Read from the *compiled* index, deliberately: that is the exact module the
367
+ * runtime imports and reads the export off, so this cannot disagree with what
368
+ * actually happens at boot. It is a textual check rather than an import because
369
+ * a freshly built bundle cannot resolve its own dependencies until it is
370
+ * deployed — the same reason schema hashing reads source.
371
+ *
372
+ * Errs toward `false`: a missed detection costs a deploy rejection whose message
373
+ * says exactly how to proceed, while a false positive would hand back the crash
374
+ * loop this exists to prevent.
375
+ */
376
+ function detectStorageAuthorize(compiledConfigDir, depth = 0) {
377
+ const indexPath = [
378
+ ".js",
379
+ ".mjs",
380
+ ".ts"
381
+ ].map((ext) => path.join(compiledConfigDir, `index${ext}`)).find((candidate) => fs.existsSync(candidate));
382
+ if (!indexPath) return false;
383
+ return moduleExportsStorageAuthorize(indexPath, depth);
384
+ }
385
+ /**
386
+ * Whether one compiled module re-exports or defines `storageAuthorize`.
387
+ *
388
+ * Split out from {@link detectStorageAuthorize} so a wildcard re-export can be
389
+ * followed. `export * from "./storage.js"` is an ordinary way to write a config
390
+ * barrel, and treating it as "no hook" rejected deploys that were correct — with
391
+ * a message telling the developer to add a hook they had already written.
392
+ */
393
+ function moduleExportsStorageAuthorize(modulePath, depth) {
394
+ let source;
395
+ try {
396
+ source = fs.readFileSync(modulePath, "utf8");
397
+ } catch {
398
+ return false;
399
+ }
400
+ if (/\bexport\s+(?:async\s+)?(?:const|let|var|function)\s+storageAuthorize\b/.test(source)) return true;
401
+ for (const clause of source.matchAll(/\bexport\s*\{([^}]*)\}/g)) if (clause[1].split(",").map((entry) => {
402
+ const parts = entry.split(/\bas\b/);
403
+ return parts[parts.length - 1].trim();
404
+ }).includes("storageAuthorize")) return true;
405
+ if (depth < 3) for (const clause of source.matchAll(/\bexport\s*\*\s*from\s*["']([^"']+)["']/g)) {
406
+ const specifier = clause[1];
407
+ if (!specifier.startsWith(".")) continue;
408
+ const resolved = resolveRelativeModule(path.dirname(modulePath), specifier);
409
+ if (resolved && moduleExportsStorageAuthorize(resolved, depth + 1)) return true;
410
+ }
411
+ return false;
412
+ }
413
+ /**
414
+ * Resolve a relative ESM specifier to a file on disk.
415
+ *
416
+ * Compiled output carries explicit `.js` extensions, but the same function reads
417
+ * TypeScript sources during a source boot, where the specifier may be
418
+ * extensionless or point at a directory index.
419
+ */
420
+ function resolveRelativeModule(fromDir, specifier) {
421
+ const base = path.resolve(fromDir, specifier);
422
+ const candidates = [
423
+ base,
424
+ `${base}.js`,
425
+ `${base}.mjs`,
426
+ `${base}.ts`,
427
+ path.join(base, "index.js"),
428
+ path.join(base, "index.mjs"),
429
+ path.join(base, "index.ts")
430
+ ];
431
+ for (const candidate of candidates) try {
432
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
433
+ } catch {
434
+ continue;
435
+ }
436
+ if (/\.js$/.test(base)) {
437
+ const asTs = base.replace(/\.js$/, ".ts");
438
+ try {
439
+ if (fs.existsSync(asTs) && fs.statSync(asTs).isFile()) return asTs;
440
+ } catch {
441
+ return null;
442
+ }
443
+ }
444
+ return null;
445
+ }
446
+ /**
447
+ * Detect native code in the dependency closure.
448
+ *
449
+ * Walks declared runtime dependencies breadth-first through `node_modules`,
450
+ * flagging anything with a `binding.gyp`, a prebuilt `.node` binary, or an
451
+ * install script that builds one. The managed runtime cannot run these: a
452
+ * binary compiled for one image will not load in another, and finding that out
453
+ * at deploy time is far better than in a crash loop.
454
+ *
455
+ * The walk is bounded. A dependency graph can be enormous, and this is a
456
+ * heuristic gate whose false negatives are caught at deploy time anyway.
457
+ */
458
+ function detectNativeDependencies(projectRoot, declared, limit = 2e3) {
459
+ const found = [];
460
+ const seen = /* @__PURE__ */ new Set();
461
+ const queue = Object.keys(declared);
462
+ let visited = 0;
463
+ const searchRoots = [
464
+ path.join(projectRoot, "node_modules"),
465
+ path.join(projectRoot, "backend", "node_modules"),
466
+ path.join(projectRoot, "config", "node_modules")
467
+ ].filter((dir) => fs.existsSync(dir));
468
+ while (queue.length > 0 && visited < limit) {
469
+ const name = queue.shift();
470
+ if (seen.has(name)) continue;
471
+ seen.add(name);
472
+ visited++;
473
+ if (KNOWN_NATIVE_PACKAGES.has(name)) {
474
+ found.push({
475
+ name,
476
+ reason: "known native module"
477
+ });
478
+ continue;
479
+ }
480
+ const packageDir = searchRoots.map((root) => path.join(root, ...name.split("/"))).find((dir) => fs.existsSync(path.join(dir, "package.json")));
481
+ if (!packageDir) continue;
482
+ let pkg;
483
+ try {
484
+ pkg = JSON.parse(fs.readFileSync(path.join(packageDir, "package.json"), "utf8"));
485
+ } catch {
486
+ continue;
487
+ }
488
+ if (pkg.gypfile || fs.existsSync(path.join(packageDir, "binding.gyp"))) {
489
+ found.push({
490
+ name,
491
+ reason: "builds a native addon (binding.gyp)"
492
+ });
493
+ continue;
494
+ }
495
+ const install = `${pkg.scripts?.install ?? ""} ${pkg.scripts?.preinstall ?? ""} ${pkg.scripts?.postinstall ?? ""}`;
496
+ if (/node-gyp|prebuild|node-pre-gyp|cmake-js/.test(install)) {
497
+ found.push({
498
+ name,
499
+ reason: "install script compiles native code"
500
+ });
501
+ continue;
502
+ }
503
+ if (hasNodeBinary(packageDir)) {
504
+ found.push({
505
+ name,
506
+ reason: "ships a prebuilt .node binary"
507
+ });
508
+ continue;
509
+ }
510
+ for (const dep of Object.keys(pkg.dependencies ?? {})) if (!seen.has(dep)) queue.push(dep);
511
+ }
512
+ return found;
513
+ }
514
+ /** Shallow scan for `.node` binaries — deep enough for the usual `build/Release`. */
515
+ function hasNodeBinary(dir, depth = 0) {
516
+ if (depth > 3) return false;
517
+ let entries;
518
+ try {
519
+ entries = fs.readdirSync(dir, { withFileTypes: true });
520
+ } catch {
521
+ return false;
522
+ }
523
+ for (const entry of entries) {
524
+ if (entry.isFile() && entry.name.endsWith(".node")) return true;
525
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".bin") {
526
+ if (hasNodeBinary(path.join(dir, entry.name), depth + 1)) return true;
527
+ }
528
+ }
529
+ return false;
530
+ }
531
+ /**
532
+ * Whether a dependency name resolves to a package *inside this repository* — a
533
+ * workspace package rather than a registry one.
534
+ *
535
+ * The bundle's declared deps are installed with `npm install` from the public
536
+ * registry beside the bundle at boot. A workspace package is not there, so
537
+ * declaring it guarantees a boot-time install failure. The most common case is
538
+ * the standard `config` package: the backend depends on it by name, but it is
539
+ * *carried in the bundle* (as `entry.config`), so it must never also be an npm
540
+ * dependency. Projects often express this as a `workspace:` range — caught
541
+ * separately — but a plain `"*"` against a workspace symlink is just as common
542
+ * and looks like a registry range, so the symlink is what actually settles it.
543
+ *
544
+ * Detection: the installed `node_modules/<name>` is a symlink whose real path is
545
+ * inside the project and not within a pnpm virtual store (`.pnpm`). That is
546
+ * exactly a workspace link and nothing else.
547
+ */
548
+ function resolvesToWorkspacePackage(projectRoot, name) {
549
+ let realRoot;
550
+ try {
551
+ realRoot = fs.realpathSync(projectRoot);
552
+ } catch {
553
+ realRoot = projectRoot;
554
+ }
555
+ for (const base of [
556
+ projectRoot,
557
+ path.join(projectRoot, "backend"),
558
+ path.join(projectRoot, "config")
559
+ ]) {
560
+ const link = path.join(base, "node_modules", name);
561
+ try {
562
+ if (!fs.lstatSync(link).isSymbolicLink()) continue;
563
+ const real = fs.realpathSync(link);
564
+ const insideRepo = real.startsWith(realRoot + path.sep);
565
+ const inStore = real.includes(`${path.sep}.pnpm${path.sep}`) || real.includes(`${path.sep}node_modules${path.sep}`);
566
+ if (insideRepo && !inStore) return true;
567
+ } catch {}
568
+ }
569
+ return false;
570
+ }
571
+ /**
572
+ * The manifests a bundle's declared dependencies come from, least specific first.
573
+ *
574
+ * Order is the whole point. `collectDeclaredDependencies` lets a later manifest
575
+ * win, and the app — `backend/` — is the one whose code was written and
576
+ * typechecked against the range it names. The project root is a workspace root:
577
+ * it exists to hold scripts and the tooling every workspace shares, and its
578
+ * ranges were never exercised by the code that ends up in the bundle. It used to
579
+ * be read last, so a scaffold declaring `dotenv ^16` at the root and `^17.4.2`
580
+ * in the backend shipped a manifest telling the managed runtime to install 16.
581
+ */
582
+ var BUNDLE_DEP_MANIFESTS = [
583
+ "package.json",
584
+ "config/package.json",
585
+ "backend/package.json"
586
+ ];
587
+ /**
588
+ * Specifiers the registry the managed runtime installs from cannot resolve.
589
+ *
590
+ * `workspace:` is handled separately (it is normal and expected inside this
591
+ * repo's own projects); everything here is a specifier that works on the
592
+ * developer's disk and nowhere else. The runtime installs `deps.declared` from
593
+ * npm with no access to the machine the bundle was built on, so a `file:` or a
594
+ * `git+` range is a deploy-time install failure — the worst place to find out.
595
+ */
596
+ var NON_REGISTRY_PROTOCOLS = [
597
+ "file:",
598
+ "link:",
599
+ "portal:",
600
+ "git+",
601
+ "git:",
602
+ "github:"
603
+ ];
604
+ /** Whether a range names something npm could not install from the registry. */
605
+ function nonRegistrySpecifier(version) {
606
+ const trimmed = version.trim();
607
+ for (const protocol of NON_REGISTRY_PROTOCOLS) if (trimmed.startsWith(protocol)) return protocol;
608
+ if (/^\.{0,2}\//.test(trimmed)) return "a filesystem path";
609
+ return null;
610
+ }
611
+ /**
612
+ * Collect the runtime dependencies a bundle needs installed beside it.
613
+ *
614
+ * Packages the runtime image already provides are excluded — reinstalling a
615
+ * second copy of the server next to the one running the process is at best
616
+ * wasted space and at worst a version conflict. Workspace packages are excluded
617
+ * too: they are not on the registry the runtime installs from, and the project's
618
+ * own config package already travels inside the bundle.
619
+ *
620
+ * Throws when a declared range is not something the registry can resolve, or
621
+ * when two manifests declare a name at ranges nothing satisfies. The
622
+ * alternative is a bundle that builds green and fails at the deploy's install
623
+ * step, minutes later and on a machine the developer cannot see. `rebase build`
624
+ * reports both in its own words first; this is the backstop for every other
625
+ * caller, `rebase cloud deploy` included.
626
+ */
627
+ function collectDeclaredDependencies(projectRoot) {
628
+ const declared = {};
629
+ const conflicts = detectDeclaredDepConflicts(projectRoot);
630
+ if (conflicts.length > 0) {
631
+ const described = conflicts.map((c) => {
632
+ const [a, b] = c.declarations;
633
+ return `${c.name}: ${a.range} (${a.file}) vs ${b.range} (${b.file})`;
634
+ }).join("; ");
635
+ throw new Error(`No single version satisfies every range this project declares — ${described}. The bundle installs one of them and the rest of the project was built against the other. Declare one range, in the app that uses it.`);
636
+ }
637
+ for (const relative of BUNDLE_DEP_MANIFESTS) {
638
+ const file = path.join(projectRoot, relative);
639
+ if (!fs.existsSync(file)) continue;
640
+ let pkg;
641
+ try {
642
+ pkg = JSON.parse(fs.readFileSync(file, "utf8"));
643
+ } catch {
644
+ continue;
645
+ }
646
+ for (const [name, version] of Object.entries(pkg.dependencies ?? {})) {
647
+ if (RUNTIME_PROVIDED.has(name)) continue;
648
+ if (typeof version === "string" && version.startsWith("workspace:")) continue;
649
+ if (resolvesToWorkspacePackage(projectRoot, name)) continue;
650
+ const protocol = typeof version === "string" ? nonRegistrySpecifier(version) : null;
651
+ if (protocol && parseEnvBoolean(process.env.REBASE_E2E) === true && protocol === "file:") {
652
+ declared[name] = version;
653
+ continue;
654
+ }
655
+ if (protocol) throw new Error(`${relative} declares "${name}": "${version}", which the managed runtime cannot install — ${protocol} resolves only on this machine. Publish the package to a registry and depend on it by version, or vendor its source into the backend.`);
656
+ declared[name] = version;
657
+ }
658
+ }
659
+ return declared;
660
+ }
661
+ /**
662
+ * Lowest version a range could resolve to, or null if it is not a range.
663
+ *
664
+ * Kept deliberately tiny and local. The published range grammar here is a caret,
665
+ * a tilde, an exact version or a `>=` floor, and the alternative — a semver
666
+ * dependency in the CLI — buys breadth this does not need.
667
+ */
668
+ function lowerBoundOf(range) {
669
+ const raw = range.trim().replace(/^[\^~]/, "").replace(/^>=\s*/, "").replace(/^v/, "");
670
+ if (!/^\d+(\.\d+){0,2}$/.test(raw)) return null;
671
+ const [major, minor = 0, patch = 0] = raw.split(".").map(Number);
672
+ return [
673
+ major,
674
+ minor,
675
+ patch
676
+ ];
677
+ }
678
+ /** Highest version a range could resolve to (exclusive), or null. */
679
+ function upperBoundOf(range) {
680
+ const trimmed = range.trim();
681
+ const min = lowerBoundOf(trimmed);
682
+ if (!min) return null;
683
+ if (trimmed.startsWith(">=")) return null;
684
+ const [major, minor, patch] = min;
685
+ if (trimmed.startsWith("^")) {
686
+ if (major > 0) return [
687
+ major + 1,
688
+ 0,
689
+ 0
690
+ ];
691
+ if (minor > 0) return [
692
+ 0,
693
+ minor + 1,
694
+ 0
695
+ ];
696
+ return [
697
+ 0,
698
+ 0,
699
+ patch + 1
700
+ ];
701
+ }
702
+ if (trimmed.startsWith("~")) return trimmed.replace(/^~v?/, "").split(".").length >= 2 ? [
703
+ major,
704
+ minor + 1,
705
+ 0
706
+ ] : [
707
+ major + 1,
708
+ 0,
709
+ 0
710
+ ];
711
+ const parts = trimmed.replace(/^v/, "").split(".").length;
712
+ if (parts === 1) return [
713
+ major + 1,
714
+ 0,
715
+ 0
716
+ ];
717
+ if (parts === 2) return [
718
+ major,
719
+ minor + 1,
720
+ 0
721
+ ];
722
+ return [
723
+ major,
724
+ minor,
725
+ patch + 1
726
+ ];
727
+ }
728
+ function compareTriples(a, b) {
729
+ for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
730
+ return 0;
731
+ }
732
+ /**
733
+ * Whether a declared range could EVER resolve at or above `target`.
734
+ *
735
+ * The same question the control plane asks at intake, asked here first. Only a
736
+ * range whose entire span sits below the target is reported — `^0.10.0` can
737
+ * never cross to 0.12 whatever npm publishes — because a false alarm on a build
738
+ * that would have worked trains people to ignore the warning that matters.
739
+ */
740
+ function canReach(range, target) {
741
+ const ceiling = upperBoundOf(range);
742
+ const floor = lowerBoundOf(target);
743
+ if (!floor || !lowerBoundOf(range)) return null;
744
+ if (!ceiling) return true;
745
+ return compareTriples(ceiling, floor) > 0;
746
+ }
747
+ /**
748
+ * Whether no single published version can satisfy both ranges.
749
+ *
750
+ * Only a provable disagreement counts. Anything this tiny grammar cannot read —
751
+ * a tag, an `||`, a `*` — returns false, because a build refused over a range
752
+ * the CLI merely failed to parse is worse than the drift it was looking for.
753
+ */
754
+ function rangesDisjoint(a, b) {
755
+ const loA = lowerBoundOf(a);
756
+ const loB = lowerBoundOf(b);
757
+ if (!loA || !loB) return false;
758
+ const hiA = upperBoundOf(a);
759
+ const hiB = upperBoundOf(b);
760
+ if (hiA && compareTriples(hiA, loB) <= 0) return true;
761
+ if (hiB && compareTriples(hiB, loA) <= 0) return true;
762
+ return false;
763
+ }
764
+ /**
765
+ * Names two of the bundle's manifests declare at ranges nothing satisfies.
766
+ *
767
+ * `collectDeclaredDependencies` has to pick one — the app's — and the loser is
768
+ * silent: the bundle tells the managed runtime to install a version the other
769
+ * half of the project was never compiled against. The stock scaffold shipped
770
+ * exactly that (`dotenv ^16` at the root, `^17.4.2` in `backend/`), so every
771
+ * bundle built from a default project asked for dotenv 16.
772
+ *
773
+ * Scoped to the three manifests that feed `deps.declared`, and to their
774
+ * `dependencies`: a `frontend/` that wants a different major of a build tool is
775
+ * not this problem, and `devDependencies` never reach the bundle.
776
+ */
777
+ function detectDeclaredDepConflicts(projectRoot) {
778
+ const seen = /* @__PURE__ */ new Map();
779
+ const conflicts = [];
780
+ for (const relative of BUNDLE_DEP_MANIFESTS) {
781
+ const file = path.join(projectRoot, relative);
782
+ if (!fs.existsSync(file)) continue;
783
+ let pkg;
784
+ try {
785
+ pkg = JSON.parse(fs.readFileSync(file, "utf8"));
786
+ } catch {
787
+ continue;
788
+ }
789
+ for (const [name, range] of Object.entries(pkg.dependencies ?? {})) {
790
+ if (typeof range !== "string") continue;
791
+ if (RUNTIME_PROVIDED.has(name)) continue;
792
+ if (range.startsWith("workspace:")) continue;
793
+ const first = seen.get(name);
794
+ if (!first) {
795
+ seen.set(name, {
796
+ name,
797
+ range,
798
+ file: relative
799
+ });
800
+ continue;
801
+ }
802
+ if (first.range === range) continue;
803
+ if (!rangesDisjoint(first.range, range)) continue;
804
+ conflicts.push({
805
+ name,
806
+ declarations: [first, {
807
+ name,
808
+ range,
809
+ file: relative
810
+ }]
811
+ });
812
+ }
813
+ }
814
+ return conflicts;
815
+ }
816
+ /**
817
+ * Find `@rebasepro/*` dependencies pinned to a version older than this CLI.
818
+ *
819
+ * This is the only place a developer can be told. In development, every
820
+ * `@rebasepro/*` resolves through pnpm's `link:`/`workspace:` overrides to the
821
+ * checkout, so the version STRINGS in package.json are never exercised — the
822
+ * project runs fine locally on whatever is on disk, and the declared numbers are
823
+ * first honoured when the runtime npm-installs them from a bundle in the cloud.
824
+ * A project scaffolded at 0.10.0 therefore keeps working on a developer's
825
+ * machine indefinitely while being, in the cloud, a 0.10.0 driver.
826
+ *
827
+ * That matters because the image supplies only `@rebasepro/server`; the database
828
+ * driver comes from these declarations and a newer runtime never updates it.
829
+ * Every package.json is scanned, `dependencies` and `devDependencies` both,
830
+ * because they have to be bumped together and the one that gets forgotten is the
831
+ * one nobody looks at.
832
+ */
833
+ function detectFrameworkDepDrift(projectRoot, cliVersion) {
834
+ const found = [];
835
+ for (const relative of [
836
+ "package.json",
837
+ "backend/package.json",
838
+ "config/package.json",
839
+ "frontend/package.json"
840
+ ]) {
841
+ const file = path.join(projectRoot, relative);
842
+ if (!fs.existsSync(file)) continue;
843
+ try {
844
+ const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
845
+ for (const block of [pkg.dependencies, pkg.devDependencies]) for (const [name, range] of Object.entries(block ?? {})) {
846
+ if (!name.startsWith("@rebasepro/")) continue;
847
+ if (typeof range !== "string") continue;
848
+ found.push({
849
+ name,
850
+ range,
851
+ file: relative
852
+ });
853
+ }
854
+ } catch {}
855
+ }
856
+ const behind = found.filter((d) => canReach(d.range, cliVersion) === false);
857
+ const bounds = new Set(found.map((d) => lowerBoundOf(d.range)).filter((b) => b != null).map((b) => b.join(".")));
858
+ return {
859
+ behind,
860
+ disagreeing: bounds.size > 1 ? [...bounds].sort() : [],
861
+ conflicting: detectDeclaredDepConflicts(projectRoot)
862
+ };
863
+ }
864
+ /**
865
+ * The `[start, end)` ranges of `//` and block comments in JavaScript source.
866
+ *
867
+ * `normalizeEsmSpecifiers` scans emitted output with a regular expression, and a
868
+ * regular expression cannot tell a specifier from a specifier somebody wrote
869
+ * about. The stock template ships one: `config/resources.ts` documents
870
+ * `import { media } from "../resources";` inside a docblock, tsc preserves the
871
+ * comment, and every `rebase build` of an untouched scaffold printed
872
+ * `⚠ 1 import(s) could not be resolved`. A warning that is wrong on the default
873
+ * project teaches people to ignore the warning that predicts a boot failure.
874
+ *
875
+ * Strings are tracked only so a `//` inside one — every `http://` in the tree —
876
+ * does not open a comment that swallows the rest of the line. A `'` or `"` span
877
+ * ends at the newline as well as at its closing quote: a JavaScript string
878
+ * cannot contain a raw newline, so a quote that reaches one was never a string
879
+ * (a regex literal's, most likely) and the mistake must not run on.
880
+ */
881
+ function commentSpans(source) {
882
+ const spans = [];
883
+ let quote = null;
884
+ let i = 0;
885
+ while (i < source.length) {
886
+ const c = source[i];
887
+ if (quote) {
888
+ if (c === "\\") {
889
+ i += 2;
890
+ continue;
891
+ }
892
+ if (c === quote || c === "\n" && quote !== "`") quote = null;
893
+ i++;
894
+ continue;
895
+ }
896
+ if (c === "\"" || c === "'" || c === "`") {
897
+ quote = c;
898
+ i++;
899
+ continue;
900
+ }
901
+ if (c === "/" && source[i + 1] === "/") {
902
+ const newline = source.indexOf("\n", i);
903
+ const end = newline === -1 ? source.length : newline;
904
+ spans.push([i, end]);
905
+ i = end;
906
+ continue;
907
+ }
908
+ if (c === "/" && source[i + 1] === "*") {
909
+ const close = source.indexOf("*/", i + 2);
910
+ const end = close === -1 ? source.length : close + 2;
911
+ spans.push([i, end]);
912
+ i = end;
913
+ continue;
914
+ }
915
+ i++;
916
+ }
917
+ return spans;
918
+ }
919
+ /**
920
+ * Rewrite relative import specifiers in emitted JavaScript so Node can resolve them.
921
+ *
922
+ * TypeScript deliberately does not touch specifiers: `moduleResolution: "bundler"`
923
+ * lets a project write `from "./posts"` or `from "./collections"`, and TypeScript
924
+ * emits them unchanged on the assumption that a bundler will finish the job.
925
+ * Nothing bundles a Rebase bundle — the runtime imports these files directly with
926
+ * Node's ESM loader, which requires a full path with an extension and refuses
927
+ * directory imports outright.
928
+ *
929
+ * Without this, adopting the bundle would mean asking every project written in
930
+ * the (extremely common) extensionless style to rewrite all of its imports. The
931
+ * rewrite is mechanical and verifiable: only relative specifiers are touched, and
932
+ * only when the target file actually exists on disk.
933
+ *
934
+ * `write: false` computes the same answer and leaves every file as it was —
935
+ * `rebase normalize-imports --check`, which reports and must not repair.
936
+ */
937
+ function normalizeEsmSpecifiers(outDir, options = {}) {
938
+ const write = options.write !== false;
939
+ const unresolved = [];
940
+ let rewritten = 0;
941
+ const SPECIFIER = /(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)(["'])(\.[^"']*)\2/g;
942
+ const walk = (dir) => {
943
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
944
+ const full = path.join(dir, entry.name);
945
+ if (entry.isDirectory()) {
946
+ if (entry.name === "node_modules") continue;
947
+ walk(full);
948
+ } else if (entry.isFile() && entry.name.endsWith(".js")) rewriteFile(full);
949
+ }
950
+ };
951
+ const rewriteFile = (file) => {
952
+ const original = fs.readFileSync(file, "utf8");
953
+ const dir = path.dirname(file);
954
+ const comments = commentSpans(original);
955
+ const inComment = (at) => comments.some(([start, end]) => at >= start && at < end);
956
+ const updated = original.replace(SPECIFIER, (match, prefix, quote, specifier, offset) => {
957
+ if (inComment(offset)) return match;
958
+ if (/\.(js|mjs|cjs|json|node)$/.test(specifier)) return match;
959
+ const target = path.resolve(dir, specifier);
960
+ if (fs.existsSync(`${target}.js`)) {
961
+ rewritten++;
962
+ return `${prefix}${quote}${specifier}.js${quote}`;
963
+ }
964
+ if (fs.existsSync(path.join(target, "index.js"))) {
965
+ rewritten++;
966
+ return `${prefix}${quote}${specifier}${specifier.endsWith("/") ? "index.js" : "/index.js"}${quote}`;
967
+ }
968
+ if (specifier.endsWith(".ts") && fs.existsSync(`${target.slice(0, -3)}.js`)) {
969
+ rewritten++;
970
+ return `${prefix}${quote}${specifier.slice(0, -3)}.js${quote}`;
971
+ }
972
+ unresolved.push(`${path.basename(file)} → ${specifier}`);
973
+ return match;
974
+ });
975
+ if (write && updated !== original) fs.writeFileSync(file, updated, "utf8");
976
+ };
977
+ if (fs.existsSync(outDir)) walk(outDir);
978
+ return {
979
+ rewritten,
980
+ unresolved
981
+ };
982
+ }
983
+ /**
984
+ * Remove a previous build so stale output cannot masquerade as current.
985
+ *
986
+ * The containment check matters because this is a recursive force-delete of a
987
+ * path that came from a command-line flag: `rebase build --out ../..` would
988
+ * otherwise erase the parent of the project. The manifest's own paths are
989
+ * checked the same way; a flag deserves no less.
990
+ */
991
+ function cleanOutDir(projectRoot, outDir) {
992
+ const relative = path.relative(projectRoot, outDir);
993
+ if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Refusing to build into "${outDir}": the output directory must be inside the project.`);
994
+ if (fs.existsSync(outDir)) fs.rmSync(outDir, {
995
+ recursive: true,
996
+ force: true
997
+ });
998
+ fs.mkdirSync(outDir, { recursive: true });
999
+ }
1000
+ /**
1001
+ * Regenerate the Drizzle schema from the collections.
1002
+ *
1003
+ * Delegated to the database driver's own CLI — the same code `rebase schema
1004
+ * generate` runs — so there is one implementation of what a schema is. When no
1005
+ * driver is resolvable the build continues with a warning rather than failing:
1006
+ * a `baas` project has no schema to generate, and a project mid-install should
1007
+ * get a clear message rather than a hard stop.
1008
+ */
1009
+ async function regenerateSchema(projectRoot, configDir, options) {
1010
+ const backendDir = path.join(projectRoot, "backend");
1011
+ if (!fs.existsSync(backendDir)) return;
1012
+ const plugin = getActiveBackendPlugin(backendDir);
1013
+ const script = plugin ? resolvePluginCliScript(backendDir, plugin) : null;
1014
+ if (!script) {
1015
+ log(options, chalk.dim(" (no database driver found — skipping schema generation)"));
1016
+ return;
1017
+ }
1018
+ const runner = script.endsWith(".ts") ? resolveTsx(projectRoot) : "node";
1019
+ if (!runner) {
1020
+ log(options, chalk.dim(" (tsx not installed — skipping schema generation)"));
1021
+ return;
1022
+ }
1023
+ const collectionsPath = path.join("..", configDir, "collections");
1024
+ const { execa } = await import("execa");
1025
+ try {
1026
+ await execa(runner, [
1027
+ script,
1028
+ "schema",
1029
+ "generate",
1030
+ "--collections",
1031
+ collectionsPath
1032
+ ], {
1033
+ cwd: backendDir,
1034
+ stdio: "pipe"
1035
+ });
1036
+ log(options, chalk.dim(" regenerated database schema from collections"));
1037
+ } catch (err) {
1038
+ const detail = err instanceof Error ? err.message : String(err);
1039
+ throw new Error(`Schema generation failed, so the bundle was not written.\n${detail}\nRun \`rebase schema generate\` to see the full output, or pass --skip-schema if the committed schema is deliberately hand-maintained.`);
1040
+ }
1041
+ }
1042
+ /**
1043
+ * A hand-written server entrypoint that a bundle does not use.
1044
+ *
1045
+ * `rebase dev` runs `backend/src/index.ts` whenever a project has one, so for
1046
+ * the whole of local development that file *is* the server and every route
1047
+ * written in it works. A bundle has no entrypoint of its own: the runtime boots
1048
+ * the bundle and mounts what the manifest points at — the config package,
1049
+ * functions, crons and the schema. The file is not compiled, not shipped, and
1050
+ * never imported.
1051
+ *
1052
+ * Nothing said so. A project with custom routes in its entrypoint built clean,
1053
+ * deployed green, and answered 404 on every one of them, with the file still
1054
+ * sitting in the repository looking exactly like the server.
1055
+ *
1056
+ * A project that means to own its server process runs `rebase eject`, which
1057
+ * writes an entrypoint, a Dockerfile and a compose file together and flips the
1058
+ * backend to `runtime: "custom"`. The warning names that route rather than
1059
+ * implying the file is a mistake — but eject writes *its* entrypoint, so the
1060
+ * warning must not read as "eject will keep what you wrote here".
1061
+ */
1062
+ function findUnusedServerEntry(projectRoot, functionsDir) {
1063
+ const found = [path.join("backend", "src", "index.ts"), path.join(path.dirname(functionsDir), "src", "index.ts")].find((candidate) => fs.existsSync(path.join(projectRoot, candidate)));
1064
+ return found ? found.split(path.sep).join("/") : void 0;
1065
+ }
1066
+ /**
1067
+ * The bundle manifest, as a pure function of its inputs.
1068
+ *
1069
+ * Separated from `buildBundle` so the SHAPE a host reads can be pinned by a
1070
+ * test without compiling a project: `tooling/contracts/bundle-manifest.json`
1071
+ * is written from this with fixed inputs, and the control plane's own tests
1072
+ * read that file to prove they consume what the CLI emits. That contract is
1073
+ * what was missing when buckets moved from `storage.sources` into `resources`
1074
+ * and the control plane went on reading the old field for two weeks — every
1075
+ * declared bucket arriving as nothing, with both suites green.
1076
+ */
1077
+ function composeBundleManifest(input) {
1078
+ return {
1079
+ bundleFormat: BUNDLE_FORMAT_VERSION,
1080
+ runtime: {
1081
+ range: input.runtimeRange,
1082
+ builtAgainst: input.builtAgainst,
1083
+ contract: RUNTIME_CONTRACT_VERSION
1084
+ },
1085
+ schemaVersion: input.schemaVersion,
1086
+ app: input.appName,
1087
+ kind: "backend",
1088
+ entry: input.entry,
1089
+ collections: [...input.collectionSlugs].sort(),
1090
+ ...input.functions.length > 0 ? { functions: input.functions } : {},
1091
+ hooks: {
1092
+ native: input.nativeModules.length > 0,
1093
+ nativeModules: input.nativeModules.length > 0 ? input.nativeModules : void 0
1094
+ },
1095
+ storage: { authorize: input.declaresStorageAuthorize },
1096
+ resources: input.resources,
1097
+ deps: { declared: input.declaredDeps },
1098
+ build: input.build
1099
+ };
1100
+ }
1101
+ /**
1102
+ * What a build refusing on {@link detectDeclaredDepConflicts} says: every name,
1103
+ * both ranges and where each is declared.
1104
+ */
1105
+ function describeDepConflicts(conflicts) {
1106
+ const lines = [`${conflicts.length} dependency range(s) no single version satisfies:`];
1107
+ for (const { name, declarations: [first, second] } of conflicts) lines.push(` ${name}`, ` ${first.range} (${first.file})`, ` ${second.range} (${second.file})`);
1108
+ lines.push(" The bundle installs one range; the other half of the project was built against the other.", " Declare one range, in the app that uses it.");
1109
+ return lines.join("\n");
1110
+ }
1111
+ async function buildBundle(options) {
1112
+ const { projectRoot, app, appName } = options;
1113
+ const conflicts = detectDeclaredDepConflicts(projectRoot);
1114
+ if (conflicts.length > 0) throw new Error(describeDepConflicts(conflicts));
1115
+ const paths = resolveBackendPaths(app, projectRoot);
1116
+ const outDir = path.resolve(projectRoot, options.outDir ?? "dist-bundle");
1117
+ const includes = [];
1118
+ const addIfExists = (relative, pattern) => {
1119
+ if (fs.existsSync(path.join(projectRoot, relative))) includes.push(pattern);
1120
+ };
1121
+ if (paths.hasConfig) addIfExists(paths.config, `${paths.config}/**/*.ts`);
1122
+ addIfExists(paths.functions, `${paths.functions}/**/*.ts`);
1123
+ addIfExists(paths.crons, `${paths.crons}/**/*.ts`);
1124
+ if (fs.existsSync(path.join(projectRoot, paths.schema))) includes.push(paths.schema);
1125
+ if (includes.length === 0) throw new Error(`Nothing to build for app "${appName}". Expected a config directory at "${paths.config}" or functions at "${paths.functions}".`);
1126
+ if (paths.hasCollections && options.skipSchema !== true) await regenerateSchema(projectRoot, paths.config, options);
1127
+ const unusedEntry = findUnusedServerEntry(projectRoot, paths.functions);
1128
+ if (unusedEntry) {
1129
+ const parts = [
1130
+ ...paths.hasCollections ? [`${paths.config}/`] : [],
1131
+ `${paths.functions}/`,
1132
+ "the schema"
1133
+ ];
1134
+ const compiled = `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
1135
+ say(options, chalk.yellow(` ⚠ ${unusedEntry} is not the bundle's entry point — it is not compiled or shipped.`));
1136
+ say(options, chalk.dim(` The runtime boots the bundle itself and mounts ${compiled}.`));
1137
+ say(options, chalk.dim(` Routes defined there will not exist once deployed: move them to ${paths.functions}/,`));
1138
+ say(options, chalk.dim(" or run `rebase eject`, which writes an entrypoint of its own and owns"));
1139
+ say(options, chalk.dim(" the image — it does not adopt this file, and will not replace it"));
1140
+ say(options, chalk.dim(" without --force."));
1141
+ }
1142
+ log(options, chalk.dim(` compiling ${includes.length} source group(s) → ${path.relative(projectRoot, outDir)}/`));
1143
+ cleanOutDir(projectRoot, outDir);
1144
+ const tsconfigPath = await writeBundleTsconfig(projectRoot, outDir, includes, options);
1145
+ const tsc = resolveLocalBin(projectRoot, "tsc");
1146
+ if (!tsc) throw new Error("TypeScript is not installed in this project. Run your package manager's install first.");
1147
+ const { execa } = await import("execa");
1148
+ try {
1149
+ await execa(tsc, ["-p", tsconfigPath], {
1150
+ cwd: projectRoot,
1151
+ stdio: toolStdio(options.quietStdout)
1152
+ });
1153
+ } catch {
1154
+ throw new Error("TypeScript compilation failed — the bundle was not written.");
1155
+ }
1156
+ const normalized = normalizeEsmSpecifiers(outDir);
1157
+ if (normalized.rewritten > 0) log(options, chalk.dim(` resolved ${normalized.rewritten} relative import(s) for Node ESM`));
1158
+ if (normalized.unresolved.length > 0) {
1159
+ say(options, chalk.yellow(` ⚠ ${normalized.unresolved.length} import(s) could not be resolved to a file:`));
1160
+ for (const item of normalized.unresolved.slice(0, 5)) say(options, chalk.dim(` ${item}`));
1161
+ if (normalized.unresolved.length > 5) say(options, chalk.dim(` … and ${normalized.unresolved.length - 5} more`));
1162
+ }
1163
+ const compiledConfigDir = path.join(outDir, paths.config);
1164
+ const compiledCollectionsDir = path.join(compiledConfigDir, "collections");
1165
+ let collections = [];
1166
+ if (paths.hasCollections) {
1167
+ collections = await loadSourceCollections(path.join(projectRoot, paths.config, "collections"));
1168
+ if (collections.length === 0) throw new Error(`No collections were found in ${path.join(paths.config, "collections")}. Define at least one collection there, or remove the directory to have the runtime introspect collections from the live database instead.`);
1169
+ if (!fs.existsSync(compiledCollectionsDir)) throw new Error(`Compilation produced no collections directory at ${path.relative(projectRoot, compiledCollectionsDir)}.`);
1170
+ }
1171
+ const declared = collectDeclaredDependencies(projectRoot);
1172
+ const nativeModules = detectNativeDependencies(projectRoot, declared);
1173
+ const declaresStorageAuthorize = detectStorageAuthorize(path.join(outDir, paths.config));
1174
+ const resourceGraph = options.resources ?? {
1175
+ version: 1,
1176
+ resources: []
1177
+ };
1178
+ const collision = findStorageSuffixCollision(resourceGraph.resources.filter((r) => r.kind === "bucket").map((r) => ({
1179
+ key: r.key,
1180
+ engine: r.engine,
1181
+ transport: r.transport,
1182
+ ...r.label !== void 0 ? { label: r.label } : {}
1183
+ })).map((s) => s.key));
1184
+ if (collision) throw new Error(`Buckets "${collision.a}" and "${collision.b}" both map to the environment variable suffix "${collision.suffix || "(none)"}", so they would read each other's configuration. Rename one of them.`);
1185
+ const schemaOut = paths.schema.replace(/\.ts$/, ".js");
1186
+ const relative = (target) => fs.existsSync(path.join(outDir, target)) ? target : void 0;
1187
+ const functionReports = analyseFunctionsDirectory(path.join(projectRoot, paths.functions), projectRoot);
1188
+ const bundledFunctions = functionReports.map((report) => {
1189
+ const requires = [...new Set(report.issues.filter((issue) => issue.kind === "node-builtin" || issue.kind === "node-only-package").map((issue) => issue.message))];
1190
+ return {
1191
+ name: report.name,
1192
+ file: report.file.replace(/\.ts$/, ".js"),
1193
+ portable: report.portable,
1194
+ ...requires.length > 0 ? { requires } : {}
1195
+ };
1196
+ });
1197
+ for (const line of summarisePortability(functionReports)) say(options, line.trimStart().startsWith("⚠") ? chalk.yellow(line) : chalk.dim(line));
1198
+ const manifest = composeBundleManifest({
1199
+ runtimeRange: options.runtimeRange,
1200
+ builtAgainst: resolveServerVersion(projectRoot),
1201
+ schemaVersion: paths.hasCollections ? computeSchemaVersion(collections) : "",
1202
+ appName,
1203
+ entry: {
1204
+ config: paths.hasConfig ? relative(paths.config) : void 0,
1205
+ collections: paths.hasCollections ? relative(path.join(paths.config, "collections")) : void 0,
1206
+ functions: relative(paths.functions),
1207
+ crons: relative(paths.crons),
1208
+ schema: relative(schemaOut),
1209
+ usersCollection: paths.hasCollections ? relative(path.join(paths.config, `${paths.usersCollection}.js`)) : void 0
1210
+ },
1211
+ collectionSlugs: collections.map((collection) => collection.slug).filter((slug) => Boolean(slug)),
1212
+ functions: bundledFunctions,
1213
+ nativeModules,
1214
+ declaresStorageAuthorize,
1215
+ resources: resourceGraph,
1216
+ declaredDeps: declared,
1217
+ build: {
1218
+ cli: cliVersion(),
1219
+ node: process.versions.node.split(".")[0],
1220
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1221
+ }
1222
+ });
1223
+ fs.writeFileSync(path.join(outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
1224
+ fs.writeFileSync(path.join(outDir, "package.json"), `${JSON.stringify({
1225
+ name: "rebase-bundle",
1226
+ private: true,
1227
+ type: "module",
1228
+ dependencies: declared
1229
+ }, null, 2)}\n`, "utf8");
1230
+ const vendor = vendorDependencies({
1231
+ outDir,
1232
+ declared,
1233
+ nativeModules,
1234
+ required: [getActiveBackendPlugin(path.join(projectRoot, "backend"))].filter((name) => Boolean(name)),
1235
+ requested: options.vendor
1236
+ });
1237
+ if (vendor.vendored) {
1238
+ manifest.deps.vendored = true;
1239
+ manifest.deps.vendorTarget = vendor.target;
1240
+ fs.writeFileSync(path.join(outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
1241
+ }
1242
+ return {
1243
+ outDir,
1244
+ manifest,
1245
+ collectionCount: collections.length,
1246
+ vendor
1247
+ };
1248
+ }
1249
+ /** Default install target: what the published runtime image runs. */
1250
+ var VENDOR_TARGET_OS = "linux";
1251
+ var VENDOR_TARGET_CPU = "x64";
1252
+ /**
1253
+ * Where a vendored bundle starts being too big to upload.
1254
+ *
1255
+ * The control plane refuses a bundle over 100 MB, and that ceiling is not
1256
+ * arbitrary or easily raised: its pod has a 512Mi memory limit and the upload
1257
+ * route holds the body while it writes it, so the cap protects the process that
1258
+ * also serves the console, deploys and billing. Vendoring is the one change that
1259
+ * can push a bundle near it.
1260
+ *
1261
+ * So the warning is here, at build time, where the remedy is one flag away —
1262
+ * rather than at deploy time as a 413 nobody can act on without rebuilding. The
1263
+ * threshold sits below the real cap because this measures the tree on disk and
1264
+ * the upload is compressed: crossing it means "getting close", not "will fail".
1265
+ */
1266
+ var VENDOR_SIZE_WARN_BYTES = 150 * 1024 * 1024;
1267
+ /**
1268
+ * Where vendoring stops being an optimisation and becomes a bundle nobody can
1269
+ * deploy.
1270
+ *
1271
+ * Past this, shipping the tree anyway trades a faster cold start for a 413 — and
1272
+ * the 413 arrives at deploy time, after a build nobody watches, with a remedy
1273
+ * (`--no-vendor`) that requires knowing this happened. Unvendoring here costs
1274
+ * 40–60s of cold start and produces a bundle that uploads; that is the better
1275
+ * side of the trade to be on by default.
1276
+ *
1277
+ * 200 MB assumes a **2x** floor on compression, which is pessimistic for a tree
1278
+ * of JavaScript (3–5x is typical) and deliberately so: source maps and prebuilt
1279
+ * binaries compress far worse than source, and the failure this prevents is
1280
+ * asymmetric — a bundle refused at the door versus a minute of cold start.
1281
+ * `--vendor` overrides it, for a deploy path with no upload at all (a Dockerfile
1282
+ * built from source, where the tree is copied into an image and the control
1283
+ * plane never sees it).
1284
+ */
1285
+ var VENDOR_SIZE_MAX_BYTES = 200 * 1024 * 1024;
1286
+ /** Bytes on disk under `dir`. Bounded so a pathological tree cannot hang a build. */
1287
+ function directorySize(dir, budget = 2e5) {
1288
+ let total = 0;
1289
+ let visited = 0;
1290
+ const stack = [dir];
1291
+ while (stack.length > 0 && visited < budget) {
1292
+ const current = stack.pop();
1293
+ let entries;
1294
+ try {
1295
+ entries = fs.readdirSync(current, { withFileTypes: true });
1296
+ } catch {
1297
+ continue;
1298
+ }
1299
+ for (const entry of entries) {
1300
+ visited++;
1301
+ const full = path.join(current, entry.name);
1302
+ if (entry.isDirectory()) stack.push(full);
1303
+ else if (entry.isFile()) try {
1304
+ total += fs.statSync(full).size;
1305
+ } catch {}
1306
+ }
1307
+ }
1308
+ return total;
1309
+ }
1310
+ /**
1311
+ * Install the bundle's declared dependencies into the bundle itself.
1312
+ *
1313
+ * ## What this buys
1314
+ *
1315
+ * A managed pod's bundle lives on an `emptyDir`, so it is re-fetched and
1316
+ * re-installed on **every** start — an eviction, a node failure, an OOM, a
1317
+ * runtime rollout. The install is 35–55 seconds of a 40–60 second cold start,
1318
+ * which makes it the price of every unplanned restart a tenant suffers. Doing it
1319
+ * once at build time instead of every time at boot takes that to roughly the
1320
+ * cost of untarring.
1321
+ *
1322
+ * The pod side needs no change to benefit: the init container already skips
1323
+ * installing when `node_modules` is present, a guard that existed for
1324
+ * pre-baked images and turns out to be exactly the hook this needs.
1325
+ *
1326
+ * ## Why it refuses to vendor native code
1327
+ *
1328
+ * A compiled binary is valid only for the platform it was built for, and a
1329
+ * developer's machine is rarely the deployment's. The managed runtime already
1330
+ * refuses bundles containing native modules for the same reason, so this refusal
1331
+ * costs nothing there — but a self-hosted project may legitimately use them, and
1332
+ * for those the honest answer is to install in the container, where the platform
1333
+ * is known.
1334
+ *
1335
+ * ## Why `--os` and `--cpu` are not optional
1336
+ *
1337
+ * The dangerous case is not native code, which is detectable. It is a pure-JS
1338
+ * package whose real work lives in a **platform-specific optional dependency** —
1339
+ * `esbuild` being the one everybody meets. Installing on an Apple Silicon Mac
1340
+ * resolves `@esbuild/darwin-arm64`, produces a tree that looks complete, and
1341
+ * fails at import inside a linux/amd64 pod. npm resolves optional dependencies
1342
+ * for the declared target rather than the host when told to, so it is told to.
1343
+ */
1344
+ function vendorDependencies(options) {
1345
+ if (options.requested === false) return {
1346
+ vendored: false,
1347
+ skipped: "disabled with --no-vendor"
1348
+ };
1349
+ if (Object.keys(options.declared).length === 0) return {
1350
+ vendored: false,
1351
+ skipped: "the bundle declares no dependencies"
1352
+ };
1353
+ if (options.nativeModules.length > 0) return {
1354
+ vendored: false,
1355
+ skipped: `the dependency closure contains native code (${options.nativeModules.map((m) => m.name).join(", ")}), which is only valid on the platform it was compiled for`
1356
+ };
1357
+ const target = {
1358
+ os: VENDOR_TARGET_OS,
1359
+ cpu: "x64",
1360
+ node: process.versions.node.split(".")[0]
1361
+ };
1362
+ const run = options.run ?? ((cmd, args, cwd) => {
1363
+ const result = spawnSync(cmd, args, {
1364
+ cwd,
1365
+ stdio: "pipe",
1366
+ encoding: "utf8"
1367
+ });
1368
+ if (result.error) throw result.error;
1369
+ if (result.status !== 0) throw new Error(`${cmd} ${args.join(" ")} exited ${result.status}\n${result.stderr || result.stdout || ""}`.trim());
1370
+ });
1371
+ try {
1372
+ run("npm", [
1373
+ "install",
1374
+ "--omit=dev",
1375
+ "--ignore-scripts",
1376
+ "--no-audit",
1377
+ "--no-fund",
1378
+ `--os=${target.os}`,
1379
+ `--cpu=${target.cpu}`
1380
+ ], options.outDir);
1381
+ } catch (error) {
1382
+ return {
1383
+ vendored: false,
1384
+ skipped: `npm install failed: ${error instanceof Error ? error.message : String(error)}`
1385
+ };
1386
+ }
1387
+ const installed = path.join(options.outDir, "node_modules");
1388
+ if (!fs.existsSync(installed)) return {
1389
+ vendored: false,
1390
+ skipped: "npm install produced no node_modules"
1391
+ };
1392
+ const missing = (options.required ?? []).filter((name) => !fs.existsSync(path.join(installed, ...name.split("/"), "package.json")));
1393
+ if (missing.length > 0) {
1394
+ fs.rmSync(installed, {
1395
+ recursive: true,
1396
+ force: true
1397
+ });
1398
+ fs.rmSync(path.join(options.outDir, "package-lock.json"), { force: true });
1399
+ return {
1400
+ vendored: false,
1401
+ skipped: `the installed tree is missing ${missing.join(", ")}, which the runtime resolves from the bundle — the bundle declares it at a version no registry can serve (a workspace link), so nothing was vendored rather than shipping a tree that boots without a driver`
1402
+ };
1403
+ }
1404
+ const bytes = directorySize(installed);
1405
+ if (bytes > 209715200 && options.requested !== true) {
1406
+ fs.rmSync(installed, {
1407
+ recursive: true,
1408
+ force: true
1409
+ });
1410
+ fs.rmSync(path.join(options.outDir, "package-lock.json"), { force: true });
1411
+ return {
1412
+ vendored: false,
1413
+ skipped: `the installed tree is ${Math.round(bytes / (1024 * 1024))} MB, past the point where the upload can be expected to fit under the control plane's 100 MB limit — a bundle that is refused at the door is worse than one that installs at boot. Pass --vendor to keep it anyway (a deploy that builds from source never uploads the tree), or shrink the declared dependencies`
1414
+ };
1415
+ }
1416
+ return {
1417
+ vendored: true,
1418
+ target,
1419
+ bytes
1420
+ };
1421
+ }
1422
+ /**
1423
+ * Package a built static app (a `static` or bundled-`admin` app) into a bundle.
1424
+ *
1425
+ * A static bundle is the counterpart to a backend bundle: the same shape, the
1426
+ * same runtime image runs it, but its manifest says `mode: "static"` and it
1427
+ * carries only the built assets under `static/`. That is what lets a frontend or
1428
+ * admin app be its own deployable, scalable unit rather than something baked into
1429
+ * the backend container.
1430
+ *
1431
+ * `assetsDir` is the app's built output (e.g. `frontend/dist`), already produced
1432
+ * by its own build command. This copies it into the bundle and writes the
1433
+ * manifest — no compilation, no dependency closure (a static bundle installs
1434
+ * nothing at boot).
1435
+ */
1436
+ /**
1437
+ * Fold a built static app into a backend bundle, so one runtime serves both.
1438
+ *
1439
+ * ## Why this exists
1440
+ *
1441
+ * A managed tenant runs one pod, and `bootFromBundle` on the backend path already
1442
+ * knows how to serve a SPA — it looks for `entry.static` and mounts `serveSPA`
1443
+ * last, behind `REBASE_SERVE_STATIC`. What was missing was anything putting the
1444
+ * assets there.
1445
+ *
1446
+ * The consequence was not subtle. A project whose custom image served its website
1447
+ * at `/` and its API at `/api` — the shape the scaffolded template produces — lost
1448
+ * the website the moment it moved to the managed runtime: the API answered
1449
+ * perfectly and every page 404'd. Managed could not be a drop-in replacement for
1450
+ * custom while the frontend simply vanished.
1451
+ *
1452
+ * Folding restores parity with the container it replaces, which is the only
1453
+ * honest baseline. It is deliberately the FIRST implementation and not the last:
1454
+ * a static app on its own bucket behind a CDN is better for cache behaviour and
1455
+ * lets the frontend deploy independently. But that needs infrastructure that does
1456
+ * not exist yet, and "your site is gone" is not an acceptable state to leave a
1457
+ * project in while it gets built.
1458
+ *
1459
+ * The trade it makes, stated plainly: frontend and backend now deploy together
1460
+ * and the bundle carries the built assets. For a project that was shipping both
1461
+ * in one image already, that is exactly what it had.
1462
+ */
1463
+ function foldStaticIntoBundle(options) {
1464
+ const { bundleDir, assetsDir, appName, path: basePath, host, spa, cms } = options;
1465
+ const manifestPath = path.join(bundleDir, "manifest.json");
1466
+ if (!fs.existsSync(manifestPath)) throw new Error(`No manifest at ${manifestPath} — build the backend bundle first.`);
1467
+ if (!fs.existsSync(assetsDir)) throw new Error(`No built assets at ${assetsDir}.`);
1468
+ const dir = path.posix.join("static", appName);
1469
+ const staticOut = path.join(bundleDir, "static", appName);
1470
+ fs.rmSync(staticOut, {
1471
+ recursive: true,
1472
+ force: true
1473
+ });
1474
+ fs.mkdirSync(staticOut, { recursive: true });
1475
+ fs.cpSync(assetsDir, staticOut, { recursive: true });
1476
+ let fileCount = 0;
1477
+ const count = (target) => {
1478
+ for (const entry of fs.readdirSync(target, { withFileTypes: true })) if (entry.isDirectory()) count(path.join(target, entry.name));
1479
+ else fileCount++;
1480
+ };
1481
+ count(staticOut);
1482
+ precompressStaticAssets(staticOut);
1483
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
1484
+ const existing = (manifest.entry?.static ?? []).filter((entry) => entry.dir !== dir);
1485
+ manifest.entry = {
1486
+ ...manifest.entry,
1487
+ static: [...existing, {
1488
+ path: basePath,
1489
+ ...host ? { host } : {},
1490
+ dir,
1491
+ spa,
1492
+ name: appName,
1493
+ ...cms ? { cms } : {}
1494
+ }].sort(compareMountOrder)
1495
+ };
1496
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
1497
+ return {
1498
+ fileCount,
1499
+ dir
1500
+ };
1501
+ }
1502
+ function buildStaticBundle(options) {
1503
+ const { projectRoot, appName, assetsDir, outDir, runtimeRange } = options;
1504
+ const basePath = options.path ?? "/";
1505
+ cleanOutDir(projectRoot, outDir);
1506
+ const staticOut = path.join(outDir, "static");
1507
+ fs.mkdirSync(staticOut, { recursive: true });
1508
+ fs.cpSync(assetsDir, staticOut, { recursive: true });
1509
+ let fileCount = 0;
1510
+ const count = (dir) => {
1511
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) if (entry.isDirectory()) count(path.join(dir, entry.name));
1512
+ else fileCount++;
1513
+ };
1514
+ count(staticOut);
1515
+ precompressStaticAssets(staticOut);
1516
+ const manifest = {
1517
+ bundleFormat: BUNDLE_FORMAT_VERSION,
1518
+ runtime: {
1519
+ range: runtimeRange,
1520
+ builtAgainst: resolveServerVersion(projectRoot),
1521
+ contract: RUNTIME_CONTRACT_VERSION
1522
+ },
1523
+ schemaVersion: "",
1524
+ app: appName,
1525
+ kind: "static",
1526
+ entry: { static: [{
1527
+ path: basePath,
1528
+ ...options.host ? { host: options.host } : {},
1529
+ dir: "static",
1530
+ spa: options.spa ?? true,
1531
+ name: appName,
1532
+ ...options.cms ? { cms: options.cms } : {}
1533
+ }] },
1534
+ hooks: { native: false },
1535
+ deps: { declared: {} },
1536
+ build: {
1537
+ cli: cliVersion(),
1538
+ node: process.versions.node.split(".")[0],
1539
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1540
+ }
1541
+ };
1542
+ fs.writeFileSync(path.join(outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
1543
+ fs.writeFileSync(path.join(outDir, "package.json"), `${JSON.stringify({
1544
+ name: "rebase-bundle",
1545
+ private: true,
1546
+ type: "module",
1547
+ dependencies: {}
1548
+ }, null, 2)}\n`, "utf8");
1549
+ return {
1550
+ outDir,
1551
+ manifest,
1552
+ fileCount
1553
+ };
1554
+ }
1555
+ /**
1556
+ * Which files in a collections directory are collections.
1557
+ *
1558
+ * Mirrors the runtime loader's rules exactly, and must keep mirroring them: the
1559
+ * set of files counted here decides the schema version, and the runtime decides
1560
+ * what it serves the same way. A divergence would show up as a client that is
1561
+ * permanently "out of date" against a server that agrees with it.
1562
+ *
1563
+ * (`._*` guards macOS AppleDouble files, which look like sources and are not.)
1564
+ */
1565
+ function isCollectionSourceFile(name) {
1566
+ if (name.startsWith(".")) return false;
1567
+ if (name.includes(".test.") || name.includes(".spec.")) return false;
1568
+ if (name.endsWith(".d.ts")) return false;
1569
+ if (name === "index.ts" || name === "index.js") return false;
1570
+ return name.endsWith(".ts") || name.endsWith(".js");
1571
+ }
1572
+ /**
1573
+ * Load collections from **source**, for hashing and for the manifest's slug list.
1574
+ *
1575
+ * Deliberately not the compiled output. A compiled bundle imports its
1576
+ * dependencies from beside itself — that is the whole point of shipping a
1577
+ * `package.json` with it — but at build time nothing has been installed there
1578
+ * yet, and under pnpm the project's own `node_modules` lives one directory per
1579
+ * package, so the emitted files genuinely cannot resolve their imports until
1580
+ * they are deployed.
1581
+ *
1582
+ * Reading source costs nothing in fidelity: compilation erases types, it does
1583
+ * not change the values a collection module exports, so the hash is the same
1584
+ * either way.
1585
+ */
1586
+ async function loadSourceCollections(collectionsDir) {
1587
+ if (!fs.existsSync(collectionsDir)) return [];
1588
+ const { createJiti } = await import("jiti");
1589
+ const jiti = createJiti(path.join(collectionsDir, "index.ts"), {
1590
+ interopDefault: true,
1591
+ esmResolve: true
1592
+ });
1593
+ const files = fs.readdirSync(collectionsDir).filter(isCollectionSourceFile).sort();
1594
+ const collections = [];
1595
+ const failures = [];
1596
+ for (const file of files) try {
1597
+ const mod = await jiti.import(path.join(collectionsDir, file));
1598
+ const collection = mod.default ?? mod;
1599
+ if (collection && typeof collection === "object" && "slug" in collection) collections.push(collection);
1600
+ else failures.push(`${file}: no default-exported collection`);
1601
+ } catch (err) {
1602
+ failures.push(`${file}: ${err instanceof Error ? err.message : String(err)}`);
1603
+ }
1604
+ if (failures.length > 0) throw new Error(`Could not read ${failures.length} collection file(s):\n` + failures.map((f) => ` • ${f}`).join("\n"));
1605
+ return collections;
1606
+ }
1607
+ /** The `@rebasepro/server` version the project resolves — what it was built against. */
1608
+ function resolveServerVersion(projectRoot) {
1609
+ const candidates = [path.join(projectRoot, "node_modules", "@rebasepro", "server", "package.json"), path.join(projectRoot, "backend", "node_modules", "@rebasepro", "server", "package.json")];
1610
+ for (const candidate of candidates) {
1611
+ if (!fs.existsSync(candidate)) continue;
1612
+ try {
1613
+ return JSON.parse(fs.readFileSync(candidate, "utf8")).version;
1614
+ } catch {}
1615
+ }
1616
+ return "unknown";
1617
+ }
1618
+ //#endregion
1619
+ export { foldStaticIntoBundle as _, VENDOR_TARGET_OS as a, toolStdio as b, collectDeclaredDependencies as c, describeDepConflicts as d, detectDeclaredDepConflicts as f, findUnusedServerEntry as g, detectStorageAuthorize as h, VENDOR_TARGET_CPU as i, commentSpans as l, detectNativeDependencies as m, VENDOR_SIZE_MAX_BYTES as n, buildBundle as o, detectFrameworkDepDrift as p, VENDOR_SIZE_WARN_BYTES as r, buildStaticBundle as s, DEFAULT_BUNDLE_DIR as t, composeBundleManifest as u, normalizeEsmSpecifiers as v, vendorDependencies as y };
1620
+
1621
+ //# sourceMappingURL=bundle-CqURVN-K.js.map