@rebasepro/cli 0.10.1-canary.b1e3dbf → 0.10.1-canary.ff9ccd6

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/bundle.d.ts CHANGED
@@ -98,6 +98,41 @@ export declare function buildBundle(options: BuildBundleOptions): Promise<BuildB
98
98
  * manifest — no compilation, no dependency closure (a static bundle installs
99
99
  * nothing at boot).
100
100
  */
101
+ /**
102
+ * Fold a built static app into a backend bundle, so one runtime serves both.
103
+ *
104
+ * ## Why this exists
105
+ *
106
+ * A managed tenant runs one pod, and `bootFromBundle` on the backend path already
107
+ * knows how to serve a SPA — it looks for `entry.static` and mounts `serveSPA`
108
+ * last, behind `REBASE_SERVE_STATIC`. What was missing was anything putting the
109
+ * assets there.
110
+ *
111
+ * The consequence was not subtle. A project whose custom image served its website
112
+ * at `/` and its API at `/api` — the shape the scaffolded template produces — lost
113
+ * the website the moment it moved to the managed runtime: the API answered
114
+ * perfectly and every page 404'd. Managed could not be a drop-in replacement for
115
+ * custom while the frontend simply vanished.
116
+ *
117
+ * Folding restores parity with the container it replaces, which is the only
118
+ * honest baseline. It is deliberately the FIRST implementation and not the last:
119
+ * a static app on its own bucket behind a CDN is better for cache behaviour and
120
+ * lets the frontend deploy independently. But that needs infrastructure that does
121
+ * not exist yet, and "your site is gone" is not an acceptable state to leave a
122
+ * project in while it gets built.
123
+ *
124
+ * The trade it makes, stated plainly: frontend and backend now deploy together
125
+ * and the bundle carries the built assets. For a project that was shipping both
126
+ * in one image already, that is exactly what it had.
127
+ */
128
+ export declare function foldStaticIntoBundle(options: {
129
+ /** The backend bundle directory, already written. */
130
+ bundleDir: string;
131
+ /** Directory of built frontend assets (the static app's `output`). */
132
+ assetsDir: string;
133
+ }): {
134
+ fileCount: number;
135
+ };
101
136
  export declare function buildStaticBundle(options: {
102
137
  projectRoot: string;
103
138
  appName: string;
@@ -23,6 +23,31 @@ export declare function bundleDeployBody(input: {
23
23
  manifest: RebaseBundleManifest;
24
24
  app?: string;
25
25
  message?: string;
26
+ /**
27
+ * Every app this repository declares in `rebase.json`, so the platform can
28
+ * register the whole set rather than only the one being deployed.
29
+ */
30
+ declaredApps?: DeclaredApp[];
26
31
  }): Record<string, unknown>;
32
+ /** An app as `rebase.json` declares it, reduced to what the registry stores. */
33
+ export interface DeclaredApp {
34
+ name: string;
35
+ type: string;
36
+ }
37
+ /**
38
+ * The apps a project manifest declares.
39
+ *
40
+ * A deploy only ever ships ONE app's bundle, so the trigger alone could never
41
+ * tell the platform that the repository also contains a web frontend and an
42
+ * admin panel — and the Apps page, whose whole job is to show the set, listed a
43
+ * single entry called "backend". Sending the declared set fixes that without
44
+ * pretending the others are deployed: the platform registers them, and their
45
+ * status says what is actually true.
46
+ */
47
+ export declare function declaredAppsFrom(manifest: {
48
+ apps?: Record<string, {
49
+ type?: string;
50
+ }>;
51
+ } | null | undefined): DeclaredApp[];
27
52
  /** Upload a bundle archive; returns the control-plane bundle id. */
28
53
  export declare function uploadBundle(url: string, token: string, projectId: string, tarPath: string): Promise<string>;
@@ -0,0 +1,46 @@
1
+ /** The apps section of a project manifest, as much of it as folding needs. */
2
+ export interface FoldableManifest {
3
+ apps?: Record<string, {
4
+ type?: string;
5
+ build?: string;
6
+ output?: string;
7
+ }>;
8
+ }
9
+ export interface FoldOptions {
10
+ projectRoot: string;
11
+ manifest: FoldableManifest;
12
+ /** The backend bundle directory, already written. */
13
+ bundleDir: string;
14
+ /** Skip running the app's own build command; fold what is already built. */
15
+ skipBuild?: boolean;
16
+ log?: (message: string) => void;
17
+ }
18
+ export interface FoldOutcome {
19
+ appName: string;
20
+ fileCount: number;
21
+ }
22
+ /**
23
+ * Which static app, if any, should be served by the backend.
24
+ *
25
+ * Exactly one `static` app is folded. With several, folding would have to choose,
26
+ * and silently picking one of two websites is worse than doing nothing — so it
27
+ * declines and names what it saw. Pure, so the decision is testable without a
28
+ * filesystem.
29
+ */
30
+ export declare function selectFoldableApp(manifest: FoldableManifest): {
31
+ app?: {
32
+ name: string;
33
+ build?: string;
34
+ output?: string;
35
+ };
36
+ /** Why nothing will be folded, when that is the answer. */
37
+ reason?: string;
38
+ };
39
+ /**
40
+ * Build the project's frontend and fold it into the backend bundle.
41
+ *
42
+ * Throws rather than exiting, so the caller decides whether a missing frontend
43
+ * should fail its command — a `build` may reasonably want to stop, and so should
44
+ * a deploy, but that is not this function's call to make.
45
+ */
46
+ export declare function foldFrontendIntoBundle(options: FoldOptions): Promise<FoldOutcome | null>;
package/dist/index.es.js CHANGED
@@ -3395,6 +3395,59 @@ async function buildBundle(options) {
3395
3395
  * manifest — no compilation, no dependency closure (a static bundle installs
3396
3396
  * nothing at boot).
3397
3397
  */
3398
+ /**
3399
+ * Fold a built static app into a backend bundle, so one runtime serves both.
3400
+ *
3401
+ * ## Why this exists
3402
+ *
3403
+ * A managed tenant runs one pod, and `bootFromBundle` on the backend path already
3404
+ * knows how to serve a SPA — it looks for `entry.static` and mounts `serveSPA`
3405
+ * last, behind `REBASE_SERVE_STATIC`. What was missing was anything putting the
3406
+ * assets there.
3407
+ *
3408
+ * The consequence was not subtle. A project whose custom image served its website
3409
+ * at `/` and its API at `/api` — the shape the scaffolded template produces — lost
3410
+ * the website the moment it moved to the managed runtime: the API answered
3411
+ * perfectly and every page 404'd. Managed could not be a drop-in replacement for
3412
+ * custom while the frontend simply vanished.
3413
+ *
3414
+ * Folding restores parity with the container it replaces, which is the only
3415
+ * honest baseline. It is deliberately the FIRST implementation and not the last:
3416
+ * a static app on its own bucket behind a CDN is better for cache behaviour and
3417
+ * lets the frontend deploy independently. But that needs infrastructure that does
3418
+ * not exist yet, and "your site is gone" is not an acceptable state to leave a
3419
+ * project in while it gets built.
3420
+ *
3421
+ * The trade it makes, stated plainly: frontend and backend now deploy together
3422
+ * and the bundle carries the built assets. For a project that was shipping both
3423
+ * in one image already, that is exactly what it had.
3424
+ */
3425
+ function foldStaticIntoBundle(options) {
3426
+ const { bundleDir, assetsDir } = options;
3427
+ const manifestPath = path.join(bundleDir, "manifest.json");
3428
+ if (!fs.existsSync(manifestPath)) throw new Error(`No manifest at ${manifestPath} — build the backend bundle first.`);
3429
+ if (!fs.existsSync(assetsDir)) throw new Error(`No built assets at ${assetsDir}.`);
3430
+ const staticOut = path.join(bundleDir, "static");
3431
+ fs.rmSync(staticOut, {
3432
+ recursive: true,
3433
+ force: true
3434
+ });
3435
+ fs.mkdirSync(staticOut, { recursive: true });
3436
+ fs.cpSync(assetsDir, staticOut, { recursive: true });
3437
+ let fileCount = 0;
3438
+ const count = (dir) => {
3439
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) if (entry.isDirectory()) count(path.join(dir, entry.name));
3440
+ else fileCount++;
3441
+ };
3442
+ count(staticOut);
3443
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
3444
+ manifest.entry = {
3445
+ ...manifest.entry,
3446
+ static: "static"
3447
+ };
3448
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
3449
+ return { fileCount };
3450
+ }
3398
3451
  function buildStaticBundle(options) {
3399
3452
  const { projectRoot, appName, assetsDir, outDir, runtimeRange } = options;
3400
3453
  cleanOutDir(projectRoot, outDir);
@@ -3517,6 +3570,76 @@ function resolveCliVersion() {
3517
3570
  return "unknown";
3518
3571
  }
3519
3572
  //#endregion
3573
+ //#region src/fold-static.ts
3574
+ /**
3575
+ * Folding a project's frontend into its backend bundle.
3576
+ *
3577
+ * Shared by `rebase build` and `rebase cloud deploy` deliberately. It lived in
3578
+ * the build *command* first, and `deploy` rebuilds the bundle itself — so a
3579
+ * deploy silently produced a bundle without the frontend, packed 164 KB where
3580
+ * 39 MB was expected, and the site 404'd on the managed runtime exactly as if
3581
+ * folding had never been written. Two callers building the same artefact must
3582
+ * share the step that completes it.
3583
+ *
3584
+ * Why fold at all: `bootFromBundle` already serves a SPA from `entry.static`
3585
+ * behind `REBASE_SERVE_STATIC` (default on). A managed tenant runs one pod, so
3586
+ * putting the built site in the bundle gives it the shape a custom container
3587
+ * already had — site at `/`, API at `/api` — which is the only honest baseline
3588
+ * for calling the managed runtime a drop-in replacement.
3589
+ */
3590
+ /**
3591
+ * Which static app, if any, should be served by the backend.
3592
+ *
3593
+ * Exactly one `static` app is folded. With several, folding would have to choose,
3594
+ * and silently picking one of two websites is worse than doing nothing — so it
3595
+ * declines and names what it saw. Pure, so the decision is testable without a
3596
+ * filesystem.
3597
+ */
3598
+ function selectFoldableApp(manifest) {
3599
+ const statics = Object.entries(manifest.apps ?? {}).filter(([, app]) => app?.type === "static").map(([name, app]) => ({
3600
+ name,
3601
+ build: app?.build,
3602
+ output: app?.output
3603
+ }));
3604
+ if (statics.length === 0) return {};
3605
+ if (statics.length > 1) return { reason: `${statics.length} static apps (${statics.map((s) => s.name).join(", ")}) — none folded in. Pick one to serve from the backend, or host them separately.` };
3606
+ const only = statics[0];
3607
+ if (!only.output) return { reason: `"${only.name}" declares no output directory — not folded in.` };
3608
+ return { app: only };
3609
+ }
3610
+ /**
3611
+ * Build the project's frontend and fold it into the backend bundle.
3612
+ *
3613
+ * Throws rather than exiting, so the caller decides whether a missing frontend
3614
+ * should fail its command — a `build` may reasonably want to stop, and so should
3615
+ * a deploy, but that is not this function's call to make.
3616
+ */
3617
+ async function foldFrontendIntoBundle(options) {
3618
+ const { projectRoot, manifest, bundleDir, skipBuild } = options;
3619
+ const log = options.log ?? ((m) => console.log(m));
3620
+ const { app, reason } = selectFoldableApp(manifest);
3621
+ if (reason) {
3622
+ log(chalk.yellow(` ⚠ ${reason}`));
3623
+ return null;
3624
+ }
3625
+ if (!app) return null;
3626
+ if (app.build && !skipBuild) await execa(app.build, {
3627
+ cwd: projectRoot,
3628
+ stdio: "inherit",
3629
+ shell: true
3630
+ });
3631
+ const assetsDir = path.join(projectRoot, app.output);
3632
+ if (!fs.existsSync(assetsDir)) throw new Error(`"${app.name}" declared output "${app.output}" does not exist after building — the bundle would ship without a frontend.`);
3633
+ const { fileCount } = foldStaticIntoBundle({
3634
+ bundleDir,
3635
+ assetsDir
3636
+ });
3637
+ return {
3638
+ appName: app.name,
3639
+ fileCount
3640
+ };
3641
+ }
3642
+ //#endregion
3520
3643
  //#region src/commands/build.ts
3521
3644
  /**
3522
3645
  * CLI command: rebase build [app...]
@@ -3557,6 +3680,8 @@ async function buildCommand(rawArgs = []) {
3557
3680
  "--out": String,
3558
3681
  "--skip-type-check": Boolean,
3559
3682
  "--skip-schema": Boolean,
3683
+ "--no-static": Boolean,
3684
+ "--skip-static-build": Boolean,
3560
3685
  "--legacy": Boolean,
3561
3686
  "--help": Boolean,
3562
3687
  "-h": "--help"
@@ -3627,6 +3752,19 @@ async function buildCommand(rawArgs = []) {
3627
3752
  console.log(chalk.yellow(` ⚠ native dependencies detected: ${names}`));
3628
3753
  console.log(chalk.dim(" These cannot run on the managed runtime. See `rebase doctor`."));
3629
3754
  }
3755
+ if (!args["--no-static"]) {
3756
+ const folded = await foldFrontendIntoBundle({
3757
+ projectRoot,
3758
+ manifest,
3759
+ bundleDir: result.outDir,
3760
+ skipBuild: args["--skip-static-build"] === true,
3761
+ log: (m) => console.log(m)
3762
+ }).catch((err) => {
3763
+ console.error(chalk.red(` ✗ ${err instanceof Error ? err.message : String(err)}`));
3764
+ process.exit(1);
3765
+ });
3766
+ if (folded) console.log(chalk.green(` ✓ ${folded.appName} folded in`) + chalk.dim(` (${folded.fileCount} file(s) → served at /)`));
3767
+ }
3630
3768
  } else if (app.type === "static" || app.type === "admin") await buildAssetApp(projectRoot, name, app, manifest.runtime, args["--out"]);
3631
3769
  else if (app.type === "custom") console.log(chalk.dim(" custom container — built at deploy time from its Dockerfile"));
3632
3770
  console.log("");
@@ -4771,7 +4909,7 @@ async function linkCommand(rawArgs) {
4771
4909
  })).data;
4772
4910
  if (projects.length === 0) fail("No projects found for your account.", `Create one with ${chalk.bold("rebase cloud projects create")}.`);
4773
4911
  const { picked } = await inquirer.prompt([{
4774
- type: "list",
4912
+ type: "select",
4775
4913
  name: "picked",
4776
4914
  message: "Select a project to link:",
4777
4915
  choices: projects.map((p) => ({
@@ -4815,7 +4953,7 @@ async function selectOrgCommand(rawArgs) {
4815
4953
  let chosen = target ? orgs.find((o) => String(o.id) === target || o.slug === target) : void 0;
4816
4954
  if (!chosen && !target) {
4817
4955
  const { picked } = await inquirer.prompt([{
4818
- type: "list",
4956
+ type: "select",
4819
4957
  name: "picked",
4820
4958
  message: "Select the active organization:",
4821
4959
  choices: orgs.map((o) => ({
@@ -5092,6 +5230,7 @@ function packBundle(bundleDir, outPath) {
5092
5230
  const child = spawn("tar", [
5093
5231
  "-czf",
5094
5232
  outPath,
5233
+ "--no-xattrs",
5095
5234
  "--exclude",
5096
5235
  "node_modules",
5097
5236
  "-C",
@@ -5123,9 +5262,28 @@ function bundleDeployBody(input) {
5123
5262
  app: input.app ?? input.manifest.app ?? "backend",
5124
5263
  client: "cli",
5125
5264
  frameworkVersion: input.manifest.runtime?.builtAgainst,
5265
+ ...input.declaredApps?.length ? { declaredApps: input.declaredApps } : {},
5126
5266
  ...input.message ? { message: input.message } : {}
5127
5267
  };
5128
5268
  }
5269
+ /**
5270
+ * The apps a project manifest declares.
5271
+ *
5272
+ * A deploy only ever ships ONE app's bundle, so the trigger alone could never
5273
+ * tell the platform that the repository also contains a web frontend and an
5274
+ * admin panel — and the Apps page, whose whole job is to show the set, listed a
5275
+ * single entry called "backend". Sending the declared set fixes that without
5276
+ * pretending the others are deployed: the platform registers them, and their
5277
+ * status says what is actually true.
5278
+ */
5279
+ function declaredAppsFrom(manifest) {
5280
+ const apps = manifest?.apps;
5281
+ if (!apps || typeof apps !== "object") return [];
5282
+ return Object.entries(apps).filter(([name]) => name.trim().length > 0).map(([name, value]) => ({
5283
+ name,
5284
+ type: String(value?.type ?? "custom")
5285
+ }));
5286
+ }
5129
5287
  /** Upload a bundle archive; returns the control-plane bundle id. */
5130
5288
  async function uploadBundle(url, token, projectId, tarPath) {
5131
5289
  const bytes = fs.readFileSync(tarPath);
@@ -5277,6 +5435,17 @@ async function deployBundle(opts) {
5277
5435
  runtimeRange: loaded.manifest.runtime,
5278
5436
  log: (m) => console.log(chalk.gray(m))
5279
5437
  })).outDir;
5438
+ try {
5439
+ const folded = await foldFrontendIntoBundle({
5440
+ projectRoot,
5441
+ manifest: loaded.manifest,
5442
+ bundleDir,
5443
+ log: (m) => console.log(m)
5444
+ });
5445
+ if (folded) console.log(chalk.gray(` folded ${folded.appName} in (${folded.fileCount} file(s), served at /)`));
5446
+ } catch (err) {
5447
+ fail(err instanceof Error ? err.message : String(err), "Fix the frontend build, or pass --no-static to deploy the API alone.");
5448
+ }
5280
5449
  }
5281
5450
  const manifest = readBundleManifest(bundleDir);
5282
5451
  if (manifest.hooks?.native) {
@@ -5300,11 +5469,16 @@ async function deployBundle(opts) {
5300
5469
  }
5301
5470
  console.log("");
5302
5471
  console.log(` 🚀 Triggering managed deployment for ${chalk.bold(projectRef)} (schema ${manifest.schemaVersion})...`);
5472
+ let declaredApps = [];
5473
+ try {
5474
+ declaredApps = declaredAppsFrom(loadManifest(process.cwd()).manifest);
5475
+ } catch {}
5303
5476
  const body = bundleDeployBody({
5304
5477
  projectId,
5305
5478
  bundleId,
5306
5479
  manifest,
5307
- message: opts.message
5480
+ message: opts.message,
5481
+ declaredApps
5308
5482
  });
5309
5483
  try {
5310
5484
  const res = await client.functions.invoke("deploy", body);
@@ -5727,7 +5901,7 @@ async function createDatabase(rawArgs) {
5727
5901
  let type = args["--type"];
5728
5902
  if (!type) {
5729
5903
  const { picked } = await inquirer.prompt([{
5730
- type: "list",
5904
+ type: "select",
5731
5905
  name: "picked",
5732
5906
  message: "Database type:",
5733
5907
  choices: [{
@@ -8965,6 +9139,6 @@ ${chalk.gray("Documentation: https://rebase.pro/docs")}
8965
9139
  `);
8966
9140
  }
8967
9141
  //#endregion
8968
- export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, MANIFEST_FILENAME, ManifestError, appsCommand, assessManagedCompatibility, authCommand, buildBundle, buildCommand, buildInitQuestions, buildStaticBundle, buildableApps, cloudCommand, collectDeclaredDependencies, configureEnvFile, createRebaseApp, dbCommand, detectNativeDependencies, detectPackageManager, detectStorageAuthorize, devCommand, doctorCommand, entry, findBackendApp, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, isPnpmAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, printInitHelp, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveLocalBin, resolvePluginCliScript, resolveTsx, schemaCommand, startCommand, synthesizeManifest, validateManifest, validateProjectName, validateTsxInstallation, writeManifest };
9142
+ export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, MANIFEST_FILENAME, ManifestError, appsCommand, assessManagedCompatibility, authCommand, buildBundle, buildCommand, buildInitQuestions, buildStaticBundle, buildableApps, cloudCommand, collectDeclaredDependencies, configureEnvFile, createRebaseApp, dbCommand, detectNativeDependencies, detectPackageManager, detectStorageAuthorize, devCommand, doctorCommand, entry, findBackendApp, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, foldStaticIntoBundle, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, isPnpmAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, printInitHelp, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveLocalBin, resolvePluginCliScript, resolveTsx, schemaCommand, startCommand, synthesizeManifest, validateManifest, validateProjectName, validateTsxInstallation, writeManifest };
8969
9143
 
8970
9144
  //# sourceMappingURL=index.es.js.map