@rebasepro/cli 0.11.1-canary.gfd39654 → 0.12.1-canary.g009ed95
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/bin/rebase.js +48 -1
- package/dist/bundle.d.ts +60 -7
- package/dist/commands/cloud/context.d.ts +53 -0
- package/dist/commands/cloud/deploy.d.ts +64 -0
- package/dist/commands/cloud/index.d.ts +23 -0
- package/dist/commands/dev.d.ts +16 -0
- package/dist/commands/eject.d.ts +1 -0
- package/dist/commands/init.d.ts +19 -15
- package/dist/fold-static.d.ts +41 -15
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +1262 -285
- package/dist/index.es.js.map +1 -1
- package/dist/manifest.d.ts +27 -8
- package/package.json +11 -11
- package/runtime/dev-server.mjs +0 -1
- package/templates/{template/backend → eject}/Dockerfile +16 -4
- package/templates/{template → eject}/backend/src/env.ts +0 -1
- package/templates/{template → eject}/backend/src/index.ts +41 -27
- package/templates/eject/docker-compose.custom.yml +71 -0
- package/templates/overlays/baas/backend/package.json +3 -6
- package/templates/overlays/baas/backend/tsconfig.json +8 -2
- package/templates/overlays/baas/config/index.ts +15 -0
- package/templates/overlays/baas/config/package.json +28 -0
- package/templates/overlays/baas/package.json +3 -3
- package/templates/overlays/baas/pnpm-workspace.yaml +1 -0
- package/templates/overlays/baas/rebase.json +2 -6
- package/templates/template/.env.example +15 -0
- package/templates/template/README.md +56 -22
- package/templates/template/ai-instructions.md +1 -0
- package/templates/template/backend/functions/hello.ts +45 -14
- package/templates/template/backend/package.json +3 -6
- package/templates/template/backend/tsconfig.json +23 -2
- package/templates/template/config/collections/index.ts +9 -1
- package/templates/template/config/tsconfig.json +16 -1
- package/templates/template/docker-compose.yml +62 -38
- package/templates/template/frontend/package.json +3 -3
- package/templates/template/frontend/src/App.tsx +2 -1
- package/templates/template/frontend/src/main.tsx +10 -2
- package/templates/template/frontend/vite.config.ts +5 -1
- package/templates/template/package.json +1 -2
- package/templates/template/rebase.json +5 -8
- package/templates/overlays/baas/backend/src/index.ts +0 -216
- package/templates/template/frontend/Dockerfile +0 -52
- package/templates/template/frontend/nginx.conf +0 -40
- /package/templates/overlays/baas/{backend/src → config}/storage.ts +0 -0
package/dist/index.es.js
CHANGED
|
@@ -12,7 +12,7 @@ import crypto from "crypto";
|
|
|
12
12
|
import { execSync, spawn, spawnSync } from "child_process";
|
|
13
13
|
import os from "os";
|
|
14
14
|
import { createRebaseClient } from "@rebasepro/client";
|
|
15
|
-
import { BUNDLE_FORMAT_VERSION, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, deserializeCollections } from "@rebasepro/types";
|
|
15
|
+
import { BUNDLE_FORMAT_VERSION, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, normalizeStorageSources, storageEnvSuffix } from "@rebasepro/types";
|
|
16
16
|
import { generateSDK } from "@rebasepro/codegen";
|
|
17
17
|
import { createRequire } from "module";
|
|
18
18
|
//#region src/utils/package-manager.ts
|
|
@@ -355,8 +355,9 @@ function requireProjectRoot() {
|
|
|
355
355
|
const root = findProjectRoot();
|
|
356
356
|
if (!root) {
|
|
357
357
|
console.error(chalk.red("✗ Could not find a Rebase project root."));
|
|
358
|
-
console.error(chalk.gray(
|
|
359
|
-
console.error(chalk.gray("
|
|
358
|
+
console.error(chalk.gray(` Looked in this directory and every parent for a ${MANIFEST_FILENAME},`));
|
|
359
|
+
console.error(chalk.gray(" a package.json with a \"backend\" workspace, or a backend/ next to a config/."));
|
|
360
|
+
console.error(chalk.gray(" Run this from inside a project, or create one with `rebase init`."));
|
|
360
361
|
process.exit(1);
|
|
361
362
|
}
|
|
362
363
|
return root;
|
|
@@ -532,7 +533,7 @@ async function requireClient(rawArgs) {
|
|
|
532
533
|
* the ingress and the console read — see saas/backend/src/utils/tenant-domain.ts).
|
|
533
534
|
*
|
|
534
535
|
* The CLI cannot know this value: it is per-deployment configuration (production
|
|
535
|
-
* serves tenants at `
|
|
536
|
+
* serves tenants at `rebase.website`, a dev control plane at `localhost`). It
|
|
536
537
|
* used to be hardcoded to `rebase.pro`, so `cloud projects create` congratulated
|
|
537
538
|
* the user with a URL that resolves nowhere near their app.
|
|
538
539
|
*
|
|
@@ -602,6 +603,39 @@ function removeLink(cwd = process.cwd()) {
|
|
|
602
603
|
}
|
|
603
604
|
return false;
|
|
604
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* Flags that may appear anywhere on a `rebase cloud` line, including *before*
|
|
608
|
+
* the resource group.
|
|
609
|
+
*
|
|
610
|
+
* They have to be declared wherever positionals are resolved, because `arg`'s
|
|
611
|
+
* `permissive: true` does not merely tolerate an undeclared flag — it pushes it
|
|
612
|
+
* into `_` alongside the positionals, and for a flag that takes a value it
|
|
613
|
+
* pushes the value in too. So `cloud --project acme storage create` parsed
|
|
614
|
+
* without this spec yields `_` of `["--project", "acme", "storage", "create"]`,
|
|
615
|
+
* and the group reads as `"acme"`: a real project name, in the group position,
|
|
616
|
+
* dispatching to nothing. Skipping tokens that start with `-` does not save you
|
|
617
|
+
* there — the damage is the orphaned value, which looks exactly like a
|
|
618
|
+
* positional.
|
|
619
|
+
*
|
|
620
|
+
* Only genuinely global flags belong here. Group-specific ones (`--bucket`,
|
|
621
|
+
* `--region`, …) are declared by the handler that owns them and always follow
|
|
622
|
+
* the group, so they cannot shift the group or action.
|
|
623
|
+
*
|
|
624
|
+
* `-p` is `--project` in eighteen places and `--password` in `login`. That
|
|
625
|
+
* ambiguity does not matter to the one caller that reads this spec: it resolves
|
|
626
|
+
* positionals and never looks at a flag's value, so all it needs to know is
|
|
627
|
+
* that `-p` takes one. Anything that wants the value must keep declaring it
|
|
628
|
+
* itself, with the meaning its own command gives it.
|
|
629
|
+
*/
|
|
630
|
+
var GLOBAL_CLOUD_FLAGS = {
|
|
631
|
+
"--json": Boolean,
|
|
632
|
+
"--yes": Boolean,
|
|
633
|
+
"--help": Boolean,
|
|
634
|
+
"--project": String,
|
|
635
|
+
"-p": "--project",
|
|
636
|
+
"-y": "--yes",
|
|
637
|
+
"-h": "--help"
|
|
638
|
+
};
|
|
605
639
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
606
640
|
/**
|
|
607
641
|
* The raw project reference to operate on: explicit `--project` flag wins,
|
|
@@ -707,6 +741,35 @@ function emit(human, json) {
|
|
|
707
741
|
if (JSON_MODE) printJson(json);
|
|
708
742
|
else human();
|
|
709
743
|
}
|
|
744
|
+
/**
|
|
745
|
+
* Print a warning (+ optional hint) — in every output mode, always to stderr.
|
|
746
|
+
*
|
|
747
|
+
* `emit` is for a command's *result*, and JSON mode legitimately replaces the
|
|
748
|
+
* human rendering of one. A warning is not a result: it says the command is
|
|
749
|
+
* about to do something the caller may not have meant, and that is exactly as
|
|
750
|
+
* true when the output is piped. Gating one on `!isJsonMode()` deleted it
|
|
751
|
+
* precisely where nobody was watching the terminal — a `--source` deploy ejected
|
|
752
|
+
* a live project off the managed runtime and said so only to a TTY that wasn't
|
|
753
|
+
* there.
|
|
754
|
+
*
|
|
755
|
+
* stdout carries the JSON value and nothing else, so warnings go to stderr:
|
|
756
|
+
* a machine parser reading stdout cannot be corrupted by one. Only the
|
|
757
|
+
* *formatting* may depend on the mode — colour and indentation for a terminal,
|
|
758
|
+
* plain ASCII otherwise. Whether a warning is emitted at all may not.
|
|
759
|
+
*
|
|
760
|
+
* Anything a caller might branch on belongs in the JSON payload as well; stderr
|
|
761
|
+
* is for whoever reads the transcript afterwards.
|
|
762
|
+
*/
|
|
763
|
+
function warn(message, hint) {
|
|
764
|
+
if (JSON_MODE) {
|
|
765
|
+
process.stderr.write(`warning: ${stripAnsi(message)}\n`);
|
|
766
|
+
if (hint) process.stderr.write(` ${stripAnsi(hint)}\n`);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
console.error("");
|
|
770
|
+
console.error(chalk.yellow(` ⚠ ${message}`));
|
|
771
|
+
if (hint) console.error(chalk.gray(` ${hint}`));
|
|
772
|
+
}
|
|
710
773
|
/** Print an error (+ optional hint) and exit non-zero. Never returns. */
|
|
711
774
|
function fail(message, hint, code) {
|
|
712
775
|
if (JSON_MODE) {
|
|
@@ -829,7 +892,7 @@ function openUrl(target, label = "Opening") {
|
|
|
829
892
|
//#region src/commands/init.ts
|
|
830
893
|
var access = promisify(fs.access);
|
|
831
894
|
var __filename$1 = fileURLToPath(import.meta.url);
|
|
832
|
-
var __dirname$
|
|
895
|
+
var __dirname$2 = path.dirname(__filename$1);
|
|
833
896
|
function findParentDir(currentDir, targetName) {
|
|
834
897
|
const root = path.parse(currentDir).root;
|
|
835
898
|
while (currentDir && currentDir !== root) {
|
|
@@ -838,22 +901,56 @@ function findParentDir(currentDir, targetName) {
|
|
|
838
901
|
}
|
|
839
902
|
return null;
|
|
840
903
|
}
|
|
841
|
-
var cliRoot = findParentDir(__dirname$
|
|
904
|
+
var cliRoot = findParentDir(__dirname$2, "cli");
|
|
842
905
|
var PROJECT_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
906
|
+
/**
|
|
907
|
+
* Every scaffolded file that carries a `{{PLACEHOLDER}}`.
|
|
908
|
+
*
|
|
909
|
+
* Exported because it is the single source of truth: `init.test.ts` reads it and
|
|
910
|
+
* asserts that every template file containing `{{` appears here. It used to be a
|
|
911
|
+
* local, with the test harness keeping a *second* copy — so the two drifted, and
|
|
912
|
+
* a test built on the copy could not observe the production list at all.
|
|
913
|
+
*
|
|
914
|
+
* The drift shipped: `docker-compose.yml` arrived with the self-host work
|
|
915
|
+
* (26fd5259c) and was added to neither, so every scaffolded project got a literal
|
|
916
|
+
* `name: {{PROJECT_NAME}}`. In YAML `{{...}}` is a map, not a string, so the
|
|
917
|
+
* documented `docker compose up` path failed on the file before doing anything:
|
|
918
|
+
*
|
|
919
|
+
* yaml: unmarshal errors: line 28: cannot unmarshal !!map into string
|
|
920
|
+
*/
|
|
921
|
+
var TEMPLATE_PLACEHOLDER_FILES = [
|
|
922
|
+
"package.json",
|
|
923
|
+
"frontend/package.json",
|
|
924
|
+
"backend/package.json",
|
|
925
|
+
"config/package.json",
|
|
926
|
+
"frontend/index.html",
|
|
927
|
+
"pnpm-workspace.yaml",
|
|
928
|
+
"docker-compose.yml",
|
|
929
|
+
"README.md"
|
|
930
|
+
];
|
|
843
931
|
/** Returns an error message, or null when the name is a valid package name. */
|
|
844
932
|
function validateProjectName(name) {
|
|
845
933
|
if (!name.trim()) return "Project name is required";
|
|
846
934
|
if (!PROJECT_NAME_RE.test(name)) return "Project name must start with a lowercase letter or number and contain only lowercase letters, numbers, hyphens, dots, or underscores";
|
|
847
935
|
return null;
|
|
848
936
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
937
|
+
/**
|
|
938
|
+
* Whether to scaffold the admin panel alongside the backend.
|
|
939
|
+
*
|
|
940
|
+
* A boolean, not a named pair. It was `--flavor cms|baas`, and neither word
|
|
941
|
+
* survived what they described: "cms" is a product category rather than a thing
|
|
942
|
+
* this tool builds, and "baas" was the same value that used to appear as
|
|
943
|
+
* `backend.mode` — which is now derived from whether collections are declared.
|
|
944
|
+
* What is left is one question: does this project get an admin UI?
|
|
945
|
+
*/
|
|
946
|
+
var HEADLESS_CHOICES = [{
|
|
947
|
+
name: "Backend + admin — API plus an admin UI, driven by collections you define (like Payload/Directus)",
|
|
948
|
+
value: false,
|
|
949
|
+
short: "Backend + admin"
|
|
853
950
|
}, {
|
|
854
|
-
name: "
|
|
855
|
-
value:
|
|
856
|
-
short: "
|
|
951
|
+
name: "Backend only — headless API over your database. No collections, no UI (like Supabase)",
|
|
952
|
+
value: true,
|
|
953
|
+
short: "Backend only"
|
|
857
954
|
}];
|
|
858
955
|
var PRESET_CHOICES = [
|
|
859
956
|
{
|
|
@@ -878,7 +975,7 @@ var PRESET_CHOICES = [
|
|
|
878
975
|
* types registered by the installed version of inquirer.
|
|
879
976
|
*/
|
|
880
977
|
function buildInitQuestions(params) {
|
|
881
|
-
const { nameArg, templateArg,
|
|
978
|
+
const { nameArg, templateArg, headlessArg, hasGitFlag, hasInstallFlag, pm } = params;
|
|
882
979
|
const questions = [];
|
|
883
980
|
if (!nameArg) questions.push({
|
|
884
981
|
type: "input",
|
|
@@ -887,12 +984,12 @@ function buildInitQuestions(params) {
|
|
|
887
984
|
default: "my-rebase-app",
|
|
888
985
|
validate: (input) => validateProjectName(input) ?? true
|
|
889
986
|
});
|
|
890
|
-
if (
|
|
987
|
+
if (headlessArg === void 0) questions.push({
|
|
891
988
|
type: "select",
|
|
892
|
-
name: "
|
|
989
|
+
name: "headless",
|
|
893
990
|
message: "What do you want to build?",
|
|
894
|
-
choices:
|
|
895
|
-
default:
|
|
991
|
+
choices: HEADLESS_CHOICES,
|
|
992
|
+
default: false
|
|
896
993
|
});
|
|
897
994
|
if (!templateArg) questions.push({
|
|
898
995
|
type: "select",
|
|
@@ -900,7 +997,7 @@ function buildInitQuestions(params) {
|
|
|
900
997
|
message: "Choose a starter template:",
|
|
901
998
|
choices: PRESET_CHOICES,
|
|
902
999
|
default: "blog",
|
|
903
|
-
when: (answers) => (
|
|
1000
|
+
when: (answers) => !(headlessArg ?? answers.headless)
|
|
904
1001
|
});
|
|
905
1002
|
if (!hasGitFlag) questions.push({
|
|
906
1003
|
type: "confirm",
|
|
@@ -956,7 +1053,7 @@ ${chalk.bold("Usage")}
|
|
|
956
1053
|
|
|
957
1054
|
${chalk.bold("Options")}
|
|
958
1055
|
${chalk.blue("-t, --template")} ${chalk.gray("<preset>")} blog | ecommerce | blank ${chalk.gray("(default: blog)")}
|
|
959
|
-
${chalk.blue("
|
|
1056
|
+
${chalk.blue("--headless")} Backend only — no admin panel, no collections
|
|
960
1057
|
${chalk.blue("-y, --yes")} Accept defaults, never prompt ${chalk.gray("(required for CI / non-TTY)")}
|
|
961
1058
|
${chalk.blue("-i, --install")} Install dependencies after scaffolding
|
|
962
1059
|
${chalk.blue("-g, --git")} Initialize a git repository and make an initial commit
|
|
@@ -965,14 +1062,14 @@ ${chalk.bold("Options")}
|
|
|
965
1062
|
${chalk.blue("--project")} ${chalk.gray("<slug>")} Link the scaffold to a Rebase Cloud project
|
|
966
1063
|
${chalk.blue("--setup-key")} ${chalk.gray("<key>")} One-time key authenticating the cloud link ${chalk.gray("(use with --project)")}
|
|
967
1064
|
|
|
968
|
-
${chalk.bold("
|
|
969
|
-
${chalk.
|
|
970
|
-
${chalk.blue("
|
|
971
|
-
|
|
1065
|
+
${chalk.bold("What gets scaffolded")}
|
|
1066
|
+
${chalk.gray("default")} Backend + an admin UI, driven by collections you define ${chalk.gray("(like Payload/Directus)")}
|
|
1067
|
+
${chalk.blue("--headless")} Backend only, over your existing database ${chalk.gray("(like Supabase)")}
|
|
1068
|
+
${chalk.gray("--template has no effect: there are no collections to seed.")}
|
|
972
1069
|
|
|
973
1070
|
${chalk.bold("Examples")}
|
|
974
1071
|
${chalk.gray("$")} rebase init my-shop --template ecommerce --install
|
|
975
|
-
${chalk.gray("$")} rebase init my-api --
|
|
1072
|
+
${chalk.gray("$")} rebase init my-api --headless --yes
|
|
976
1073
|
${chalk.gray("$")} rebase init . --yes --git
|
|
977
1074
|
`);
|
|
978
1075
|
}
|
|
@@ -993,14 +1090,13 @@ async function promptForOptions(rawArgs, pm) {
|
|
|
993
1090
|
"--database-url": String,
|
|
994
1091
|
"--introspect": Boolean,
|
|
995
1092
|
"--template": String,
|
|
996
|
-
"--
|
|
1093
|
+
"--headless": Boolean,
|
|
997
1094
|
"--project": String,
|
|
998
1095
|
"--setup-key": String,
|
|
999
1096
|
"--yes": Boolean,
|
|
1000
1097
|
"-g": "--git",
|
|
1001
1098
|
"-i": "--install",
|
|
1002
1099
|
"-t": "--template",
|
|
1003
|
-
"-f": "--flavor",
|
|
1004
1100
|
"-y": "--yes"
|
|
1005
1101
|
}, {
|
|
1006
1102
|
argv: rawArgs.slice(3),
|
|
@@ -1021,11 +1117,7 @@ async function promptForOptions(rawArgs, pm) {
|
|
|
1021
1117
|
console.error(chalk.red(`Unknown template "${templateArg}". Available: ${PRESET_CHOICES.map((p) => p.value).join(", ")}`));
|
|
1022
1118
|
process.exit(1);
|
|
1023
1119
|
}
|
|
1024
|
-
const
|
|
1025
|
-
if (flavorArg && !FLAVOR_CHOICES.some((f) => f.value === flavorArg)) {
|
|
1026
|
-
console.error(chalk.red(`Unknown flavor "${flavorArg}". Available: ${FLAVOR_CHOICES.map((f) => f.value).join(", ")}`));
|
|
1027
|
-
process.exit(1);
|
|
1028
|
-
}
|
|
1120
|
+
const headlessArg = args["--headless"] === true ? true : void 0;
|
|
1029
1121
|
if (isNonInteractive) {
|
|
1030
1122
|
const projectName = nameArg || "my-rebase-app";
|
|
1031
1123
|
const targetDirectory = path.resolve(process.cwd(), projectName);
|
|
@@ -1041,7 +1133,7 @@ async function promptForOptions(rawArgs, pm) {
|
|
|
1041
1133
|
introspect: args["--introspect"] || false,
|
|
1042
1134
|
preset: templateArg || "blog",
|
|
1043
1135
|
explicitPreset: !!templateArg,
|
|
1044
|
-
|
|
1136
|
+
headless: headlessArg ?? false,
|
|
1045
1137
|
pm,
|
|
1046
1138
|
pmCommands,
|
|
1047
1139
|
cloudProject: args["--project"] || void 0,
|
|
@@ -1052,14 +1144,14 @@ async function promptForOptions(rawArgs, pm) {
|
|
|
1052
1144
|
if (!process.stdin.isTTY) {
|
|
1053
1145
|
console.error(chalk.red("Cannot prompt: this is a non-interactive terminal (no TTY)."));
|
|
1054
1146
|
console.error(chalk.yellow(" Re-run with --yes to accept defaults, passing any choices as flags, e.g.:"));
|
|
1055
|
-
console.error(chalk.yellow(` rebase init ${nameArg || "my-app"} --yes --template blog
|
|
1056
|
-
console.error(chalk.gray(" Options: --template <blog|ecommerce|blank> --
|
|
1147
|
+
console.error(chalk.yellow(` rebase init ${nameArg || "my-app"} --yes --template blog`));
|
|
1148
|
+
console.error(chalk.gray(" Options: --template <blog|ecommerce|blank> --headless --database-url <url> --install --git"));
|
|
1057
1149
|
process.exit(1);
|
|
1058
1150
|
}
|
|
1059
1151
|
const questions = buildInitQuestions({
|
|
1060
1152
|
nameArg,
|
|
1061
1153
|
templateArg,
|
|
1062
|
-
|
|
1154
|
+
headlessArg,
|
|
1063
1155
|
hasGitFlag: !!args["--git"],
|
|
1064
1156
|
hasInstallFlag: !!args["--install"],
|
|
1065
1157
|
pm
|
|
@@ -1079,7 +1171,7 @@ async function promptForOptions(rawArgs, pm) {
|
|
|
1079
1171
|
introspect: answers.introspect || false,
|
|
1080
1172
|
preset: templateArg || answers.preset || "blog",
|
|
1081
1173
|
explicitPreset: !!templateArg,
|
|
1082
|
-
|
|
1174
|
+
headless: headlessArg ?? Boolean(answers.headless),
|
|
1083
1175
|
pm,
|
|
1084
1176
|
pmCommands,
|
|
1085
1177
|
cloudProject: args["--project"] || void 0,
|
|
@@ -1163,12 +1255,12 @@ async function createProject$1(options) {
|
|
|
1163
1255
|
const shipped = path.join(options.targetDirectory, from);
|
|
1164
1256
|
if (fs.existsSync(shipped)) fs.renameSync(shipped, path.join(options.targetDirectory, to));
|
|
1165
1257
|
}
|
|
1166
|
-
if (options.
|
|
1167
|
-
if (options.
|
|
1258
|
+
if (options.headless && options.explicitPreset) console.log(chalk.yellow(` Ignoring --template ${options.preset}: a headless project declares no collections.`));
|
|
1259
|
+
if (!options.headless) {
|
|
1168
1260
|
if (options.introspect && options.preset !== "blank") console.log(chalk.gray(" Using the blank template: collections will come from your database."));
|
|
1169
1261
|
await applyPreset(options.targetDirectory, options.introspect ? "blank" : options.preset);
|
|
1170
1262
|
}
|
|
1171
|
-
await
|
|
1263
|
+
await applyHeadless(options.targetDirectory, options.headless);
|
|
1172
1264
|
await replacePlaceholders(options);
|
|
1173
1265
|
await configureEnvFile(options.targetDirectory, options.databaseUrl);
|
|
1174
1266
|
if (options.git) {
|
|
@@ -1266,7 +1358,7 @@ async function createProject$1(options) {
|
|
|
1266
1358
|
console.log("");
|
|
1267
1359
|
const runDev = pmCommands.run("dev");
|
|
1268
1360
|
const runDbPush = pmCommands.run("db:push");
|
|
1269
|
-
const isBaas = options.
|
|
1361
|
+
const isBaas = options.headless;
|
|
1270
1362
|
const cdTarget = formatCdTarget(process.cwd(), options.targetDirectory);
|
|
1271
1363
|
if (cdTarget) console.log(` ${chalk.cyan("cd")} ${cdTarget}`);
|
|
1272
1364
|
if (!options.installDeps) console.log(` ${chalk.cyan(installCmd.join(" "))}`);
|
|
@@ -1336,18 +1428,31 @@ async function createProject$1(options) {
|
|
|
1336
1428
|
* presets directory so the final project is clean.
|
|
1337
1429
|
*/
|
|
1338
1430
|
/**
|
|
1339
|
-
* Reduce the scaffolded project to the chosen
|
|
1431
|
+
* Reduce the scaffolded project to the chosen shape.
|
|
1340
1432
|
*
|
|
1341
|
-
* The base template is the full
|
|
1342
|
-
* collections
|
|
1343
|
-
*
|
|
1433
|
+
* The base template is the full triad. `--headless` drops the frontend and the
|
|
1434
|
+
* declared collections — there is nothing to define, since the server derives
|
|
1435
|
+
* its API from the database — and overlays the files that differ.
|
|
1436
|
+
*
|
|
1437
|
+
* The config *package* stays, holding only `storageAuthorize`. Storage is not
|
|
1438
|
+
* under row-level security, so a deployment with file storage enabled and no
|
|
1439
|
+
* access model serves every user's files to every signed-in user; the server
|
|
1440
|
+
* refuses to boot in that state. Deleting the package outright would leave a
|
|
1441
|
+
* headless project with nowhere to put the hook, and the scaffold's own
|
|
1442
|
+
* docker-compose.yml enables storage — so the first `docker compose up` would
|
|
1443
|
+
* crash-loop.
|
|
1344
1444
|
*/
|
|
1345
|
-
async function
|
|
1346
|
-
if (
|
|
1347
|
-
|
|
1445
|
+
async function applyHeadless(targetDirectory, headless) {
|
|
1446
|
+
if (!headless) return;
|
|
1447
|
+
fs.rmSync(path.join(targetDirectory, "frontend"), {
|
|
1448
|
+
recursive: true,
|
|
1449
|
+
force: true
|
|
1450
|
+
});
|
|
1451
|
+
fs.rmSync(path.join(targetDirectory, "config", "collections"), {
|
|
1348
1452
|
recursive: true,
|
|
1349
1453
|
force: true
|
|
1350
1454
|
});
|
|
1455
|
+
for (const stray of ["admin.d.ts", "frontend-assets.d.ts"]) fs.rmSync(path.join(targetDirectory, "config", stray), { force: true });
|
|
1351
1456
|
fs.rmSync(path.join(targetDirectory, "backend", "src", "schema.generated.ts"), { force: true });
|
|
1352
1457
|
const overlayDir = path.resolve(cliRoot, "templates", "overlays", "baas");
|
|
1353
1458
|
if (!fs.existsSync(overlayDir)) {
|
|
@@ -1394,15 +1499,7 @@ function cleanupPresets(presetsDir) {
|
|
|
1394
1499
|
});
|
|
1395
1500
|
}
|
|
1396
1501
|
async function replacePlaceholders(options) {
|
|
1397
|
-
const filesToProcess =
|
|
1398
|
-
"package.json",
|
|
1399
|
-
"frontend/package.json",
|
|
1400
|
-
"backend/package.json",
|
|
1401
|
-
"config/package.json",
|
|
1402
|
-
"frontend/index.html",
|
|
1403
|
-
"pnpm-workspace.yaml",
|
|
1404
|
-
"README.md"
|
|
1405
|
-
];
|
|
1502
|
+
const filesToProcess = TEMPLATE_PLACEHOLDER_FILES;
|
|
1406
1503
|
const packageJsonPath = path.resolve(cliRoot, "package.json");
|
|
1407
1504
|
let cliVersion = "latest";
|
|
1408
1505
|
if (fs.existsSync(packageJsonPath)) cliVersion = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")).version || "latest";
|
|
@@ -1427,9 +1524,10 @@ async function replacePlaceholders(options) {
|
|
|
1427
1524
|
versionToUse = stdout.trim();
|
|
1428
1525
|
} catch {
|
|
1429
1526
|
try {
|
|
1527
|
+
const tag = cliVersion.includes("canary") ? "canary" : "latest";
|
|
1430
1528
|
const { stdout } = await execa(viewBin, [
|
|
1431
1529
|
"view",
|
|
1432
|
-
`${pkgName}@${
|
|
1530
|
+
`${pkgName}@${tag}`,
|
|
1433
1531
|
"version"
|
|
1434
1532
|
]);
|
|
1435
1533
|
if (!stdout.trim()) throw new Error("Not found");
|
|
@@ -1497,6 +1595,26 @@ async function findAvailablePort(startPort) {
|
|
|
1497
1595
|
while (!await isPortAvailable(port)) port++;
|
|
1498
1596
|
return port;
|
|
1499
1597
|
}
|
|
1598
|
+
/**
|
|
1599
|
+
* The CLI's own version, which is the runtime image tag a scaffolded project
|
|
1600
|
+
* pins. They move together: the CLI, the packages and the runtime image are cut
|
|
1601
|
+
* from one release, so the version that installed the project is the version
|
|
1602
|
+
* whose runtime can boot its bundle.
|
|
1603
|
+
*
|
|
1604
|
+
* Falls back to `latest` only when the CLI cannot read its own manifest — a
|
|
1605
|
+
* floating tag is worse than a pinned one, but better than a compose file that
|
|
1606
|
+
* names a version that was never published.
|
|
1607
|
+
*/
|
|
1608
|
+
function readCliVersion() {
|
|
1609
|
+
try {
|
|
1610
|
+
const manifest = path.resolve(cliRoot, "package.json");
|
|
1611
|
+
if (fs.existsSync(manifest)) {
|
|
1612
|
+
const pkg = JSON.parse(fs.readFileSync(manifest, "utf-8"));
|
|
1613
|
+
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
1614
|
+
}
|
|
1615
|
+
} catch {}
|
|
1616
|
+
return "latest";
|
|
1617
|
+
}
|
|
1500
1618
|
async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
1501
1619
|
const envExamplePath = path.join(targetDirectory, ".env.example");
|
|
1502
1620
|
const envPath = path.join(targetDirectory, ".env");
|
|
@@ -1506,11 +1624,22 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
1506
1624
|
const dbPassword = crypto.randomBytes(16).toString("hex");
|
|
1507
1625
|
const serviceKey = crypto.randomBytes(48).toString("base64");
|
|
1508
1626
|
let envContent = fs.readFileSync(envPath, "utf-8");
|
|
1627
|
+
envContent = envContent.replace(/^(# ║ {2})(Copy this file to \.env and fill in the values)( +║)$/m, (_match, open, old, close) => {
|
|
1628
|
+
const replacement = "Generated by `rebase init` — the secrets below are already set";
|
|
1629
|
+
const width = old.length + close.length - 1;
|
|
1630
|
+
return open + replacement.slice(0, width).padEnd(width, " ") + "║";
|
|
1631
|
+
});
|
|
1509
1632
|
envContent = envContent.replace(/^JWT_SECRET=.*$/m, `JWT_SECRET=${jwtSecret}`);
|
|
1510
1633
|
envContent = envContent.replace(/^#\s*REBASE_SERVICE_KEY=.*$/m, `REBASE_SERVICE_KEY=${serviceKey}`);
|
|
1634
|
+
const composeApiPort = /^PORT=(\d+)/m.exec(envContent)?.[1] ?? "3001";
|
|
1635
|
+
envContent = envContent.replace(/^#\s*CORS_ORIGINS=.*$/m, `CORS_ORIGINS=http://localhost:${composeApiPort}`);
|
|
1636
|
+
const runtimeVersion = readCliVersion();
|
|
1637
|
+
envContent = /^#?\s*REBASE_VERSION=.*$/m.test(envContent) ? envContent.replace(/^#?\s*REBASE_VERSION=.*$/m, `REBASE_VERSION=${runtimeVersion}`) : `${envContent.trimEnd()}\n\n# The Rebase runtime image tag docker-compose.yml pulls.\n# Change this and restart to upgrade; your project bundle is untouched.\nREBASE_VERSION=${runtimeVersion}\n`;
|
|
1511
1638
|
if (databaseUrl) {
|
|
1512
1639
|
if (/[\r\n]/.test(databaseUrl)) throw new Error("Invalid DATABASE_URL: multiline values are not allowed.");
|
|
1513
|
-
|
|
1640
|
+
const { pinSearchPath } = await import("@rebasepro/server-postgres");
|
|
1641
|
+
const pinnedUrl = pinSearchPath(databaseUrl);
|
|
1642
|
+
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${pinnedUrl}\nDATABASE_PASSWORD=${dbPassword}`);
|
|
1514
1643
|
} else {
|
|
1515
1644
|
const dbPort = await findAvailablePort(5432);
|
|
1516
1645
|
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=postgresql://rebase:${dbPassword}@localhost:${dbPort}/rebase?options=-c%20search_path=public&sslmode=disable\nDATABASE_PASSWORD=${dbPassword}`);
|
|
@@ -2014,21 +2143,51 @@ var ManifestError = class extends Error {
|
|
|
2014
2143
|
this.name = "ManifestError";
|
|
2015
2144
|
}
|
|
2016
2145
|
};
|
|
2017
|
-
var APP_TYPES = [
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2146
|
+
var APP_TYPES = ["backend", "static"];
|
|
2147
|
+
/**
|
|
2148
|
+
* App types that used to exist, and what replaced each one.
|
|
2149
|
+
*
|
|
2150
|
+
* Kept so a manifest written against the old vocabulary fails with the fix in
|
|
2151
|
+
* hand rather than with `must be one of: backend, static`, which tells a reader
|
|
2152
|
+
* what is wrong but not what to write instead.
|
|
2153
|
+
*/
|
|
2154
|
+
var REMOVED_APP_TYPES = {
|
|
2155
|
+
admin: "the admin panel is an ordinary static app now — declare it as { \"type\": \"static\", \"root\": \"frontend\", \"output\": \"frontend/dist\", \"path\": \"/admin\" }",
|
|
2156
|
+
custom: "who owns the server is a property of the backend now — declare { \"type\": \"backend\", \"runtime\": \"custom\" } instead of a separate app",
|
|
2157
|
+
mobile: "mobile apps are no longer declared in the manifest — nothing consumed this type. Remove the entry"
|
|
2158
|
+
};
|
|
2024
2159
|
/** Reserved because they name things in URLs and CLI output. */
|
|
2025
|
-
var RESERVED_APP_NAMES = new Set([
|
|
2160
|
+
var RESERVED_APP_NAMES = /* @__PURE__ */ new Set([
|
|
2026
2161
|
"api",
|
|
2027
2162
|
"health",
|
|
2028
2163
|
"metrics",
|
|
2029
2164
|
"livez",
|
|
2030
2165
|
"_rebase"
|
|
2031
2166
|
]);
|
|
2167
|
+
/**
|
|
2168
|
+
* Validate a static app's public base path.
|
|
2169
|
+
*
|
|
2170
|
+
* Normalizes to "no trailing slash, except for the root" so that mounting and
|
|
2171
|
+
* the `REBASE_APP_BASE` build variable have one shape to reason about.
|
|
2172
|
+
*/
|
|
2173
|
+
function checkAppPath(value, fieldPath, issues) {
|
|
2174
|
+
if (value === void 0) return void 0;
|
|
2175
|
+
if (typeof value !== "string" || !value.startsWith("/") || value.includes("..")) {
|
|
2176
|
+
issues.push({
|
|
2177
|
+
path: fieldPath,
|
|
2178
|
+
message: "must be an absolute path like \"/admin\""
|
|
2179
|
+
});
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
if (value !== "/" && value.endsWith("/")) {
|
|
2183
|
+
issues.push({
|
|
2184
|
+
path: fieldPath,
|
|
2185
|
+
message: "must not end with a slash — write \"/admin\", not \"/admin/\""
|
|
2186
|
+
});
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
return value;
|
|
2190
|
+
}
|
|
2032
2191
|
function isRecord(value) {
|
|
2033
2192
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2034
2193
|
}
|
|
@@ -2071,6 +2230,73 @@ function checkRelativePath(value, fieldPath, issues, { required }) {
|
|
|
2071
2230
|
}
|
|
2072
2231
|
return value;
|
|
2073
2232
|
}
|
|
2233
|
+
/** Fields each app type understands. Anything else is a typo or a newer CLI. */
|
|
2234
|
+
var KNOWN_APP_FIELDS = {
|
|
2235
|
+
backend: [
|
|
2236
|
+
"type",
|
|
2237
|
+
"runtime",
|
|
2238
|
+
"config",
|
|
2239
|
+
"functions",
|
|
2240
|
+
"crons",
|
|
2241
|
+
"schema",
|
|
2242
|
+
"usersCollection",
|
|
2243
|
+
"dockerfile",
|
|
2244
|
+
"context",
|
|
2245
|
+
"port"
|
|
2246
|
+
],
|
|
2247
|
+
static: [
|
|
2248
|
+
"type",
|
|
2249
|
+
"root",
|
|
2250
|
+
"build",
|
|
2251
|
+
"output",
|
|
2252
|
+
"path",
|
|
2253
|
+
"spa"
|
|
2254
|
+
]
|
|
2255
|
+
};
|
|
2256
|
+
/**
|
|
2257
|
+
* Report a field this CLI does not recognise.
|
|
2258
|
+
*
|
|
2259
|
+
* A warning rather than an error, and the distinction matters in both
|
|
2260
|
+
* directions. `"pathh": "/admin"` is a typo that would otherwise be silently
|
|
2261
|
+
* dropped — the app builds for `/`, mounts at `/`, and the only symptom is that
|
|
2262
|
+
* it is not where you put it. But an unknown field is also what an *older* CLI
|
|
2263
|
+
* sees when it opens a manifest written for a newer one, and refusing to build
|
|
2264
|
+
* over a field that is simply from the future would make every manifest addition
|
|
2265
|
+
* a breaking change.
|
|
2266
|
+
*
|
|
2267
|
+
* So: name it, suggest the near-miss, and carry on.
|
|
2268
|
+
*/
|
|
2269
|
+
function warnUnknownFields(name, raw, type) {
|
|
2270
|
+
const known = KNOWN_APP_FIELDS[type];
|
|
2271
|
+
if (!known) return;
|
|
2272
|
+
for (const field of Object.keys(raw)) {
|
|
2273
|
+
if (known.includes(field)) continue;
|
|
2274
|
+
const suggestion = known.find((candidate) => isNearMiss(candidate, field));
|
|
2275
|
+
console.warn(`⚠ rebase.json: apps.${name}.${field} is not a field this CLI knows.` + (suggestion ? ` Did you mean "${suggestion}"?` : " It is ignored — your CLI may be older than this manifest."));
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
/** One edit apart, ignoring case: enough for a typo, tight enough to stay quiet. */
|
|
2279
|
+
function isNearMiss(a, b) {
|
|
2280
|
+
const x = a.toLowerCase();
|
|
2281
|
+
const y = b.toLowerCase();
|
|
2282
|
+
if (x === y) return true;
|
|
2283
|
+
if (Math.abs(x.length - y.length) > 1) return false;
|
|
2284
|
+
const [shorter, longer] = x.length <= y.length ? [x, y] : [y, x];
|
|
2285
|
+
let i = 0;
|
|
2286
|
+
let j = 0;
|
|
2287
|
+
let edits = 0;
|
|
2288
|
+
while (i < shorter.length && j < longer.length) {
|
|
2289
|
+
if (shorter[i] === longer[j]) {
|
|
2290
|
+
i++;
|
|
2291
|
+
j++;
|
|
2292
|
+
continue;
|
|
2293
|
+
}
|
|
2294
|
+
if (++edits > 1) return false;
|
|
2295
|
+
if (shorter.length === longer.length) i++;
|
|
2296
|
+
j++;
|
|
2297
|
+
}
|
|
2298
|
+
return edits + (longer.length - j) + (shorter.length - i) <= 1;
|
|
2299
|
+
}
|
|
2074
2300
|
function validateApp(name, raw, issues) {
|
|
2075
2301
|
const base = `apps.${name}`;
|
|
2076
2302
|
if (!isRecord(raw)) {
|
|
@@ -2081,6 +2307,13 @@ function validateApp(name, raw, issues) {
|
|
|
2081
2307
|
return;
|
|
2082
2308
|
}
|
|
2083
2309
|
const type = raw.type;
|
|
2310
|
+
if (typeof type === "string" && REMOVED_APP_TYPES[type]) {
|
|
2311
|
+
issues.push({
|
|
2312
|
+
path: `${base}.type`,
|
|
2313
|
+
message: `"${type}" is no longer an app type — ${REMOVED_APP_TYPES[type]}`
|
|
2314
|
+
});
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2084
2317
|
if (typeof type !== "string" || !APP_TYPES.includes(type)) {
|
|
2085
2318
|
issues.push({
|
|
2086
2319
|
path: `${base}.type`,
|
|
@@ -2088,18 +2321,39 @@ function validateApp(name, raw, issues) {
|
|
|
2088
2321
|
});
|
|
2089
2322
|
return;
|
|
2090
2323
|
}
|
|
2324
|
+
warnUnknownFields(name, raw, type);
|
|
2091
2325
|
switch (type) {
|
|
2092
|
-
case "backend":
|
|
2326
|
+
case "backend": {
|
|
2093
2327
|
checkRelativePath(raw.config, `${base}.config`, issues, { required: false });
|
|
2094
2328
|
checkRelativePath(raw.functions, `${base}.functions`, issues, { required: false });
|
|
2095
2329
|
checkRelativePath(raw.crons, `${base}.crons`, issues, { required: false });
|
|
2096
2330
|
checkRelativePath(raw.schema, `${base}.schema`, issues, { required: false });
|
|
2097
2331
|
checkRelativePath(raw.usersCollection, `${base}.usersCollection`, issues, { required: false });
|
|
2098
|
-
if (raw.mode !== void 0
|
|
2332
|
+
if (raw.mode !== void 0) issues.push({
|
|
2099
2333
|
path: `${base}.mode`,
|
|
2100
|
-
message: "
|
|
2334
|
+
message: "is no longer a field — collections come from the config directory when it exists, and are introspected from the database when it does not"
|
|
2335
|
+
});
|
|
2336
|
+
const custom = raw.runtime === "custom";
|
|
2337
|
+
if (raw.runtime !== "managed" && !custom) issues.push({
|
|
2338
|
+
path: `${base}.runtime`,
|
|
2339
|
+
message: "is required, and must be \"managed\" or \"custom\""
|
|
2340
|
+
});
|
|
2341
|
+
for (const field of [
|
|
2342
|
+
"dockerfile",
|
|
2343
|
+
"context",
|
|
2344
|
+
"port"
|
|
2345
|
+
]) if (raw[field] !== void 0 && !custom) issues.push({
|
|
2346
|
+
path: `${base}.${field}`,
|
|
2347
|
+
message: "only applies to a custom runtime — set \"runtime\": \"custom\" to build your own image"
|
|
2348
|
+
});
|
|
2349
|
+
checkRelativePath(raw.dockerfile, `${base}.dockerfile`, issues, { required: false });
|
|
2350
|
+
checkRelativePath(raw.context, `${base}.context`, issues, { required: false });
|
|
2351
|
+
if (raw.port !== void 0 && (typeof raw.port !== "number" || !Number.isInteger(raw.port))) issues.push({
|
|
2352
|
+
path: `${base}.port`,
|
|
2353
|
+
message: "must be an integer"
|
|
2101
2354
|
});
|
|
2102
2355
|
return raw;
|
|
2356
|
+
}
|
|
2103
2357
|
case "static":
|
|
2104
2358
|
checkRelativePath(raw.root, `${base}.root`, issues, { required: true });
|
|
2105
2359
|
checkRelativePath(raw.output, `${base}.output`, issues, { required: true });
|
|
@@ -2111,37 +2365,7 @@ function validateApp(name, raw, issues) {
|
|
|
2111
2365
|
path: `${base}.spa`,
|
|
2112
2366
|
message: "must be a boolean"
|
|
2113
2367
|
});
|
|
2114
|
-
|
|
2115
|
-
case "admin": {
|
|
2116
|
-
const mode = raw.mode ?? "hosted";
|
|
2117
|
-
if (mode !== "hosted" && mode !== "bundled") {
|
|
2118
|
-
issues.push({
|
|
2119
|
-
path: `${base}.mode`,
|
|
2120
|
-
message: "must be \"hosted\" or \"bundled\""
|
|
2121
|
-
});
|
|
2122
|
-
return;
|
|
2123
|
-
}
|
|
2124
|
-
if (mode === "bundled") {
|
|
2125
|
-
checkRelativePath(raw.root, `${base}.root`, issues, { required: true });
|
|
2126
|
-
checkRelativePath(raw.output, `${base}.output`, issues, { required: true });
|
|
2127
|
-
}
|
|
2128
|
-
return raw;
|
|
2129
|
-
}
|
|
2130
|
-
case "mobile": {
|
|
2131
|
-
const platform = raw.platform;
|
|
2132
|
-
if (platform !== "ios" && platform !== "android" && platform !== "other") issues.push({
|
|
2133
|
-
path: `${base}.platform`,
|
|
2134
|
-
message: "must be \"ios\", \"android\" or \"other\""
|
|
2135
|
-
});
|
|
2136
|
-
return raw;
|
|
2137
|
-
}
|
|
2138
|
-
case "custom":
|
|
2139
|
-
checkRelativePath(raw.dockerfile, `${base}.dockerfile`, issues, { required: false });
|
|
2140
|
-
checkRelativePath(raw.context, `${base}.context`, issues, { required: false });
|
|
2141
|
-
if (raw.port !== void 0 && (typeof raw.port !== "number" || !Number.isInteger(raw.port))) issues.push({
|
|
2142
|
-
path: `${base}.port`,
|
|
2143
|
-
message: "must be an integer"
|
|
2144
|
-
});
|
|
2368
|
+
checkAppPath(raw.path, `${base}.path`, issues);
|
|
2145
2369
|
return raw;
|
|
2146
2370
|
default: return;
|
|
2147
2371
|
}
|
|
@@ -2155,10 +2379,13 @@ function validateManifest(raw) {
|
|
|
2155
2379
|
path: "",
|
|
2156
2380
|
message: `${MANIFEST_FILENAME} must contain a JSON object`
|
|
2157
2381
|
}] };
|
|
2158
|
-
if (typeof raw.
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2382
|
+
if (typeof raw.rebase !== "string" || raw.rebase.trim() === "") {
|
|
2383
|
+
const message = typeof raw.runtime === "string" ? `is required, e.g. "^1" — this is the top-level "runtime" key renamed, because "runtime" now means "managed" or "custom" on the backend app` : `is required, e.g. "^1"`;
|
|
2384
|
+
issues.push({
|
|
2385
|
+
path: "rebase",
|
|
2386
|
+
message
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2162
2389
|
if (!isRecord(raw.apps)) {
|
|
2163
2390
|
issues.push({
|
|
2164
2391
|
path: "apps",
|
|
@@ -2192,55 +2419,148 @@ function validateManifest(raw) {
|
|
|
2192
2419
|
path: "apps",
|
|
2193
2420
|
message: "a project may declare at most one backend app"
|
|
2194
2421
|
});
|
|
2422
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
2423
|
+
for (const [name, app] of Object.entries(apps)) {
|
|
2424
|
+
if (app.type !== "static") continue;
|
|
2425
|
+
const at = app.path ?? "/";
|
|
2426
|
+
const owner = byPath.get(at);
|
|
2427
|
+
if (owner) {
|
|
2428
|
+
issues.push({
|
|
2429
|
+
path: `apps.${name}.path`,
|
|
2430
|
+
message: `two apps cannot serve the same path — "${owner}" is already at "${at}"`
|
|
2431
|
+
});
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
byPath.set(at, name);
|
|
2435
|
+
}
|
|
2436
|
+
const storage = validateStorageSources(raw.storage, issues);
|
|
2195
2437
|
if (issues.length > 0) return { issues };
|
|
2196
2438
|
return {
|
|
2197
2439
|
manifest: {
|
|
2198
2440
|
$schema: typeof raw.$schema === "string" ? raw.$schema : void 0,
|
|
2199
|
-
|
|
2200
|
-
apps
|
|
2441
|
+
rebase: raw.rebase,
|
|
2442
|
+
apps,
|
|
2443
|
+
...storage ? { storage } : {}
|
|
2201
2444
|
},
|
|
2202
2445
|
issues
|
|
2203
2446
|
};
|
|
2204
2447
|
}
|
|
2205
2448
|
/**
|
|
2449
|
+
* Validate the `storage` block — which buckets this project uses.
|
|
2450
|
+
*
|
|
2451
|
+
* Absent means one default source, so `undefined` is a valid answer and not an
|
|
2452
|
+
* issue. Everything else is checked strictly, because each key here becomes the
|
|
2453
|
+
* suffix of a set of environment variables: a key that cannot become a variable
|
|
2454
|
+
* name, or two keys that become the *same* one, are failures worth catching
|
|
2455
|
+
* while someone is looking at the file rather than at a tenant serving one
|
|
2456
|
+
* bucket's files with another's credentials.
|
|
2457
|
+
*/
|
|
2458
|
+
function validateStorageSources(raw, issues) {
|
|
2459
|
+
if (raw === void 0) return void 0;
|
|
2460
|
+
if (!isRecord(raw)) {
|
|
2461
|
+
issues.push({
|
|
2462
|
+
path: "storage",
|
|
2463
|
+
message: "must be an object keyed by storage source name"
|
|
2464
|
+
});
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
const sources = {};
|
|
2468
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2469
|
+
if (!isRecord(value)) {
|
|
2470
|
+
issues.push({
|
|
2471
|
+
path: `storage.${key}`,
|
|
2472
|
+
message: "must be an object"
|
|
2473
|
+
});
|
|
2474
|
+
continue;
|
|
2475
|
+
}
|
|
2476
|
+
if (typeof value.engine !== "string" || value.engine.trim() === "") {
|
|
2477
|
+
issues.push({
|
|
2478
|
+
path: `storage.${key}.engine`,
|
|
2479
|
+
message: "is required, e.g. \"s3\", \"gcs\" or \"local\""
|
|
2480
|
+
});
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
if (value.transport !== void 0 && value.transport !== "server" && value.transport !== "direct") {
|
|
2484
|
+
issues.push({
|
|
2485
|
+
path: `storage.${key}.transport`,
|
|
2486
|
+
message: "must be \"server\" or \"direct\""
|
|
2487
|
+
});
|
|
2488
|
+
continue;
|
|
2489
|
+
}
|
|
2490
|
+
if (value.label !== void 0 && typeof value.label !== "string") {
|
|
2491
|
+
issues.push({
|
|
2492
|
+
path: `storage.${key}.label`,
|
|
2493
|
+
message: "must be a string"
|
|
2494
|
+
});
|
|
2495
|
+
continue;
|
|
2496
|
+
}
|
|
2497
|
+
try {
|
|
2498
|
+
storageEnvSuffix(key);
|
|
2499
|
+
} catch {
|
|
2500
|
+
issues.push({
|
|
2501
|
+
path: `storage.${key}`,
|
|
2502
|
+
message: "name cannot become an environment variable suffix — use a name containing at least one letter or digit"
|
|
2503
|
+
});
|
|
2504
|
+
continue;
|
|
2505
|
+
}
|
|
2506
|
+
sources[key] = {
|
|
2507
|
+
engine: value.engine,
|
|
2508
|
+
...value.transport !== void 0 ? { transport: value.transport } : {},
|
|
2509
|
+
...value.label !== void 0 ? { label: value.label } : {}
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
const collision = findStorageSuffixCollision(Object.keys(sources));
|
|
2513
|
+
if (collision) issues.push({
|
|
2514
|
+
path: `storage.${collision.b}`,
|
|
2515
|
+
message: `maps to the same environment variable suffix ("${collision.suffix || "(none)"}") as "${collision.a}", so the two would read each other's configuration — rename one of them`
|
|
2516
|
+
});
|
|
2517
|
+
return Object.keys(sources).length > 0 ? sources : void 0;
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2206
2520
|
* Infer a manifest from a directory that does not have one.
|
|
2207
2521
|
*
|
|
2208
2522
|
* This mirrors exactly what the template scaffolds, which is what makes adopting
|
|
2209
2523
|
* the manifest a no-op for existing projects: the synthesized result is what
|
|
2210
2524
|
* they would have written by hand.
|
|
2211
2525
|
*
|
|
2212
|
-
*
|
|
2213
|
-
*
|
|
2214
|
-
*
|
|
2215
|
-
*
|
|
2526
|
+
* The backend's `runtime` is inferred from whether the repository declares a
|
|
2527
|
+
* **Dockerfile** — the thing that actually builds an image — and from nothing
|
|
2528
|
+
* else. It used to be inferred from the presence of `backend/src/index.ts`,
|
|
2529
|
+
* which every scaffolded project had whether or not it wanted its own server, so
|
|
2530
|
+
* projects predating the manifest silently landed on the custom runtime and paid
|
|
2531
|
+
* for it (see `docs/cloud-deploy-workspace-vendoring.md`).
|
|
2216
2532
|
*/
|
|
2217
2533
|
function synthesizeManifest(projectRoot) {
|
|
2218
2534
|
const exists = (relative) => fs.existsSync(path.join(projectRoot, relative));
|
|
2219
2535
|
const apps = {};
|
|
2220
2536
|
const hasConfig = exists(DEFAULT_CONFIG_DIR);
|
|
2221
2537
|
const hasBackend = exists("backend");
|
|
2222
|
-
const
|
|
2223
|
-
if (hasBackend
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2538
|
+
const dockerfile = ["Dockerfile", "backend/Dockerfile"].find(exists);
|
|
2539
|
+
if (hasBackend || hasConfig) {
|
|
2540
|
+
const backend = dockerfile ? {
|
|
2541
|
+
type: "backend",
|
|
2542
|
+
runtime: "custom",
|
|
2543
|
+
dockerfile,
|
|
2544
|
+
context: "."
|
|
2545
|
+
} : {
|
|
2546
|
+
type: "backend",
|
|
2547
|
+
runtime: "managed"
|
|
2548
|
+
};
|
|
2231
2549
|
if (exists("backend/functions")) backend.functions = DEFAULT_FUNCTIONS_DIR;
|
|
2232
2550
|
if (exists("backend/crons")) backend.crons = DEFAULT_CRONS_DIR;
|
|
2233
2551
|
apps.backend = backend;
|
|
2552
|
+
if (!dockerfile && exists("backend/src/index.ts")) console.warn("⚠ backend/src/index.ts exists but this project's backend is managed — it is\n never loaded. Delete it, or run `rebase eject` to make it the entrypoint.");
|
|
2234
2553
|
}
|
|
2235
2554
|
if (exists("frontend")) apps.web = {
|
|
2236
2555
|
type: "static",
|
|
2237
2556
|
root: "frontend",
|
|
2238
2557
|
build: "npm run build --workspace frontend",
|
|
2239
2558
|
output: "frontend/dist",
|
|
2559
|
+
path: "/",
|
|
2240
2560
|
spa: true
|
|
2241
2561
|
};
|
|
2242
2562
|
return {
|
|
2243
|
-
|
|
2563
|
+
rebase: "^1",
|
|
2244
2564
|
apps
|
|
2245
2565
|
};
|
|
2246
2566
|
}
|
|
@@ -2282,7 +2602,7 @@ function writeManifest(projectRoot, manifest) {
|
|
|
2282
2602
|
const filePath = manifestPath(projectRoot);
|
|
2283
2603
|
const ordered = {
|
|
2284
2604
|
$schema: manifest.$schema ?? "https://rebase.pro/schemas/rebase.json",
|
|
2285
|
-
|
|
2605
|
+
rebase: manifest.rebase,
|
|
2286
2606
|
apps: manifest.apps
|
|
2287
2607
|
};
|
|
2288
2608
|
fs.writeFileSync(filePath, `${JSON.stringify(ordered, null, 4)}\n`, "utf8");
|
|
@@ -2301,13 +2621,8 @@ function buildableApps(manifest) {
|
|
|
2301
2621
|
name,
|
|
2302
2622
|
app
|
|
2303
2623
|
}));
|
|
2304
|
-
const rank = (app) =>
|
|
2305
|
-
|
|
2306
|
-
if (app.type === "admin") return 1;
|
|
2307
|
-
if (app.type === "static") return 2;
|
|
2308
|
-
return 3;
|
|
2309
|
-
};
|
|
2310
|
-
return entries.filter(({ app }) => app.type !== "mobile").sort((a, b) => rank(a.app) - rank(b.app));
|
|
2624
|
+
const rank = (app) => app.type === "backend" ? 0 : 1;
|
|
2625
|
+
return entries.sort((a, b) => rank(a.app) - rank(b.app));
|
|
2311
2626
|
}
|
|
2312
2627
|
/**
|
|
2313
2628
|
* Decide whether a project can run on the managed runtime, and say why not.
|
|
@@ -2317,28 +2632,45 @@ function buildableApps(manifest) {
|
|
|
2317
2632
|
* verdict.
|
|
2318
2633
|
*/
|
|
2319
2634
|
function assessManagedCompatibility(manifest) {
|
|
2320
|
-
const reasons = [];
|
|
2321
2635
|
const backend = findBackendApp(manifest);
|
|
2322
|
-
if (!backend) {
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2636
|
+
if (!backend) return {
|
|
2637
|
+
eligible: false,
|
|
2638
|
+
reasons: ["No backend app is declared in this repository. Only the repository that declares the backend selects the runtime."]
|
|
2639
|
+
};
|
|
2640
|
+
if (backend.app.runtime === "custom") return {
|
|
2641
|
+
eligible: false,
|
|
2642
|
+
reasons: [`App "${backend.name}" declares runtime: "custom", so it builds and runs its own image. The managed runtime boots the platform image with your bundle; the custom runtime deploys exactly the same way, from your Dockerfile.`]
|
|
2643
|
+
};
|
|
2328
2644
|
return {
|
|
2329
|
-
eligible:
|
|
2330
|
-
reasons
|
|
2645
|
+
eligible: true,
|
|
2646
|
+
reasons: []
|
|
2331
2647
|
};
|
|
2332
2648
|
}
|
|
2333
|
-
/**
|
|
2334
|
-
|
|
2649
|
+
/**
|
|
2650
|
+
* Resolve a backend app's directories against the conventions it omits.
|
|
2651
|
+
*
|
|
2652
|
+
* `hasCollections` replaces the old `mode: "cms" | "baas"` field. Where the
|
|
2653
|
+
* collections come from was never an independent choice: either they are
|
|
2654
|
+
* declared in code and the bundle ships them, or they are not and the runtime
|
|
2655
|
+
* introspects the live database at boot. Declaring it separately only created
|
|
2656
|
+
* the contradictory state — code-first declared, no collections anywhere.
|
|
2657
|
+
*
|
|
2658
|
+
* `hasConfig` is deliberately a **separate** question. A headless project has no
|
|
2659
|
+
* `config/collections`, but it may still ship a config package — that is where
|
|
2660
|
+
* the `storageAuthorize` hook lives, and storage is not under row-level
|
|
2661
|
+
* security, so without one the server refuses to boot with storage enabled.
|
|
2662
|
+
* Collapsing the two would leave a headless project nowhere to put it.
|
|
2663
|
+
*/
|
|
2664
|
+
function resolveBackendPaths(app, projectRoot) {
|
|
2665
|
+
const config = app.config ?? "config";
|
|
2335
2666
|
return {
|
|
2336
|
-
config
|
|
2667
|
+
config,
|
|
2337
2668
|
functions: app.functions ?? "backend/functions",
|
|
2338
2669
|
crons: app.crons ?? "backend/crons",
|
|
2339
2670
|
schema: app.schema ?? "backend/src/schema.generated.ts",
|
|
2340
2671
|
usersCollection: app.usersCollection ?? "collections/users",
|
|
2341
|
-
|
|
2672
|
+
hasConfig: fs.existsSync(path.join(projectRoot, config)),
|
|
2673
|
+
hasCollections: fs.existsSync(path.join(projectRoot, config, "collections"))
|
|
2342
2674
|
};
|
|
2343
2675
|
}
|
|
2344
2676
|
//#endregion
|
|
@@ -2401,22 +2733,19 @@ function devRuntimeEnv(projectRoot) {
|
|
|
2401
2733
|
REBASE_DEV_CONFIG: "config",
|
|
2402
2734
|
REBASE_DEV_FUNCTIONS: "backend/functions",
|
|
2403
2735
|
REBASE_DEV_CRONS: "backend/crons",
|
|
2404
|
-
REBASE_DEV_SCHEMA: "backend/src/schema.generated.ts"
|
|
2405
|
-
REBASE_DEV_MODE: "cms"
|
|
2736
|
+
REBASE_DEV_SCHEMA: "backend/src/schema.generated.ts"
|
|
2406
2737
|
};
|
|
2407
2738
|
try {
|
|
2408
2739
|
const backend = findBackendApp(loadManifest(projectRoot).manifest);
|
|
2409
2740
|
if (backend) {
|
|
2410
|
-
const paths = resolveBackendPaths(backend.app);
|
|
2741
|
+
const paths = resolveBackendPaths(backend.app, projectRoot);
|
|
2411
2742
|
result.REBASE_DEV_CONFIG = paths.config;
|
|
2412
2743
|
result.REBASE_DEV_FUNCTIONS = paths.functions;
|
|
2413
2744
|
result.REBASE_DEV_CRONS = paths.crons;
|
|
2414
2745
|
result.REBASE_DEV_SCHEMA = paths.schema;
|
|
2415
|
-
result.REBASE_DEV_MODE = paths.mode;
|
|
2416
2746
|
result.REBASE_DEV_APP = backend.name;
|
|
2417
2747
|
}
|
|
2418
2748
|
} catch {}
|
|
2419
|
-
if (!fs.existsSync(path.join(projectRoot, result.REBASE_DEV_CONFIG))) result.REBASE_DEV_MODE = "baas";
|
|
2420
2749
|
return result;
|
|
2421
2750
|
}
|
|
2422
2751
|
/** Well-known filename the backend writes its actual port to. */
|
|
@@ -2825,7 +3154,7 @@ ${chalk.green.bold("Description")}
|
|
|
2825
3154
|
*/
|
|
2826
3155
|
var DEFAULT_BUNDLE_DIR = "dist-bundle";
|
|
2827
3156
|
/** Packages whose presence means the bundle cannot run on a stock runtime image. */
|
|
2828
|
-
var KNOWN_NATIVE_PACKAGES = new Set([
|
|
3157
|
+
var KNOWN_NATIVE_PACKAGES = /* @__PURE__ */ new Set([
|
|
2829
3158
|
"sharp",
|
|
2830
3159
|
"canvas",
|
|
2831
3160
|
"bcrypt",
|
|
@@ -2841,7 +3170,7 @@ var KNOWN_NATIVE_PACKAGES = new Set([
|
|
|
2841
3170
|
"pg-native"
|
|
2842
3171
|
]);
|
|
2843
3172
|
/** Dependencies supplied by the runtime image itself, not by the bundle. */
|
|
2844
|
-
var RUNTIME_PROVIDED = new Set([
|
|
3173
|
+
var RUNTIME_PROVIDED = /* @__PURE__ */ new Set([
|
|
2845
3174
|
"@rebasepro/server",
|
|
2846
3175
|
"@rebasepro/types",
|
|
2847
3176
|
"@rebasepro/client",
|
|
@@ -3024,16 +3353,27 @@ async function writeBundleTsconfig(projectRoot, outDir, includes, skipTypeCheck)
|
|
|
3024
3353
|
* says exactly how to proceed, while a false positive would hand back the crash
|
|
3025
3354
|
* loop this exists to prevent.
|
|
3026
3355
|
*/
|
|
3027
|
-
function detectStorageAuthorize(compiledConfigDir) {
|
|
3356
|
+
function detectStorageAuthorize(compiledConfigDir, depth = 0) {
|
|
3028
3357
|
const indexPath = [
|
|
3029
3358
|
".js",
|
|
3030
3359
|
".mjs",
|
|
3031
3360
|
".ts"
|
|
3032
3361
|
].map((ext) => path.join(compiledConfigDir, `index${ext}`)).find((candidate) => fs.existsSync(candidate));
|
|
3033
3362
|
if (!indexPath) return false;
|
|
3363
|
+
return moduleExportsStorageAuthorize(indexPath, depth);
|
|
3364
|
+
}
|
|
3365
|
+
/**
|
|
3366
|
+
* Whether one compiled module re-exports or defines `storageAuthorize`.
|
|
3367
|
+
*
|
|
3368
|
+
* Split out from {@link detectStorageAuthorize} so a wildcard re-export can be
|
|
3369
|
+
* followed. `export * from "./storage.js"` is an ordinary way to write a config
|
|
3370
|
+
* barrel, and treating it as "no hook" rejected deploys that were correct — with
|
|
3371
|
+
* a message telling the developer to add a hook they had already written.
|
|
3372
|
+
*/
|
|
3373
|
+
function moduleExportsStorageAuthorize(modulePath, depth) {
|
|
3034
3374
|
let source;
|
|
3035
3375
|
try {
|
|
3036
|
-
source = fs.readFileSync(
|
|
3376
|
+
source = fs.readFileSync(modulePath, "utf8");
|
|
3037
3377
|
} catch {
|
|
3038
3378
|
return false;
|
|
3039
3379
|
}
|
|
@@ -3042,9 +3382,48 @@ function detectStorageAuthorize(compiledConfigDir) {
|
|
|
3042
3382
|
const parts = entry.split(/\bas\b/);
|
|
3043
3383
|
return parts[parts.length - 1].trim();
|
|
3044
3384
|
}).includes("storageAuthorize")) return true;
|
|
3385
|
+
if (depth < 3) for (const clause of source.matchAll(/\bexport\s*\*\s*from\s*["']([^"']+)["']/g)) {
|
|
3386
|
+
const specifier = clause[1];
|
|
3387
|
+
if (!specifier.startsWith(".")) continue;
|
|
3388
|
+
const resolved = resolveRelativeModule(path.dirname(modulePath), specifier);
|
|
3389
|
+
if (resolved && moduleExportsStorageAuthorize(resolved, depth + 1)) return true;
|
|
3390
|
+
}
|
|
3045
3391
|
return false;
|
|
3046
3392
|
}
|
|
3047
3393
|
/**
|
|
3394
|
+
* Resolve a relative ESM specifier to a file on disk.
|
|
3395
|
+
*
|
|
3396
|
+
* Compiled output carries explicit `.js` extensions, but the same function reads
|
|
3397
|
+
* TypeScript sources during a source boot, where the specifier may be
|
|
3398
|
+
* extensionless or point at a directory index.
|
|
3399
|
+
*/
|
|
3400
|
+
function resolveRelativeModule(fromDir, specifier) {
|
|
3401
|
+
const base = path.resolve(fromDir, specifier);
|
|
3402
|
+
const candidates = [
|
|
3403
|
+
base,
|
|
3404
|
+
`${base}.js`,
|
|
3405
|
+
`${base}.mjs`,
|
|
3406
|
+
`${base}.ts`,
|
|
3407
|
+
path.join(base, "index.js"),
|
|
3408
|
+
path.join(base, "index.mjs"),
|
|
3409
|
+
path.join(base, "index.ts")
|
|
3410
|
+
];
|
|
3411
|
+
for (const candidate of candidates) try {
|
|
3412
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
3413
|
+
} catch {
|
|
3414
|
+
continue;
|
|
3415
|
+
}
|
|
3416
|
+
if (/\.js$/.test(base)) {
|
|
3417
|
+
const asTs = base.replace(/\.js$/, ".ts");
|
|
3418
|
+
try {
|
|
3419
|
+
if (fs.existsSync(asTs) && fs.statSync(asTs).isFile()) return asTs;
|
|
3420
|
+
} catch {
|
|
3421
|
+
return null;
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
return null;
|
|
3425
|
+
}
|
|
3426
|
+
/**
|
|
3048
3427
|
* Detect native code in the dependency closure.
|
|
3049
3428
|
*
|
|
3050
3429
|
* Walks declared runtime dependencies breadth-first through `node_modules`,
|
|
@@ -3200,6 +3579,139 @@ function collectDeclaredDependencies(projectRoot) {
|
|
|
3200
3579
|
return declared;
|
|
3201
3580
|
}
|
|
3202
3581
|
/**
|
|
3582
|
+
* Lowest version a range could resolve to, or null if it is not a range.
|
|
3583
|
+
*
|
|
3584
|
+
* Kept deliberately tiny and local. The published range grammar here is a caret,
|
|
3585
|
+
* a tilde, an exact version or a `>=` floor, and the alternative — a semver
|
|
3586
|
+
* dependency in the CLI — buys breadth this does not need.
|
|
3587
|
+
*/
|
|
3588
|
+
function lowerBoundOf(range) {
|
|
3589
|
+
const raw = range.trim().replace(/^[\^~]/, "").replace(/^>=\s*/, "").replace(/^v/, "");
|
|
3590
|
+
if (!/^\d+(\.\d+){0,2}$/.test(raw)) return null;
|
|
3591
|
+
const [major, minor = 0, patch = 0] = raw.split(".").map(Number);
|
|
3592
|
+
return [
|
|
3593
|
+
major,
|
|
3594
|
+
minor,
|
|
3595
|
+
patch
|
|
3596
|
+
];
|
|
3597
|
+
}
|
|
3598
|
+
/** Highest version a range could resolve to (exclusive), or null. */
|
|
3599
|
+
function upperBoundOf(range) {
|
|
3600
|
+
const trimmed = range.trim();
|
|
3601
|
+
const min = lowerBoundOf(trimmed);
|
|
3602
|
+
if (!min) return null;
|
|
3603
|
+
if (trimmed.startsWith(">=")) return null;
|
|
3604
|
+
const [major, minor, patch] = min;
|
|
3605
|
+
if (trimmed.startsWith("^")) {
|
|
3606
|
+
if (major > 0) return [
|
|
3607
|
+
major + 1,
|
|
3608
|
+
0,
|
|
3609
|
+
0
|
|
3610
|
+
];
|
|
3611
|
+
if (minor > 0) return [
|
|
3612
|
+
0,
|
|
3613
|
+
minor + 1,
|
|
3614
|
+
0
|
|
3615
|
+
];
|
|
3616
|
+
return [
|
|
3617
|
+
0,
|
|
3618
|
+
0,
|
|
3619
|
+
patch + 1
|
|
3620
|
+
];
|
|
3621
|
+
}
|
|
3622
|
+
if (trimmed.startsWith("~")) return trimmed.replace(/^~v?/, "").split(".").length >= 2 ? [
|
|
3623
|
+
major,
|
|
3624
|
+
minor + 1,
|
|
3625
|
+
0
|
|
3626
|
+
] : [
|
|
3627
|
+
major + 1,
|
|
3628
|
+
0,
|
|
3629
|
+
0
|
|
3630
|
+
];
|
|
3631
|
+
const parts = trimmed.replace(/^v/, "").split(".").length;
|
|
3632
|
+
if (parts === 1) return [
|
|
3633
|
+
major + 1,
|
|
3634
|
+
0,
|
|
3635
|
+
0
|
|
3636
|
+
];
|
|
3637
|
+
if (parts === 2) return [
|
|
3638
|
+
major,
|
|
3639
|
+
minor + 1,
|
|
3640
|
+
0
|
|
3641
|
+
];
|
|
3642
|
+
return [
|
|
3643
|
+
major,
|
|
3644
|
+
minor,
|
|
3645
|
+
patch + 1
|
|
3646
|
+
];
|
|
3647
|
+
}
|
|
3648
|
+
function compareTriples(a, b) {
|
|
3649
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
|
|
3650
|
+
return 0;
|
|
3651
|
+
}
|
|
3652
|
+
/**
|
|
3653
|
+
* Whether a declared range could EVER resolve at or above `target`.
|
|
3654
|
+
*
|
|
3655
|
+
* The same question the control plane asks at intake, asked here first. Only a
|
|
3656
|
+
* range whose entire span sits below the target is reported — `^0.10.0` can
|
|
3657
|
+
* never cross to 0.12 whatever npm publishes — because a false alarm on a build
|
|
3658
|
+
* that would have worked trains people to ignore the warning that matters.
|
|
3659
|
+
*/
|
|
3660
|
+
function canReach(range, target) {
|
|
3661
|
+
const ceiling = upperBoundOf(range);
|
|
3662
|
+
const floor = lowerBoundOf(target);
|
|
3663
|
+
if (!floor || !lowerBoundOf(range)) return null;
|
|
3664
|
+
if (!ceiling) return true;
|
|
3665
|
+
return compareTriples(ceiling, floor) > 0;
|
|
3666
|
+
}
|
|
3667
|
+
/**
|
|
3668
|
+
* Find `@rebasepro/*` dependencies pinned to a version older than this CLI.
|
|
3669
|
+
*
|
|
3670
|
+
* This is the only place a developer can be told. In development, every
|
|
3671
|
+
* `@rebasepro/*` resolves through pnpm's `link:`/`workspace:` overrides to the
|
|
3672
|
+
* checkout, so the version STRINGS in package.json are never exercised — the
|
|
3673
|
+
* project runs fine locally on whatever is on disk, and the declared numbers are
|
|
3674
|
+
* first honoured when the runtime npm-installs them from a bundle in the cloud.
|
|
3675
|
+
* A project scaffolded at 0.10.0 therefore keeps working on a developer's
|
|
3676
|
+
* machine indefinitely while being, in the cloud, a 0.10.0 driver.
|
|
3677
|
+
*
|
|
3678
|
+
* That matters because the image supplies only `@rebasepro/server`; the database
|
|
3679
|
+
* driver comes from these declarations and a newer runtime never updates it.
|
|
3680
|
+
* Every package.json is scanned, `dependencies` and `devDependencies` both,
|
|
3681
|
+
* because they have to be bumped together and the one that gets forgotten is the
|
|
3682
|
+
* one nobody looks at.
|
|
3683
|
+
*/
|
|
3684
|
+
function detectFrameworkDepDrift(projectRoot, cliVersion) {
|
|
3685
|
+
const found = [];
|
|
3686
|
+
for (const relative of [
|
|
3687
|
+
"package.json",
|
|
3688
|
+
"backend/package.json",
|
|
3689
|
+
"config/package.json",
|
|
3690
|
+
"frontend/package.json"
|
|
3691
|
+
]) {
|
|
3692
|
+
const file = path.join(projectRoot, relative);
|
|
3693
|
+
if (!fs.existsSync(file)) continue;
|
|
3694
|
+
try {
|
|
3695
|
+
const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
3696
|
+
for (const block of [pkg.dependencies, pkg.devDependencies]) for (const [name, range] of Object.entries(block ?? {})) {
|
|
3697
|
+
if (!name.startsWith("@rebasepro/")) continue;
|
|
3698
|
+
if (typeof range !== "string") continue;
|
|
3699
|
+
found.push({
|
|
3700
|
+
name,
|
|
3701
|
+
range,
|
|
3702
|
+
file: relative
|
|
3703
|
+
});
|
|
3704
|
+
}
|
|
3705
|
+
} catch {}
|
|
3706
|
+
}
|
|
3707
|
+
const behind = found.filter((d) => canReach(d.range, cliVersion) === false);
|
|
3708
|
+
const bounds = new Set(found.map((d) => lowerBoundOf(d.range)).filter((b) => b != null).map((b) => b.join(".")));
|
|
3709
|
+
return {
|
|
3710
|
+
behind,
|
|
3711
|
+
disagreeing: bounds.size > 1 ? [...bounds].sort() : []
|
|
3712
|
+
};
|
|
3713
|
+
}
|
|
3714
|
+
/**
|
|
3203
3715
|
* Rewrite relative import specifiers in emitted JavaScript so Node can resolve them.
|
|
3204
3716
|
*
|
|
3205
3717
|
* TypeScript deliberately does not touch specifiers: `moduleResolution: "bundler"`
|
|
@@ -3328,11 +3840,10 @@ async function regenerateSchema(projectRoot, configDir, options) {
|
|
|
3328
3840
|
* deployed green, and answered 404 on every one of them, with the file still
|
|
3329
3841
|
* sitting in the repository looking exactly like the server.
|
|
3330
3842
|
*
|
|
3331
|
-
* A project that means to keep its own entrypoint
|
|
3332
|
-
*
|
|
3333
|
-
*
|
|
3334
|
-
*
|
|
3335
|
-
* a mistake.
|
|
3843
|
+
* A project that means to keep its own entrypoint runs `rebase eject`, which
|
|
3844
|
+
* writes the entrypoint, a Dockerfile and a compose file together and flips the
|
|
3845
|
+
* backend to `runtime: "custom"`. The warning names that route rather than
|
|
3846
|
+
* implying the file is a mistake.
|
|
3336
3847
|
*/
|
|
3337
3848
|
function findUnusedServerEntry(projectRoot, functionsDir) {
|
|
3338
3849
|
const found = [path.join("backend", "src", "index.ts"), path.join(path.dirname(functionsDir), "src", "index.ts")].find((candidate) => fs.existsSync(path.join(projectRoot, candidate)));
|
|
@@ -3343,22 +3854,22 @@ function findUnusedServerEntry(projectRoot, functionsDir) {
|
|
|
3343
3854
|
*/
|
|
3344
3855
|
async function buildBundle(options) {
|
|
3345
3856
|
const { projectRoot, app, appName } = options;
|
|
3346
|
-
const paths = resolveBackendPaths(app);
|
|
3857
|
+
const paths = resolveBackendPaths(app, projectRoot);
|
|
3347
3858
|
const outDir = path.resolve(projectRoot, options.outDir ?? "dist-bundle");
|
|
3348
3859
|
const includes = [];
|
|
3349
3860
|
const addIfExists = (relative, pattern) => {
|
|
3350
3861
|
if (fs.existsSync(path.join(projectRoot, relative))) includes.push(pattern);
|
|
3351
3862
|
};
|
|
3352
|
-
if (paths.
|
|
3863
|
+
if (paths.hasConfig) addIfExists(paths.config, `${paths.config}/**/*.ts`);
|
|
3353
3864
|
addIfExists(paths.functions, `${paths.functions}/**/*.ts`);
|
|
3354
3865
|
addIfExists(paths.crons, `${paths.crons}/**/*.ts`);
|
|
3355
3866
|
if (fs.existsSync(path.join(projectRoot, paths.schema))) includes.push(paths.schema);
|
|
3356
3867
|
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}".`);
|
|
3357
|
-
if (paths.
|
|
3868
|
+
if (paths.hasCollections && options.skipSchema !== true) await regenerateSchema(projectRoot, paths.config, options);
|
|
3358
3869
|
const unusedEntry = findUnusedServerEntry(projectRoot, paths.functions);
|
|
3359
3870
|
if (unusedEntry) {
|
|
3360
3871
|
const parts = [
|
|
3361
|
-
...paths.
|
|
3872
|
+
...paths.hasCollections ? [`${paths.config}/`] : [],
|
|
3362
3873
|
`${paths.functions}/`,
|
|
3363
3874
|
"the schema"
|
|
3364
3875
|
];
|
|
@@ -3366,7 +3877,7 @@ async function buildBundle(options) {
|
|
|
3366
3877
|
console.log(chalk.yellow(` ⚠ ${unusedEntry} is not the bundle's entry point — it is not compiled or shipped.`));
|
|
3367
3878
|
console.log(chalk.dim(` The runtime boots the bundle itself and mounts ${compiled}.`));
|
|
3368
3879
|
console.log(chalk.dim(` Routes defined there will not exist once deployed: move them to ${paths.functions}/,`));
|
|
3369
|
-
console.log(chalk.dim(` or
|
|
3880
|
+
console.log(chalk.dim(` or run \`rebase eject\` to make this file the entrypoint and own the image.`));
|
|
3370
3881
|
}
|
|
3371
3882
|
log(options, chalk.dim(` compiling ${includes.length} source group(s) → ${path.relative(projectRoot, outDir)}/`));
|
|
3372
3883
|
cleanOutDir(projectRoot, outDir);
|
|
@@ -3391,14 +3902,17 @@ async function buildBundle(options) {
|
|
|
3391
3902
|
const compiledConfigDir = path.join(outDir, paths.config);
|
|
3392
3903
|
const compiledCollectionsDir = path.join(compiledConfigDir, "collections");
|
|
3393
3904
|
let collections = [];
|
|
3394
|
-
if (paths.
|
|
3905
|
+
if (paths.hasCollections) {
|
|
3395
3906
|
collections = await loadSourceCollections(path.join(projectRoot, paths.config, "collections"));
|
|
3396
|
-
if (collections.length === 0) throw new Error(`No collections were found in ${path.join(paths.config, "collections")}.
|
|
3907
|
+
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.`);
|
|
3397
3908
|
if (!fs.existsSync(compiledCollectionsDir)) throw new Error(`Compilation produced no collections directory at ${path.relative(projectRoot, compiledCollectionsDir)}.`);
|
|
3398
3909
|
}
|
|
3399
3910
|
const declared = collectDeclaredDependencies(projectRoot);
|
|
3400
3911
|
const nativeModules = detectNativeDependencies(projectRoot, declared);
|
|
3401
3912
|
const declaresStorageAuthorize = detectStorageAuthorize(path.join(outDir, paths.config));
|
|
3913
|
+
const storageSources = normalizeStorageSources(options.storage, void 0);
|
|
3914
|
+
const collision = findStorageSuffixCollision(storageSources.map((s) => s.key));
|
|
3915
|
+
if (collision) throw new Error(`Storage sources "${collision.a}" and "${collision.b}" in rebase.json both map to the environment variable suffix "${collision.suffix || "(none)"}", so they would read each other's configuration. Rename one of them.`);
|
|
3402
3916
|
const schemaOut = paths.schema.replace(/\.ts$/, ".js");
|
|
3403
3917
|
const relative = (target) => fs.existsSync(path.join(outDir, target)) ? target : void 0;
|
|
3404
3918
|
const manifest = {
|
|
@@ -3408,23 +3922,26 @@ async function buildBundle(options) {
|
|
|
3408
3922
|
builtAgainst: resolveServerVersion(projectRoot),
|
|
3409
3923
|
contract: RUNTIME_CONTRACT_VERSION
|
|
3410
3924
|
},
|
|
3411
|
-
schemaVersion: paths.
|
|
3925
|
+
schemaVersion: paths.hasCollections ? computeSchemaVersion(collections) : "",
|
|
3412
3926
|
app: appName,
|
|
3413
|
-
|
|
3927
|
+
kind: "backend",
|
|
3414
3928
|
entry: {
|
|
3415
|
-
config: paths.
|
|
3416
|
-
collections: paths.
|
|
3929
|
+
config: paths.hasConfig ? relative(paths.config) : void 0,
|
|
3930
|
+
collections: paths.hasCollections ? relative(path.join(paths.config, "collections")) : void 0,
|
|
3417
3931
|
functions: relative(paths.functions),
|
|
3418
3932
|
crons: relative(paths.crons),
|
|
3419
3933
|
schema: relative(schemaOut),
|
|
3420
|
-
usersCollection: paths.
|
|
3934
|
+
usersCollection: paths.hasCollections ? relative(path.join(paths.config, `${paths.usersCollection}.js`)) : void 0
|
|
3421
3935
|
},
|
|
3422
3936
|
collections: collections.map((collection) => collection.slug).filter((slug) => Boolean(slug)).sort(),
|
|
3423
3937
|
hooks: {
|
|
3424
3938
|
native: nativeModules.length > 0,
|
|
3425
3939
|
nativeModules: nativeModules.length > 0 ? nativeModules : void 0
|
|
3426
3940
|
},
|
|
3427
|
-
storage: {
|
|
3941
|
+
storage: {
|
|
3942
|
+
authorize: declaresStorageAuthorize,
|
|
3943
|
+
...storageSources.length > 0 ? { sources: storageSources } : {}
|
|
3944
|
+
},
|
|
3428
3945
|
deps: { declared },
|
|
3429
3946
|
build: {
|
|
3430
3947
|
cli: resolveCliVersion(),
|
|
@@ -3487,11 +4004,12 @@ async function buildBundle(options) {
|
|
|
3487
4004
|
* in one image already, that is exactly what it had.
|
|
3488
4005
|
*/
|
|
3489
4006
|
function foldStaticIntoBundle(options) {
|
|
3490
|
-
const { bundleDir, assetsDir } = options;
|
|
4007
|
+
const { bundleDir, assetsDir, appName, path: basePath, spa } = options;
|
|
3491
4008
|
const manifestPath = path.join(bundleDir, "manifest.json");
|
|
3492
4009
|
if (!fs.existsSync(manifestPath)) throw new Error(`No manifest at ${manifestPath} — build the backend bundle first.`);
|
|
3493
4010
|
if (!fs.existsSync(assetsDir)) throw new Error(`No built assets at ${assetsDir}.`);
|
|
3494
|
-
const
|
|
4011
|
+
const dir = path.posix.join("static", appName);
|
|
4012
|
+
const staticOut = path.join(bundleDir, "static", appName);
|
|
3495
4013
|
fs.rmSync(staticOut, {
|
|
3496
4014
|
recursive: true,
|
|
3497
4015
|
force: true
|
|
@@ -3499,21 +4017,30 @@ function foldStaticIntoBundle(options) {
|
|
|
3499
4017
|
fs.mkdirSync(staticOut, { recursive: true });
|
|
3500
4018
|
fs.cpSync(assetsDir, staticOut, { recursive: true });
|
|
3501
4019
|
let fileCount = 0;
|
|
3502
|
-
const count = (
|
|
3503
|
-
for (const entry of fs.readdirSync(
|
|
4020
|
+
const count = (target) => {
|
|
4021
|
+
for (const entry of fs.readdirSync(target, { withFileTypes: true })) if (entry.isDirectory()) count(path.join(target, entry.name));
|
|
3504
4022
|
else fileCount++;
|
|
3505
4023
|
};
|
|
3506
4024
|
count(staticOut);
|
|
3507
4025
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
4026
|
+
const existing = (manifest.entry?.static ?? []).filter((entry) => entry.dir !== dir);
|
|
3508
4027
|
manifest.entry = {
|
|
3509
4028
|
...manifest.entry,
|
|
3510
|
-
static:
|
|
4029
|
+
static: [...existing, {
|
|
4030
|
+
path: basePath,
|
|
4031
|
+
dir,
|
|
4032
|
+
spa
|
|
4033
|
+
}]
|
|
3511
4034
|
};
|
|
3512
4035
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
3513
|
-
return {
|
|
4036
|
+
return {
|
|
4037
|
+
fileCount,
|
|
4038
|
+
dir
|
|
4039
|
+
};
|
|
3514
4040
|
}
|
|
3515
4041
|
function buildStaticBundle(options) {
|
|
3516
4042
|
const { projectRoot, appName, assetsDir, outDir, runtimeRange } = options;
|
|
4043
|
+
const basePath = options.path ?? "/";
|
|
3517
4044
|
cleanOutDir(projectRoot, outDir);
|
|
3518
4045
|
const staticOut = path.join(outDir, "static");
|
|
3519
4046
|
fs.mkdirSync(staticOut, { recursive: true });
|
|
@@ -3533,8 +4060,12 @@ function buildStaticBundle(options) {
|
|
|
3533
4060
|
},
|
|
3534
4061
|
schemaVersion: "",
|
|
3535
4062
|
app: appName,
|
|
3536
|
-
|
|
3537
|
-
entry: { static:
|
|
4063
|
+
kind: "static",
|
|
4064
|
+
entry: { static: [{
|
|
4065
|
+
path: basePath,
|
|
4066
|
+
dir: "static",
|
|
4067
|
+
spa: options.spa ?? true
|
|
4068
|
+
}] },
|
|
3538
4069
|
hooks: { native: false },
|
|
3539
4070
|
deps: { declared: {} },
|
|
3540
4071
|
build: {
|
|
@@ -3636,7 +4167,7 @@ function resolveCliVersion() {
|
|
|
3636
4167
|
//#endregion
|
|
3637
4168
|
//#region src/fold-static.ts
|
|
3638
4169
|
/**
|
|
3639
|
-
* Folding a project's
|
|
4170
|
+
* Folding a project's static apps into its backend bundle.
|
|
3640
4171
|
*
|
|
3641
4172
|
* Shared by `rebase build` and `rebase cloud deploy` deliberately. It lived in
|
|
3642
4173
|
* the build *command* first, and `deploy` rebuilds the bundle itself — so a
|
|
@@ -3645,34 +4176,77 @@ function resolveCliVersion() {
|
|
|
3645
4176
|
* folding had never been written. Two callers building the same artefact must
|
|
3646
4177
|
* share the step that completes it.
|
|
3647
4178
|
*
|
|
3648
|
-
* Why fold at all: `bootFromBundle`
|
|
4179
|
+
* Why fold at all: `bootFromBundle` serves static apps from `entry.static`
|
|
3649
4180
|
* behind `REBASE_SERVE_STATIC` (default on). A managed tenant runs one pod, so
|
|
3650
|
-
* putting the built
|
|
3651
|
-
* already had — site at `/`, API at `/api` — which is the
|
|
3652
|
-
* for calling the managed runtime a drop-in replacement.
|
|
4181
|
+
* putting the built assets in the bundle gives it the shape a custom container
|
|
4182
|
+
* already had — site at `/`, admin at `/admin`, API at `/api` — which is the
|
|
4183
|
+
* only honest baseline for calling the managed runtime a drop-in replacement.
|
|
4184
|
+
*
|
|
4185
|
+
* **Every** static app is folded, each at its declared path. Folding used to
|
|
4186
|
+
* pick exactly one and refuse when it found two, which meant a project with a
|
|
4187
|
+
* site and an admin panel deployed with neither.
|
|
3653
4188
|
*/
|
|
3654
4189
|
/**
|
|
3655
|
-
*
|
|
4190
|
+
* Every static app in the manifest, in mount order.
|
|
3656
4191
|
*
|
|
3657
|
-
*
|
|
3658
|
-
*
|
|
3659
|
-
*
|
|
3660
|
-
* filesystem.
|
|
4192
|
+
* Longest path first, so the `/`-rooted app is registered last — its catch-all
|
|
4193
|
+
* would otherwise claim its siblings' URLs. Pure, so the ordering is testable
|
|
4194
|
+
* without a filesystem.
|
|
3661
4195
|
*/
|
|
3662
|
-
function
|
|
3663
|
-
const
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
4196
|
+
function foldableApps(manifest) {
|
|
4197
|
+
const apps = [];
|
|
4198
|
+
const skipped = [];
|
|
4199
|
+
for (const [name, app] of Object.entries(manifest.apps ?? {})) {
|
|
4200
|
+
if (app?.type !== "static") continue;
|
|
4201
|
+
if (!app.output) {
|
|
4202
|
+
skipped.push({
|
|
4203
|
+
name,
|
|
4204
|
+
reason: `"${name}" declares no output directory — not folded in.`
|
|
4205
|
+
});
|
|
4206
|
+
continue;
|
|
4207
|
+
}
|
|
4208
|
+
apps.push({
|
|
4209
|
+
name,
|
|
4210
|
+
build: app.build,
|
|
4211
|
+
output: app.output,
|
|
4212
|
+
path: app.path ?? "/",
|
|
4213
|
+
spa: app.spa ?? true
|
|
4214
|
+
});
|
|
4215
|
+
}
|
|
4216
|
+
apps.sort((a, b) => b.path.length - a.path.length);
|
|
4217
|
+
return {
|
|
4218
|
+
apps,
|
|
4219
|
+
skipped
|
|
4220
|
+
};
|
|
4221
|
+
}
|
|
4222
|
+
/**
|
|
4223
|
+
* Assert a built app's assets are actually rooted at the path it is served from.
|
|
4224
|
+
*
|
|
4225
|
+
* An app mounted at `/admin` but built with Vite's default `base: "/"` emits
|
|
4226
|
+
* `<script src="/assets/index-a1b2.js">`. The server serves `index.html` fine
|
|
4227
|
+
* and 404s every asset: a blank page, no server error, nothing in the logs. It
|
|
4228
|
+
* is the single most expensive silent failure in this design, so it is a build
|
|
4229
|
+
* error rather than a runtime surprise.
|
|
4230
|
+
*
|
|
4231
|
+
* Only `<script src>` and `<link href>` are inspected — those are what a bundler
|
|
4232
|
+
* rewrites through `base`. Author-written anchors and canonical URLs are not
|
|
4233
|
+
* evidence of a misbuild.
|
|
4234
|
+
*/
|
|
4235
|
+
function assertBuiltForPath(indexHtml, basePath, appName) {
|
|
4236
|
+
if (basePath === "/") return;
|
|
4237
|
+
const offenders = [];
|
|
4238
|
+
for (const match of indexHtml.matchAll(/<(?:script|link)\b[^>]*?\b(?:src|href)\s*=\s*["']([^"']+)["']/gi)) {
|
|
4239
|
+
const ref = match[1];
|
|
4240
|
+
if (!ref.startsWith("/")) continue;
|
|
4241
|
+
if (ref === `${basePath}` || ref.startsWith(`${basePath}/`)) continue;
|
|
4242
|
+
offenders.push(ref);
|
|
4243
|
+
}
|
|
4244
|
+
if (offenders.length === 0) return;
|
|
4245
|
+
throw new Error(`"${appName}" is declared at ${basePath} but its build emitted assets rooted at /.\n index.html references: ${offenders.slice(0, 3).join(", ")}\n The app would load a blank page. Set \`base\` from REBASE_APP_BASE in its
|
|
4246
|
+
build config — see docs/apps-and-runtimes.md §4.2.`);
|
|
3673
4247
|
}
|
|
3674
4248
|
/**
|
|
3675
|
-
* Build the project's
|
|
4249
|
+
* Build the project's static apps and fold them into the backend bundle.
|
|
3676
4250
|
*
|
|
3677
4251
|
* Throws rather than exiting, so the caller decides whether a missing frontend
|
|
3678
4252
|
* should fail its command — a `build` may reasonably want to stop, and so should
|
|
@@ -3681,27 +4255,39 @@ function selectFoldableApp(manifest) {
|
|
|
3681
4255
|
async function foldFrontendIntoBundle(options) {
|
|
3682
4256
|
const { projectRoot, manifest, bundleDir, skipBuild } = options;
|
|
3683
4257
|
const log = options.log ?? ((m) => console.log(m));
|
|
3684
|
-
const {
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
4258
|
+
const { apps, skipped } = foldableApps(manifest);
|
|
4259
|
+
for (const { reason } of skipped) log(chalk.yellow(` ⚠ ${reason}`));
|
|
4260
|
+
if (apps.length === 0) return [];
|
|
4261
|
+
const outcomes = [];
|
|
4262
|
+
for (const app of apps) {
|
|
4263
|
+
if (app.build && !skipBuild) await execa(app.build, {
|
|
4264
|
+
cwd: projectRoot,
|
|
4265
|
+
stdio: "inherit",
|
|
4266
|
+
shell: true,
|
|
4267
|
+
env: {
|
|
4268
|
+
REBASE_APP_PATH: app.path,
|
|
4269
|
+
REBASE_APP_BASE: app.path === "/" ? "/" : `${app.path}/`,
|
|
4270
|
+
REBASE_APP_NAME: app.name
|
|
4271
|
+
}
|
|
4272
|
+
});
|
|
4273
|
+
const assetsDir = path.join(projectRoot, app.output);
|
|
4274
|
+
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.`);
|
|
4275
|
+
const indexHtml = path.join(assetsDir, "index.html");
|
|
4276
|
+
if (fs.existsSync(indexHtml)) assertBuiltForPath(fs.readFileSync(indexHtml, "utf8"), app.path, app.name);
|
|
4277
|
+
const { fileCount } = foldStaticIntoBundle({
|
|
4278
|
+
bundleDir,
|
|
4279
|
+
assetsDir,
|
|
4280
|
+
appName: app.name,
|
|
4281
|
+
path: app.path,
|
|
4282
|
+
spa: app.spa
|
|
4283
|
+
});
|
|
4284
|
+
outcomes.push({
|
|
4285
|
+
appName: app.name,
|
|
4286
|
+
fileCount,
|
|
4287
|
+
path: app.path
|
|
4288
|
+
});
|
|
3688
4289
|
}
|
|
3689
|
-
|
|
3690
|
-
if (app.build && !skipBuild) await execa(app.build, {
|
|
3691
|
-
cwd: projectRoot,
|
|
3692
|
-
stdio: "inherit",
|
|
3693
|
-
shell: true
|
|
3694
|
-
});
|
|
3695
|
-
const assetsDir = path.join(projectRoot, app.output);
|
|
3696
|
-
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.`);
|
|
3697
|
-
const { fileCount } = foldStaticIntoBundle({
|
|
3698
|
-
bundleDir,
|
|
3699
|
-
assetsDir
|
|
3700
|
-
});
|
|
3701
|
-
return {
|
|
3702
|
-
appName: app.name,
|
|
3703
|
-
fileCount
|
|
3704
|
-
};
|
|
4290
|
+
return outcomes;
|
|
3705
4291
|
}
|
|
3706
4292
|
//#endregion
|
|
3707
4293
|
//#region src/commands/build.ts
|
|
@@ -3719,7 +4305,7 @@ async function foldFrontendIntoBundle(options) {
|
|
|
3719
4305
|
* entrypoint, falls back to the previous behaviour: run every workspace's own
|
|
3720
4306
|
* `build` script. Nothing that built before stops building.
|
|
3721
4307
|
*/
|
|
3722
|
-
function printHelp$
|
|
4308
|
+
function printHelp$4() {
|
|
3723
4309
|
console.log(`
|
|
3724
4310
|
${chalk.bold("rebase build")} — build the apps declared in rebase.json
|
|
3725
4311
|
|
|
@@ -3754,7 +4340,7 @@ async function buildCommand(rawArgs = []) {
|
|
|
3754
4340
|
permissive: true
|
|
3755
4341
|
});
|
|
3756
4342
|
if (args["--help"]) {
|
|
3757
|
-
printHelp$
|
|
4343
|
+
printHelp$4();
|
|
3758
4344
|
return;
|
|
3759
4345
|
}
|
|
3760
4346
|
const projectRoot = requireProjectRoot();
|
|
@@ -3798,13 +4384,20 @@ async function buildCommand(rawArgs = []) {
|
|
|
3798
4384
|
console.log(`${chalk.bold("Rebase")} — building ${targets.length} app(s)\n`);
|
|
3799
4385
|
for (const { name, app } of targets) {
|
|
3800
4386
|
console.log(chalk.cyan(`▸ ${name}`) + chalk.dim(` (${app.type})`));
|
|
4387
|
+
if (app.type === "backend" && app.runtime === "custom") {
|
|
4388
|
+
console.log(chalk.dim(" custom runtime — this project builds its own image, not a bundle"));
|
|
4389
|
+
console.log(chalk.dim(` ${chalk.cyan(`npm run build --workspace ${name}`)} then ${chalk.cyan(`docker build -f ${app.dockerfile ?? "Dockerfile"} .`)}`));
|
|
4390
|
+
console.log("");
|
|
4391
|
+
continue;
|
|
4392
|
+
}
|
|
3801
4393
|
if (app.type === "backend") {
|
|
3802
4394
|
const result = await buildBundle({
|
|
3803
4395
|
projectRoot,
|
|
3804
4396
|
appName: name,
|
|
3805
4397
|
app,
|
|
3806
4398
|
outDir: args["--out"],
|
|
3807
|
-
runtimeRange: manifest.
|
|
4399
|
+
runtimeRange: manifest.rebase,
|
|
4400
|
+
storage: manifest.storage,
|
|
3808
4401
|
skipTypeCheck: args["--skip-type-check"],
|
|
3809
4402
|
skipSchema: args["--skip-schema"]
|
|
3810
4403
|
});
|
|
@@ -3816,6 +4409,16 @@ async function buildCommand(rawArgs = []) {
|
|
|
3816
4409
|
console.log(chalk.yellow(` ⚠ native dependencies detected: ${names}`));
|
|
3817
4410
|
console.log(chalk.dim(" These cannot run on the managed runtime. See `rebase doctor`."));
|
|
3818
4411
|
}
|
|
4412
|
+
const drift = detectFrameworkDepDrift(projectRoot, resolveCliVersion());
|
|
4413
|
+
if (drift.behind.length > 0) {
|
|
4414
|
+
console.log(chalk.yellow(` ⚠ framework dependencies older than this CLI (${resolveCliVersion()}):`));
|
|
4415
|
+
for (const dep of drift.behind) console.log(chalk.dim(` ${dep.name}@${dep.range} (${dep.file})`));
|
|
4416
|
+
console.log(chalk.dim(" The image supplies the server, but your bundle supplies the database"));
|
|
4417
|
+
console.log(chalk.dim(" driver — a newer runtime does not update it. Bump these and rebuild."));
|
|
4418
|
+
} else if (drift.disagreeing.length > 0) {
|
|
4419
|
+
console.log(chalk.yellow(` ⚠ mixed @rebasepro versions declared: ${drift.disagreeing.join(", ")}`));
|
|
4420
|
+
console.log(chalk.dim(" These are published together and expect to run together; pin them alike."));
|
|
4421
|
+
}
|
|
3819
4422
|
if (!args["--no-static"]) {
|
|
3820
4423
|
const folded = await foldFrontendIntoBundle({
|
|
3821
4424
|
projectRoot,
|
|
@@ -3827,28 +4430,24 @@ async function buildCommand(rawArgs = []) {
|
|
|
3827
4430
|
console.error(chalk.red(` ✗ ${err instanceof Error ? err.message : String(err)}`));
|
|
3828
4431
|
process.exit(1);
|
|
3829
4432
|
});
|
|
3830
|
-
|
|
4433
|
+
for (const outcome of folded ?? []) console.log(chalk.green(` ✓ ${outcome.appName} folded in`) + chalk.dim(` (${outcome.fileCount} file(s) → served at ${outcome.path})`));
|
|
3831
4434
|
}
|
|
3832
|
-
} else if (app.type === "static"
|
|
3833
|
-
else if (app.type === "custom") console.log(chalk.dim(" custom container — built at deploy time from its Dockerfile"));
|
|
4435
|
+
} else if (app.type === "static") await buildAssetApp(projectRoot, name, app, manifest.rebase, args["--out"]);
|
|
3834
4436
|
console.log("");
|
|
3835
4437
|
}
|
|
3836
4438
|
console.log(chalk.green("✓ Build complete."));
|
|
3837
4439
|
}
|
|
3838
4440
|
/**
|
|
3839
|
-
* Build a static
|
|
4441
|
+
* Build a static app and package it into a static bundle.
|
|
3840
4442
|
*
|
|
3841
4443
|
* Runs the app's own build command, checks it produced the declared output, then
|
|
3842
|
-
* packages that output into a `static`-
|
|
4444
|
+
* packages that output into a `static`-kind bundle — the same deployable shape as
|
|
3843
4445
|
* a backend bundle, so a frontend or admin app deploys through the identical
|
|
3844
4446
|
* path and runs on the identical image, just serving files instead of an API.
|
|
3845
4447
|
*/
|
|
3846
4448
|
async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride) {
|
|
3847
4449
|
const asset = app;
|
|
3848
|
-
|
|
3849
|
-
console.log(chalk.dim(" hosted admin panel — nothing to build"));
|
|
3850
|
-
return;
|
|
3851
|
-
}
|
|
4450
|
+
const basePath = asset.path ?? "/";
|
|
3852
4451
|
if (!asset.build) {
|
|
3853
4452
|
console.log(chalk.dim(" no build command declared — skipping"));
|
|
3854
4453
|
return;
|
|
@@ -3857,7 +4456,12 @@ async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride)
|
|
|
3857
4456
|
await execa(asset.build, {
|
|
3858
4457
|
cwd: projectRoot,
|
|
3859
4458
|
stdio: "inherit",
|
|
3860
|
-
shell: true
|
|
4459
|
+
shell: true,
|
|
4460
|
+
env: {
|
|
4461
|
+
REBASE_APP_PATH: basePath,
|
|
4462
|
+
REBASE_APP_BASE: basePath === "/" ? "/" : `${basePath}/`,
|
|
4463
|
+
REBASE_APP_NAME: name
|
|
4464
|
+
}
|
|
3861
4465
|
});
|
|
3862
4466
|
} catch {
|
|
3863
4467
|
console.error(chalk.red(` ✗ build command failed for "${name}"`));
|
|
@@ -3872,15 +4476,24 @@ async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride)
|
|
|
3872
4476
|
console.error(chalk.red(` ✗ declared output "${asset.output}" does not exist after building`));
|
|
3873
4477
|
process.exit(1);
|
|
3874
4478
|
}
|
|
4479
|
+
const indexHtml = path.join(outputPath, "index.html");
|
|
4480
|
+
if (fs.existsSync(indexHtml)) try {
|
|
4481
|
+
assertBuiltForPath(fs.readFileSync(indexHtml, "utf8"), basePath, name);
|
|
4482
|
+
} catch (err) {
|
|
4483
|
+
console.error(chalk.red(` ✗ ${err instanceof Error ? err.message : String(err)}`));
|
|
4484
|
+
process.exit(1);
|
|
4485
|
+
}
|
|
3875
4486
|
const result = buildStaticBundle({
|
|
3876
4487
|
projectRoot,
|
|
3877
4488
|
appName: name,
|
|
3878
4489
|
assetsDir: outputPath,
|
|
3879
4490
|
outDir: outOverride ? path.resolve(process.cwd(), outOverride) : path.join(projectRoot, `dist-bundle-${name}`),
|
|
3880
|
-
runtimeRange
|
|
4491
|
+
runtimeRange,
|
|
4492
|
+
path: basePath,
|
|
4493
|
+
spa: asset.spa ?? true
|
|
3881
4494
|
});
|
|
3882
4495
|
const rel = path.relative(projectRoot, result.outDir);
|
|
3883
|
-
console.log(chalk.green(` ✓ static bundle → ${rel}/`) + chalk.dim(` (${result.fileCount} file(s))`));
|
|
4496
|
+
console.log(chalk.green(` ✓ static bundle → ${rel}/`) + chalk.dim(` (${result.fileCount} file(s) → served at ${basePath})`));
|
|
3884
4497
|
}
|
|
3885
4498
|
/** The pre-manifest behaviour: build every workspace package. */
|
|
3886
4499
|
async function runWorkspaceBuilds(projectRoot) {
|
|
@@ -3898,6 +4511,234 @@ async function runWorkspaceBuilds(projectRoot) {
|
|
|
3898
4511
|
}
|
|
3899
4512
|
}
|
|
3900
4513
|
//#endregion
|
|
4514
|
+
//#region src/commands/eject.ts
|
|
4515
|
+
/**
|
|
4516
|
+
* CLI command: rebase eject
|
|
4517
|
+
*
|
|
4518
|
+
* The supported route from the managed runtime to a custom one.
|
|
4519
|
+
*
|
|
4520
|
+
* Without it, `runtime: "custom"` is a mode a user can only reach by
|
|
4521
|
+
* hand-writing an entrypoint they have never seen. The template used to solve
|
|
4522
|
+
* that by scaffolding `backend/src/index.ts` into every project — ~190 lines
|
|
4523
|
+
* configuring CORS, auth, cookies, storage and history, which the managed
|
|
4524
|
+
* runtime never loads. It was the most important-looking file in a new project
|
|
4525
|
+
* and editing it did nothing.
|
|
4526
|
+
*
|
|
4527
|
+
* So the file moved here. A managed project does not carry it; a project that
|
|
4528
|
+
* asks for it gets it together with the Dockerfile and the manifest change that
|
|
4529
|
+
* make it actually run.
|
|
4530
|
+
*
|
|
4531
|
+
* There is deliberately no `rebase uneject`. Going back is deleting two files
|
|
4532
|
+
* and editing one line, and a command that silently discarded a user's server
|
|
4533
|
+
* code would be worse than its absence.
|
|
4534
|
+
*/
|
|
4535
|
+
var __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
|
|
4536
|
+
/** Walk up to the package root, which holds `templates/`. */
|
|
4537
|
+
function findCliRoot(from) {
|
|
4538
|
+
const root = path.parse(from).root;
|
|
4539
|
+
let dir = from;
|
|
4540
|
+
while (dir && dir !== root) {
|
|
4541
|
+
if (fs.existsSync(path.join(dir, "templates", "eject"))) return dir;
|
|
4542
|
+
dir = path.dirname(dir);
|
|
4543
|
+
}
|
|
4544
|
+
return null;
|
|
4545
|
+
}
|
|
4546
|
+
/** Files the eject payload contributes, as `<source> → <destination>`. */
|
|
4547
|
+
var PAYLOAD = [
|
|
4548
|
+
{
|
|
4549
|
+
from: "backend/src/index.ts",
|
|
4550
|
+
to: "backend/src/index.ts",
|
|
4551
|
+
overwrite: true
|
|
4552
|
+
},
|
|
4553
|
+
{
|
|
4554
|
+
from: "backend/src/env.ts",
|
|
4555
|
+
to: "backend/src/env.ts",
|
|
4556
|
+
overwrite: true
|
|
4557
|
+
},
|
|
4558
|
+
{
|
|
4559
|
+
from: "Dockerfile",
|
|
4560
|
+
to: "Dockerfile",
|
|
4561
|
+
overwrite: false
|
|
4562
|
+
},
|
|
4563
|
+
{
|
|
4564
|
+
from: "docker-compose.custom.yml",
|
|
4565
|
+
to: "docker-compose.custom.yml",
|
|
4566
|
+
overwrite: false
|
|
4567
|
+
}
|
|
4568
|
+
];
|
|
4569
|
+
/**
|
|
4570
|
+
* The project's name, for the `{{PROJECT_NAME}}` the payload carries.
|
|
4571
|
+
*
|
|
4572
|
+
* Falls back to the directory name — a compose project name is cosmetic, and a
|
|
4573
|
+
* missing or unreadable package.json is not a reason to refuse to eject.
|
|
4574
|
+
*/
|
|
4575
|
+
function projectNameOf(projectRoot) {
|
|
4576
|
+
try {
|
|
4577
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"));
|
|
4578
|
+
if (typeof pkg.name === "string" && pkg.name.trim()) return pkg.name.trim();
|
|
4579
|
+
} catch {}
|
|
4580
|
+
return path.basename(projectRoot);
|
|
4581
|
+
}
|
|
4582
|
+
function printHelp$3() {
|
|
4583
|
+
console.log(`
|
|
4584
|
+
${chalk.bold("rebase eject")} — take ownership of the server process
|
|
4585
|
+
|
|
4586
|
+
Writes the backend entrypoint and a Dockerfile into this project, and flips its
|
|
4587
|
+
backend to ${chalk.cyan("runtime: \"custom\"")}. From then on this repository builds its own
|
|
4588
|
+
image: platform runtime upgrades no longer reach it, and CORS, auth wiring,
|
|
4589
|
+
storage and shutdown become yours to configure.
|
|
4590
|
+
|
|
4591
|
+
${chalk.bold("Usage")}
|
|
4592
|
+
rebase eject [app]
|
|
4593
|
+
|
|
4594
|
+
${chalk.bold("Options")}
|
|
4595
|
+
--dry-run List what would change, and change nothing
|
|
4596
|
+
-h, --help Show this help
|
|
4597
|
+
`.trim());
|
|
4598
|
+
}
|
|
4599
|
+
async function ejectCommand(rawArgs = []) {
|
|
4600
|
+
const args = arg({
|
|
4601
|
+
"--dry-run": Boolean,
|
|
4602
|
+
"--help": Boolean,
|
|
4603
|
+
"-h": "--help"
|
|
4604
|
+
}, {
|
|
4605
|
+
argv: rawArgs.slice(2),
|
|
4606
|
+
permissive: true
|
|
4607
|
+
});
|
|
4608
|
+
if (args["--help"]) {
|
|
4609
|
+
printHelp$3();
|
|
4610
|
+
return;
|
|
4611
|
+
}
|
|
4612
|
+
const projectRoot = requireProjectRoot();
|
|
4613
|
+
const dryRun = Boolean(args["--dry-run"]);
|
|
4614
|
+
const requested = args._.slice(1).find((value) => !value.startsWith("-"));
|
|
4615
|
+
let loaded;
|
|
4616
|
+
try {
|
|
4617
|
+
loaded = loadManifest(projectRoot);
|
|
4618
|
+
} catch (err) {
|
|
4619
|
+
if (err instanceof ManifestError) {
|
|
4620
|
+
console.error(chalk.red(`✗ ${err.message}`));
|
|
4621
|
+
for (const issue of err.issues) console.error(chalk.dim(` ${issue.path}: ${issue.message}`));
|
|
4622
|
+
process.exit(1);
|
|
4623
|
+
}
|
|
4624
|
+
throw err;
|
|
4625
|
+
}
|
|
4626
|
+
const { manifest } = loaded;
|
|
4627
|
+
let appName;
|
|
4628
|
+
let app;
|
|
4629
|
+
if (requested) {
|
|
4630
|
+
const declared = manifest.apps[requested];
|
|
4631
|
+
if (!declared) {
|
|
4632
|
+
console.error(chalk.red(`✗ No app named "${requested}" in rebase.json.`));
|
|
4633
|
+
console.error(chalk.dim(` Declared: ${Object.keys(manifest.apps).join(", ") || "(none)"}`));
|
|
4634
|
+
process.exit(1);
|
|
4635
|
+
}
|
|
4636
|
+
if (declared.type !== "backend") {
|
|
4637
|
+
console.error(chalk.red(`✗ "${requested}" is a ${declared.type} app — only a backend can be ejected.`));
|
|
4638
|
+
process.exit(1);
|
|
4639
|
+
}
|
|
4640
|
+
appName = requested;
|
|
4641
|
+
app = declared;
|
|
4642
|
+
} else {
|
|
4643
|
+
const backend = findBackendApp(manifest);
|
|
4644
|
+
if (!backend) {
|
|
4645
|
+
console.error(chalk.red("✗ This repository declares no backend app."));
|
|
4646
|
+
console.error(chalk.dim(" Only the repository that declares the backend chooses its runtime."));
|
|
4647
|
+
process.exit(1);
|
|
4648
|
+
}
|
|
4649
|
+
appName = backend.name;
|
|
4650
|
+
app = backend.app;
|
|
4651
|
+
}
|
|
4652
|
+
if (app.runtime === "custom") {
|
|
4653
|
+
console.error(chalk.red(`✗ "${appName}" is already ejected — it declares runtime: "custom".`));
|
|
4654
|
+
console.error(chalk.dim(` Its entrypoint is ${app.dockerfile ?? "Dockerfile"} and backend/src/index.ts.`));
|
|
4655
|
+
process.exit(1);
|
|
4656
|
+
}
|
|
4657
|
+
const cliRoot = findCliRoot(__dirname$1);
|
|
4658
|
+
if (!cliRoot) {
|
|
4659
|
+
console.error(chalk.red("✗ Could not locate the eject templates. Reinstall @rebasepro/cli."));
|
|
4660
|
+
process.exit(1);
|
|
4661
|
+
}
|
|
4662
|
+
const payloadDir = path.join(cliRoot, "templates", "eject");
|
|
4663
|
+
const planned = [];
|
|
4664
|
+
for (const file of PAYLOAD) {
|
|
4665
|
+
const source = path.join(payloadDir, file.from);
|
|
4666
|
+
if (!fs.existsSync(source)) {
|
|
4667
|
+
console.error(chalk.red(`✗ The eject template is missing ${file.from}. Reinstall @rebasepro/cli.`));
|
|
4668
|
+
process.exit(1);
|
|
4669
|
+
}
|
|
4670
|
+
const exists = fs.existsSync(path.join(projectRoot, file.to));
|
|
4671
|
+
planned.push({
|
|
4672
|
+
to: file.to,
|
|
4673
|
+
action: exists && !file.overwrite ? "keep" : "write"
|
|
4674
|
+
});
|
|
4675
|
+
}
|
|
4676
|
+
if (dryRun) {
|
|
4677
|
+
console.log(chalk.bold(`Would eject "${appName}" to a custom runtime:`));
|
|
4678
|
+
console.log("");
|
|
4679
|
+
for (const item of planned) console.log(item.action === "write" ? ` ${chalk.green("write")} ${item.to}` : ` ${chalk.dim("keep")} ${item.to} ${chalk.dim("(already exists)")}`);
|
|
4680
|
+
console.log(` ${chalk.green("write")} rebase.json ${chalk.dim("(runtime: \"custom\")")}`);
|
|
4681
|
+
console.log("");
|
|
4682
|
+
console.log(chalk.dim("Nothing was changed."));
|
|
4683
|
+
return;
|
|
4684
|
+
}
|
|
4685
|
+
const projectName = projectNameOf(projectRoot);
|
|
4686
|
+
for (const [index, file] of PAYLOAD.entries()) {
|
|
4687
|
+
if (planned[index].action === "keep") continue;
|
|
4688
|
+
const destination = path.join(projectRoot, file.to);
|
|
4689
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
4690
|
+
const contents = fs.readFileSync(path.join(payloadDir, file.from), "utf8").replace(/\{\{PROJECT_NAME\}\}/g, projectName);
|
|
4691
|
+
fs.writeFileSync(destination, contents, "utf8");
|
|
4692
|
+
}
|
|
4693
|
+
const dockerfile = app.dockerfile ?? "Dockerfile";
|
|
4694
|
+
manifest.apps[appName] = {
|
|
4695
|
+
...app,
|
|
4696
|
+
runtime: "custom",
|
|
4697
|
+
dockerfile,
|
|
4698
|
+
port: app.port ?? 8080
|
|
4699
|
+
};
|
|
4700
|
+
writeManifest(projectRoot, manifest);
|
|
4701
|
+
restoreBackendScripts(projectRoot);
|
|
4702
|
+
console.log("");
|
|
4703
|
+
console.log(chalk.green(`✓ Ejected "${appName}" to a custom runtime.`));
|
|
4704
|
+
console.log("");
|
|
4705
|
+
console.log(` ${chalk.cyan("backend/src/index.ts".padEnd(26))} your entrypoint — the runtime no longer boots the bundle`);
|
|
4706
|
+
console.log(` ${chalk.cyan("backend/src/env.ts".padEnd(26))} the environment it reads`);
|
|
4707
|
+
console.log(` ${chalk.cyan(dockerfile.padEnd(26))} your image`);
|
|
4708
|
+
console.log(` ${chalk.cyan("docker-compose.custom.yml".padEnd(26))} runs it`);
|
|
4709
|
+
console.log(` ${chalk.cyan("rebase.json".padEnd(26))} runtime: custom`);
|
|
4710
|
+
console.log("");
|
|
4711
|
+
console.log(chalk.yellow(" You now own CORS, auth wiring, storage and shutdown. Platform runtime"));
|
|
4712
|
+
console.log(chalk.yellow(" upgrades no longer reach this project."));
|
|
4713
|
+
console.log("");
|
|
4714
|
+
console.log(chalk.dim(` ${chalk.cyan("docker compose -f docker-compose.custom.yml up --build")}`));
|
|
4715
|
+
console.log(chalk.dim(" docker-compose.yml is untouched — it still runs the managed shape if you go back."));
|
|
4716
|
+
console.log("");
|
|
4717
|
+
}
|
|
4718
|
+
/**
|
|
4719
|
+
* Point the backend workspace's scripts at the entrypoint that now exists.
|
|
4720
|
+
*
|
|
4721
|
+
* A managed project's backend package deliberately declares no `main` and no
|
|
4722
|
+
* `start`: there is no entrypoint to name. Ejecting creates one.
|
|
4723
|
+
*/
|
|
4724
|
+
function restoreBackendScripts(projectRoot) {
|
|
4725
|
+
const packagePath = path.join(projectRoot, "backend", "package.json");
|
|
4726
|
+
if (!fs.existsSync(packagePath)) return;
|
|
4727
|
+
let parsed;
|
|
4728
|
+
try {
|
|
4729
|
+
parsed = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
4730
|
+
} catch {
|
|
4731
|
+
console.log(chalk.yellow(" ⚠ backend/package.json is not valid JSON — its scripts were left alone."));
|
|
4732
|
+
return;
|
|
4733
|
+
}
|
|
4734
|
+
const scripts = parsed.scripts ?? {};
|
|
4735
|
+
parsed.main ??= "src/index.ts";
|
|
4736
|
+
scripts.dev ??= "tsx watch --include=\"../config/**/*\" --include=\"./functions/**/*\" src/index.ts";
|
|
4737
|
+
scripts.start ??= "node dist/backend/src/index.js";
|
|
4738
|
+
parsed.scripts = scripts;
|
|
4739
|
+
fs.writeFileSync(packagePath, `${JSON.stringify(parsed, null, 4)}\n`, "utf8");
|
|
4740
|
+
}
|
|
4741
|
+
//#endregion
|
|
3901
4742
|
//#region src/commands/start.ts
|
|
3902
4743
|
/**
|
|
3903
4744
|
* CLI command: rebase start
|
|
@@ -4436,7 +5277,7 @@ function parseAgentFlags(rawArgs) {
|
|
|
4436
5277
|
return requested;
|
|
4437
5278
|
}
|
|
4438
5279
|
async function skillsInstall(rawArgs = []) {
|
|
4439
|
-
const projectDir = process.cwd();
|
|
5280
|
+
const projectDir = findProjectRoot() ?? process.cwd();
|
|
4440
5281
|
let skillsDir;
|
|
4441
5282
|
try {
|
|
4442
5283
|
skillsDir = getSkillsSourceDir();
|
|
@@ -4480,7 +5321,8 @@ async function skillsInstall(rawArgs = []) {
|
|
|
4480
5321
|
for (const agentKey of agents) {
|
|
4481
5322
|
const agent = AGENTS[agentKey];
|
|
4482
5323
|
const count = installForAgent(agentKey, skills, projectDir);
|
|
4483
|
-
|
|
5324
|
+
const shown = path.relative(process.cwd(), path.join(projectDir, agent.targetDir)) || agent.targetDir;
|
|
5325
|
+
console.log(` ${chalk.green("✓")} ${chalk.bold(agent.label)} — ${count} skills installed to ${chalk.gray(shown)}`);
|
|
4484
5326
|
}
|
|
4485
5327
|
console.log("");
|
|
4486
5328
|
console.log(chalk.gray(" Skills are project-local. Commit them to share with your team."));
|
|
@@ -5345,7 +6187,7 @@ function declaredAppsFrom(manifest) {
|
|
|
5345
6187
|
if (!apps || typeof apps !== "object") return [];
|
|
5346
6188
|
return Object.entries(apps).filter(([name]) => name.trim().length > 0).map(([name, value]) => ({
|
|
5347
6189
|
name,
|
|
5348
|
-
type:
|
|
6190
|
+
type: value?.type === "backend" ? "backend" : "static"
|
|
5349
6191
|
}));
|
|
5350
6192
|
}
|
|
5351
6193
|
/** Upload a bundle archive; returns the control-plane bundle id. */
|
|
@@ -5460,12 +6302,24 @@ function resolveFrameworkVersion(sourceDir) {
|
|
|
5460
6302
|
dir = parent;
|
|
5461
6303
|
}
|
|
5462
6304
|
}
|
|
6305
|
+
/**
|
|
6306
|
+
* A progress line for a human — dropped entirely in JSON mode.
|
|
6307
|
+
*
|
|
6308
|
+
* Progress is not a result. In JSON mode stdout carries the one result value
|
|
6309
|
+
* and nothing else, so every unguarded `console.log` on a deploy path was a
|
|
6310
|
+
* line printed in front of the JSON, breaking the parser meant to read it.
|
|
6311
|
+
* Warnings are the other half of this rule and go the other way: they are
|
|
6312
|
+
* `warn`, which prints in every mode, to stderr. See `warn` in `context.ts`.
|
|
6313
|
+
*/
|
|
6314
|
+
function progress(line) {
|
|
6315
|
+
if (!isJsonMode()) console.log(line);
|
|
6316
|
+
}
|
|
5463
6317
|
/** Upload a build-context tarball; returns the opaque `source` ref for deploy. */
|
|
5464
6318
|
async function uploadSource(url, token, projectId, tarPath) {
|
|
5465
6319
|
const bytes = fs.readFileSync(tarPath);
|
|
5466
6320
|
const sizeMb = (bytes.length / 1024 / 1024).toFixed(1);
|
|
5467
6321
|
if (bytes.length > MAX_SOURCE_UPLOAD_BYTES) fail(`Source context is ${sizeMb} MB — the upload cap is ${Math.round(MAX_SOURCE_UPLOAD_BYTES / 1024 / 1024)} MB.`, "Trim the build context: exclude sourcemaps (*.map), build output and large assets via .rebaseignore or .gitignore.");
|
|
5468
|
-
|
|
6322
|
+
progress(chalk.gray(` Uploading source (${sizeMb} MB)...`));
|
|
5469
6323
|
const res = await fetch(`${url}/api/functions/deploy/upload?projectId=${encodeURIComponent(projectId)}`, {
|
|
5470
6324
|
method: "POST",
|
|
5471
6325
|
headers: {
|
|
@@ -5497,23 +6351,24 @@ async function deployBundle(opts) {
|
|
|
5497
6351
|
const loaded = loadManifest(projectRoot);
|
|
5498
6352
|
const backend = findBackendApp(loaded.manifest);
|
|
5499
6353
|
if (!backend) fail("This repository declares no backend app to deploy as a bundle.", "A managed deploy runs the backend; declare one in rebase.json, or deploy from the backend's repository.");
|
|
5500
|
-
|
|
6354
|
+
progress(chalk.gray(" Building bundle..."));
|
|
5501
6355
|
bundleDir = (await buildBundle({
|
|
5502
6356
|
projectRoot,
|
|
5503
6357
|
appName: backend.name,
|
|
5504
6358
|
app: backend.app,
|
|
5505
|
-
runtimeRange: loaded.manifest.
|
|
6359
|
+
runtimeRange: loaded.manifest.rebase,
|
|
6360
|
+
storage: loaded.manifest.storage,
|
|
5506
6361
|
skipTypeCheck: opts.skipTypeCheck,
|
|
5507
|
-
log: (m) =>
|
|
6362
|
+
log: (m) => progress(chalk.gray(m))
|
|
5508
6363
|
})).outDir;
|
|
5509
6364
|
try {
|
|
5510
6365
|
const folded = await foldFrontendIntoBundle({
|
|
5511
6366
|
projectRoot,
|
|
5512
6367
|
manifest: loaded.manifest,
|
|
5513
6368
|
bundleDir,
|
|
5514
|
-
log: (m) =>
|
|
6369
|
+
log: (m) => progress(m)
|
|
5515
6370
|
});
|
|
5516
|
-
|
|
6371
|
+
for (const outcome of folded) progress(chalk.gray(` folded ${outcome.appName} in (${outcome.fileCount} file(s), served at ${outcome.path})`));
|
|
5517
6372
|
} catch (err) {
|
|
5518
6373
|
fail(err instanceof Error ? err.message : String(err), "Fix the frontend build, or pass --no-static to deploy the API alone.");
|
|
5519
6374
|
}
|
|
@@ -5530,7 +6385,7 @@ async function deployBundle(opts) {
|
|
|
5530
6385
|
try {
|
|
5531
6386
|
await packBundle(bundleDir, tarPath);
|
|
5532
6387
|
const sizeMb = (fs.statSync(tarPath).size / 1024 / 1024).toFixed(1);
|
|
5533
|
-
|
|
6388
|
+
progress(chalk.gray(` Uploading bundle (${sizeMb} MB)...`));
|
|
5534
6389
|
bundleId = await uploadBundle(url, token, projectId, tarPath);
|
|
5535
6390
|
} catch (e) {
|
|
5536
6391
|
fail(e instanceof Error ? e.message : String(e));
|
|
@@ -5538,8 +6393,10 @@ async function deployBundle(opts) {
|
|
|
5538
6393
|
} finally {
|
|
5539
6394
|
fs.rmSync(tarPath, { force: true });
|
|
5540
6395
|
}
|
|
5541
|
-
|
|
5542
|
-
|
|
6396
|
+
if (!isJsonMode()) {
|
|
6397
|
+
console.log("");
|
|
6398
|
+
console.log(` 🚀 Triggering managed deployment for ${chalk.bold(projectRef)} (schema ${manifest.schemaVersion})...`);
|
|
6399
|
+
}
|
|
5543
6400
|
let declaredApps = [];
|
|
5544
6401
|
try {
|
|
5545
6402
|
declaredApps = declaredAppsFrom(loadManifest(process.cwd()).manifest);
|
|
@@ -5627,9 +6484,75 @@ function planBareDeploy(project, latest, now) {
|
|
|
5627
6484
|
lines: ["This project has no git repository configured and no stored source archive to rebuild.", "Upload this directory with `--source .`, or set a repository URL in the project settings."]
|
|
5628
6485
|
};
|
|
5629
6486
|
}
|
|
6487
|
+
/** `code` of the warning below, and the field name it sets in the payload. */
|
|
6488
|
+
var EJECTS_MANAGED_RUNTIME = "ejects_managed_runtime";
|
|
5630
6489
|
/** The one sentence that says a source build undoes `runtimeMode: managed`. */
|
|
5631
6490
|
function ejectWarning(projectRef) {
|
|
5632
|
-
return
|
|
6491
|
+
return {
|
|
6492
|
+
code: EJECTS_MANAGED_RUNTIME,
|
|
6493
|
+
message: `${projectRef} runs on the managed runtime — this build ejects it to a custom container.`,
|
|
6494
|
+
hint: "Use `rebase cloud deploy --bundle` to stay on managed."
|
|
6495
|
+
};
|
|
6496
|
+
}
|
|
6497
|
+
/**
|
|
6498
|
+
* Why a container-image deploy of a managed project is refused — or `undefined`
|
|
6499
|
+
* to let it through.
|
|
6500
|
+
*
|
|
6501
|
+
* Every path below this point builds a container image, and a successful one
|
|
6502
|
+
* sets `runtimeMode: "custom"` server-side. So the question is never "which flag
|
|
6503
|
+
* was used" but "did the caller ask to leave the managed runtime", and only
|
|
6504
|
+
* `--force` answers it.
|
|
6505
|
+
*
|
|
6506
|
+
* `--source` used to be read as answering it too, on the theory that uploading a
|
|
6507
|
+
* build context is self-evidently a deliberate eject. It is not: `--source`
|
|
6508
|
+
* picks *which source* gets built — this directory, rather than the stale
|
|
6509
|
+
* archive the control plane is holding — and the eject is a side effect of the
|
|
6510
|
+
* answer. That is exactly how a live project got flipped to `custom` by someone
|
|
6511
|
+
* whose actual intent was "deploy what I have here", and it is the same
|
|
6512
|
+
* ignorance the bare form is refused for. Same ignorance, same refusal.
|
|
6513
|
+
*/
|
|
6514
|
+
function ejectRefusal(opts, projectRef) {
|
|
6515
|
+
if (!opts.managed || opts.force) return void 0;
|
|
6516
|
+
const eject = "To eject on purpose, add `--force`.";
|
|
6517
|
+
if (opts.source) return {
|
|
6518
|
+
message: `${projectRef} runs on the managed runtime, and \`--source\` builds a container image from this directory — which ejects it from managed. Picking a build method is not the same as asking to leave the runtime.`,
|
|
6519
|
+
hint: `Deploy this directory to the managed runtime with \`rebase cloud deploy --bundle\`. ${eject}`,
|
|
6520
|
+
code: "managed_project"
|
|
6521
|
+
};
|
|
6522
|
+
return {
|
|
6523
|
+
message: `${projectRef} runs on the managed runtime, and a plain \`rebase cloud deploy\` builds a container image instead — ejecting it from managed, from source the control plane already holds rather than this directory.`,
|
|
6524
|
+
hint: `Redeploy it with \`rebase cloud deploy --bundle\`. ${eject} \`--source . --force\` builds this directory; \`--force\` alone builds what the control plane holds.`,
|
|
6525
|
+
code: "managed_project"
|
|
6526
|
+
};
|
|
6527
|
+
}
|
|
6528
|
+
/**
|
|
6529
|
+
* Which warnings a container-image deploy has earned.
|
|
6530
|
+
*
|
|
6531
|
+
* Pure, and separate from the printing, because the printing is what went
|
|
6532
|
+
* wrong: the eject warning used to be written inline behind `!isJsonMode()`, so
|
|
6533
|
+
* the fact that a deploy ejects a managed project existed only as a side effect
|
|
6534
|
+
* of a TTY being attached. Deciding here, emitting once at the call site, means
|
|
6535
|
+
* the decision cannot be output-mode-dependent again.
|
|
6536
|
+
*
|
|
6537
|
+
* The condition is just `managed`: anything reaching this point is a container
|
|
6538
|
+
* image build that `ejectRefusal` has already let through, and on a managed
|
|
6539
|
+
* project that is an eject however it was spelled. A caller who passed `--force`
|
|
6540
|
+
* knows — the warning is for the transcript and the payload, which is what
|
|
6541
|
+
* anyone reviewing the deploy afterwards actually reads.
|
|
6542
|
+
*/
|
|
6543
|
+
function deployWarnings(opts, projectRef) {
|
|
6544
|
+
return opts.managed ? [ejectWarning(projectRef)] : [];
|
|
6545
|
+
}
|
|
6546
|
+
/** The warning half of a deploy's JSON payload — merged into whatever it emits. */
|
|
6547
|
+
function warningPayload(warnings) {
|
|
6548
|
+
return {
|
|
6549
|
+
warnings: warnings.map((w) => ({
|
|
6550
|
+
code: w.code,
|
|
6551
|
+
message: w.message,
|
|
6552
|
+
hint: w.hint ?? null
|
|
6553
|
+
})),
|
|
6554
|
+
ejectsManagedRuntime: warnings.some((w) => w.code === EJECTS_MANAGED_RUNTIME)
|
|
6555
|
+
};
|
|
5633
6556
|
}
|
|
5634
6557
|
/**
|
|
5635
6558
|
* Read the two rows the preflight needs.
|
|
@@ -5650,6 +6573,23 @@ async function readDeployContext(client, projectId) {
|
|
|
5650
6573
|
return {};
|
|
5651
6574
|
}
|
|
5652
6575
|
}
|
|
6576
|
+
/**
|
|
6577
|
+
* Whether this repository's backend declares the managed runtime.
|
|
6578
|
+
*
|
|
6579
|
+
* Deliberately quiet: a directory that is not a Rebase project, or whose
|
|
6580
|
+
* manifest does not parse, simply does not route this way — `rebase build` is
|
|
6581
|
+
* where a broken manifest gets reported, and a deploy refusing on one before it
|
|
6582
|
+
* has even said what it is doing would be the wrong place to find out.
|
|
6583
|
+
*/
|
|
6584
|
+
function declaresManagedRuntime() {
|
|
6585
|
+
try {
|
|
6586
|
+
const projectRoot = findProjectRoot();
|
|
6587
|
+
if (!projectRoot) return false;
|
|
6588
|
+
return findBackendApp(loadManifest(projectRoot).manifest)?.app.runtime === "managed";
|
|
6589
|
+
} catch {
|
|
6590
|
+
return false;
|
|
6591
|
+
}
|
|
6592
|
+
}
|
|
5653
6593
|
async function deployCommand(rawArgs, projectRef) {
|
|
5654
6594
|
const args = arg({
|
|
5655
6595
|
"--no-follow": Boolean,
|
|
@@ -5666,8 +6606,10 @@ async function deployCommand(rawArgs, projectRef) {
|
|
|
5666
6606
|
});
|
|
5667
6607
|
const { client, url } = await requireClient(rawArgs);
|
|
5668
6608
|
const projectId = await resolveProjectRef(projectRef, client);
|
|
5669
|
-
|
|
5670
|
-
|
|
6609
|
+
const declaredManaged = !args["--source"] && !args["--bundle"] && declaresManagedRuntime();
|
|
6610
|
+
if (args["--bundle"] || declaredManaged) {
|
|
6611
|
+
if (args["--bundle"] && args["--source"]) fail("--bundle and --source cannot be combined: one is a managed bundle, the other a source build.");
|
|
6612
|
+
if (declaredManaged && !isJsonMode()) console.log(chalk.gray(" rebase.json declares runtime: managed — deploying a bundle."));
|
|
5671
6613
|
await deployBundle({
|
|
5672
6614
|
client,
|
|
5673
6615
|
url,
|
|
@@ -5681,17 +6623,18 @@ async function deployCommand(rawArgs, projectRef) {
|
|
|
5681
6623
|
}
|
|
5682
6624
|
const { project, latest } = await readDeployContext(client, projectId);
|
|
5683
6625
|
const plan = planBareDeploy(project, latest, /* @__PURE__ */ new Date());
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
6626
|
+
const eject = {
|
|
6627
|
+
managed: plan.managed,
|
|
6628
|
+
source: Boolean(args["--source"]),
|
|
6629
|
+
force: args["--force"] === true
|
|
6630
|
+
};
|
|
6631
|
+
const refusal = ejectRefusal(eject, projectRef);
|
|
6632
|
+
if (refusal) fail(refusal.message, refusal.hint, refusal.code);
|
|
6633
|
+
const warnings = deployWarnings(eject, projectRef);
|
|
6634
|
+
for (const w of warnings) warn(w.message, w.hint);
|
|
6635
|
+
if (!args["--source"] && !isJsonMode()) {
|
|
5692
6636
|
console.log("");
|
|
5693
|
-
console.log(chalk.
|
|
5694
|
-
console.log(chalk.gray(" Use `rebase cloud deploy --bundle` to stay on managed."));
|
|
6637
|
+
for (const line of plan.lines) console.log(chalk.gray(` ${line}`));
|
|
5695
6638
|
}
|
|
5696
6639
|
let source;
|
|
5697
6640
|
if (args["--source"]) {
|
|
@@ -5704,8 +6647,10 @@ async function deployCommand(rawArgs, projectRef) {
|
|
|
5704
6647
|
fs.rmSync(tarPath, { force: true });
|
|
5705
6648
|
}
|
|
5706
6649
|
}
|
|
5707
|
-
|
|
5708
|
-
|
|
6650
|
+
if (!isJsonMode()) {
|
|
6651
|
+
console.log("");
|
|
6652
|
+
console.log(` 🚀 Triggering deployment for project ${chalk.bold(projectRef)}${source ? " from uploaded source" : ""}...`);
|
|
6653
|
+
}
|
|
5709
6654
|
const body = { projectId };
|
|
5710
6655
|
if (source) body.source = source;
|
|
5711
6656
|
if (args["--message"]) body.message = args["--message"];
|
|
@@ -5733,7 +6678,8 @@ async function deployCommand(rawArgs, projectRef) {
|
|
|
5733
6678
|
deploymentId,
|
|
5734
6679
|
deduplicated,
|
|
5735
6680
|
frameworkVersion: frameworkVersion ?? null,
|
|
5736
|
-
following: false
|
|
6681
|
+
following: false,
|
|
6682
|
+
...warningPayload(warnings)
|
|
5737
6683
|
});
|
|
5738
6684
|
return;
|
|
5739
6685
|
}
|
|
@@ -5747,7 +6693,8 @@ async function deployCommand(rawArgs, projectRef) {
|
|
|
5747
6693
|
deduplicated,
|
|
5748
6694
|
frameworkVersion: frameworkVersion ?? null,
|
|
5749
6695
|
following: true,
|
|
5750
|
-
status
|
|
6696
|
+
status,
|
|
6697
|
+
...warningPayload(warnings)
|
|
5751
6698
|
});
|
|
5752
6699
|
}
|
|
5753
6700
|
/**
|
|
@@ -8361,6 +9308,7 @@ function describeDatabaseState(db) {
|
|
|
8361
9308
|
* So both are printed, rather than leaving anyone to infer one from a Docker tag.
|
|
8362
9309
|
*/
|
|
8363
9310
|
function describeRuntime(project) {
|
|
9311
|
+
if (project.runtimeMode == null || project.runtimeMode.trim() === "") return `not deployed yet ${chalk.gray("· the first deploy decides (`--bundle` keeps it managed)")}`;
|
|
8364
9312
|
if (project.runtimeMode !== "managed") return `custom ${chalk.gray("· your own image")}`;
|
|
8365
9313
|
const version = project.runtimeVersion ?? "unknown";
|
|
8366
9314
|
const framework = project.runtimeFrameworkVersion;
|
|
@@ -8759,17 +9707,41 @@ async function billingCommand(rawArgs) {
|
|
|
8759
9707
|
* dispatched from here. Individual groups live in sibling modules; this file
|
|
8760
9708
|
* only routes and prints help.
|
|
8761
9709
|
*/
|
|
8762
|
-
/**
|
|
9710
|
+
/**
|
|
9711
|
+
* Positional tokens after `rebase cloud` (group, action, …).
|
|
9712
|
+
*
|
|
9713
|
+
* Two things stop a flag being mistaken for the group. `GLOBAL_CLOUD_FLAGS` is
|
|
9714
|
+
* declared so `arg` *consumes* the flags that may precede it — critically
|
|
9715
|
+
* together with their values, which is the half that filtering cannot do. The
|
|
9716
|
+
* leading-`-` skip then covers a flag nobody declared, so an unrecognised
|
|
9717
|
+
* boolean shifts nothing.
|
|
9718
|
+
*
|
|
9719
|
+
* Only leading tokens are skipped: past the group and action, an undeclared
|
|
9720
|
+
* flag and its value are somebody else's positionals and none of our business.
|
|
9721
|
+
* A flag this file has never heard of, that takes a value, placed before the
|
|
9722
|
+
* group, is the one shape still unresolvable here — there is no way to know
|
|
9723
|
+
* whether the token after it is its value or the group, and guessing either way
|
|
9724
|
+
* is worse than the handler reporting an unknown group.
|
|
9725
|
+
*
|
|
9726
|
+
* Exported so its tests can drive the real thing. The dispatch test used to
|
|
9727
|
+
* re-implement it locally as `slice(3).filter(a => !a.startsWith("-"))` — which
|
|
9728
|
+
* filtered flags, while this function did not — so the test asserted the
|
|
9729
|
+
* behaviour we wanted against a copy that had it, and stayed green for as long
|
|
9730
|
+
* as the real dispatcher was broken.
|
|
9731
|
+
*/
|
|
8763
9732
|
function positionals(rawArgs) {
|
|
8764
|
-
|
|
9733
|
+
const rest = arg(GLOBAL_CLOUD_FLAGS, {
|
|
8765
9734
|
argv: rawArgs.slice(3),
|
|
8766
9735
|
permissive: true
|
|
8767
9736
|
})._;
|
|
9737
|
+
let i = 0;
|
|
9738
|
+
while (i < rest.length && rest[i].startsWith("-")) i++;
|
|
9739
|
+
return rest.slice(i);
|
|
8768
9740
|
}
|
|
8769
9741
|
async function cloudCommand(subcommand, rawArgs) {
|
|
8770
9742
|
initOutputMode(rawArgs);
|
|
8771
9743
|
const pos = positionals(rawArgs);
|
|
8772
|
-
const group =
|
|
9744
|
+
const group = pos[0] ?? (subcommand !== "--help" ? subcommand : void 0);
|
|
8773
9745
|
const action = pos[1];
|
|
8774
9746
|
if (!group || subcommand === "--help") {
|
|
8775
9747
|
printCloudHelp();
|
|
@@ -9039,11 +10011,8 @@ async function appsCommand(subcommand, rawArgs = []) {
|
|
|
9039
10011
|
}
|
|
9040
10012
|
function describeApp(app) {
|
|
9041
10013
|
switch (app.type) {
|
|
9042
|
-
case "backend": return `
|
|
9043
|
-
case "static": return `${app.root} → ${app.output}`;
|
|
9044
|
-
case "admin": return app.mode === "bundled" ? `bundled → ${app.output ?? "?"}` : "hosted by the platform";
|
|
9045
|
-
case "mobile": return app.platform;
|
|
9046
|
-
case "custom": return app.dockerfile ?? "Dockerfile";
|
|
10014
|
+
case "backend": return app.runtime === "custom" ? `custom runtime — ${app.dockerfile ?? "Dockerfile"}` : `managed runtime, config: ${app.config ?? "config"}`;
|
|
10015
|
+
case "static": return `${app.root} → ${app.output} @ ${app.path ?? "/"}`;
|
|
9047
10016
|
default: return "";
|
|
9048
10017
|
}
|
|
9049
10018
|
}
|
|
@@ -9053,7 +10022,7 @@ async function listApps(asJson) {
|
|
|
9053
10022
|
if (asJson) {
|
|
9054
10023
|
console.log(JSON.stringify({
|
|
9055
10024
|
source: loaded.source,
|
|
9056
|
-
|
|
10025
|
+
rebase: loaded.manifest.rebase,
|
|
9057
10026
|
apps: loaded.manifest.apps,
|
|
9058
10027
|
managed: compatibility
|
|
9059
10028
|
}, null, 2));
|
|
@@ -9063,7 +10032,7 @@ async function listApps(asJson) {
|
|
|
9063
10032
|
console.log(chalk.dim("No rebase.json — showing the layout inferred from this project."));
|
|
9064
10033
|
console.log(chalk.dim(`Run ${chalk.cyan("rebase apps init")} to write it down.\n`));
|
|
9065
10034
|
}
|
|
9066
|
-
console.log(chalk.bold(`
|
|
10035
|
+
console.log(chalk.bold(`Rebase ${loaded.manifest.rebase}`));
|
|
9067
10036
|
console.log("");
|
|
9068
10037
|
const entries = Object.entries(loaded.manifest.apps);
|
|
9069
10038
|
if (entries.length === 0) {
|
|
@@ -9194,8 +10163,9 @@ async function entry(args) {
|
|
|
9194
10163
|
console.log(getVersion());
|
|
9195
10164
|
return;
|
|
9196
10165
|
}
|
|
9197
|
-
const
|
|
9198
|
-
const
|
|
10166
|
+
const words = parsedArgs._.filter((a) => !a.startsWith("-"));
|
|
10167
|
+
const command = words[0];
|
|
10168
|
+
const subcommand = words[1];
|
|
9199
10169
|
if (!command || parsedArgs["--help"] && ![
|
|
9200
10170
|
"init",
|
|
9201
10171
|
"schema",
|
|
@@ -9209,6 +10179,7 @@ async function entry(args) {
|
|
|
9209
10179
|
"api-keys",
|
|
9210
10180
|
"cloud",
|
|
9211
10181
|
"apps",
|
|
10182
|
+
"eject",
|
|
9212
10183
|
"generate-sdk"
|
|
9213
10184
|
].includes(command)) {
|
|
9214
10185
|
printHelp();
|
|
@@ -9233,9 +10204,10 @@ async function entry(args) {
|
|
|
9233
10204
|
argv: args.slice(3),
|
|
9234
10205
|
permissive: true
|
|
9235
10206
|
});
|
|
10207
|
+
const sdkRoot = sdkArgs["--help"] ? process.cwd() : requireProjectRoot();
|
|
9236
10208
|
await generateSdkCommand({
|
|
9237
|
-
collectionsDir: sdkArgs["--collections-dir"] || "
|
|
9238
|
-
output: sdkArgs["--output"] || "
|
|
10209
|
+
collectionsDir: sdkArgs["--collections-dir"] || path.join(sdkRoot, "config/collections"),
|
|
10210
|
+
output: sdkArgs["--output"] || path.join(sdkRoot, "generated/sdk"),
|
|
9239
10211
|
from: sdkArgs["--from"],
|
|
9240
10212
|
token: sdkArgs["--token"],
|
|
9241
10213
|
help: sdkArgs["--help"],
|
|
@@ -9261,6 +10233,9 @@ async function entry(args) {
|
|
|
9261
10233
|
case "apps":
|
|
9262
10234
|
await appsCommand(effectiveSubcommand, args);
|
|
9263
10235
|
break;
|
|
10236
|
+
case "eject":
|
|
10237
|
+
await ejectCommand(args);
|
|
10238
|
+
break;
|
|
9264
10239
|
case "auth":
|
|
9265
10240
|
await authCommand(effectiveSubcommand, args);
|
|
9266
10241
|
break;
|
|
@@ -9295,6 +10270,8 @@ ${chalk.green.bold("Commands")}
|
|
|
9295
10270
|
${chalk.blue.bold("dev")} Start the development server
|
|
9296
10271
|
${chalk.blue.bold("build")} Build all workspace packages
|
|
9297
10272
|
${chalk.blue.bold("start")} Start the backend server ${chalk.gray("(production)")}
|
|
10273
|
+
${chalk.blue.bold("apps list")} Show the apps this repository declares
|
|
10274
|
+
${chalk.blue.bold("eject")} Take ownership of the server process and image
|
|
9298
10275
|
|
|
9299
10276
|
${chalk.green.bold("Schema")}
|
|
9300
10277
|
${chalk.blue.bold("schema generate")} Generate Drizzle schema from collections
|
|
@@ -9312,25 +10289,25 @@ ${chalk.green.bold("SDK")}
|
|
|
9312
10289
|
|
|
9313
10290
|
${chalk.green.bold("Auth")}
|
|
9314
10291
|
${chalk.blue.bold("auth reset-password")} Reset a user's password
|
|
9315
|
-
${chalk.blue.bold("auth")} ${chalk.gray("--help")}
|
|
10292
|
+
${chalk.blue.bold("auth")} ${chalk.gray("--help")} Show auth command help
|
|
9316
10293
|
|
|
9317
10294
|
${chalk.green.bold("Diagnostics")}
|
|
9318
10295
|
${chalk.blue.bold("doctor")} Detect schema drift between collections, schema, and DB
|
|
9319
10296
|
|
|
9320
10297
|
${chalk.green.bold("AI Agent Skills")}
|
|
9321
|
-
${chalk.blue.bold("skills install")}
|
|
10298
|
+
${chalk.blue.bold("skills install")} Install Rebase agent skills for your AI coding assistant
|
|
9322
10299
|
|
|
9323
10300
|
${chalk.green.bold("API Keys")}
|
|
9324
|
-
${chalk.blue.bold("api-keys list")}
|
|
9325
|
-
${chalk.blue.bold("api-keys create")}
|
|
9326
|
-
${chalk.blue.bold("api-keys revoke")}
|
|
9327
|
-
${chalk.blue.bold("api-keys")} ${chalk.gray("--help")}
|
|
10301
|
+
${chalk.blue.bold("api-keys list")} List all service API keys
|
|
10302
|
+
${chalk.blue.bold("api-keys create")} Create a new scoped API key
|
|
10303
|
+
${chalk.blue.bold("api-keys revoke")} Revoke an existing API key
|
|
10304
|
+
${chalk.blue.bold("api-keys")} ${chalk.gray("--help")} Show API key command help
|
|
9328
10305
|
|
|
9329
10306
|
${chalk.green.bold("Rebase Cloud")}
|
|
9330
10307
|
${chalk.blue.bold("cloud login")} Sign in to the hosted control plane
|
|
9331
10308
|
${chalk.blue.bold("cloud link")} Link this directory to a cloud project
|
|
9332
10309
|
${chalk.blue.bold("cloud deploy")} Deploy the linked project + stream logs
|
|
9333
|
-
${chalk.blue.bold("cloud")} ${chalk.gray("--help")}
|
|
10310
|
+
${chalk.blue.bold("cloud")} ${chalk.gray("--help")} Show all cloud commands
|
|
9334
10311
|
|
|
9335
10312
|
${chalk.green.bold("Options")}
|
|
9336
10313
|
${chalk.blue("--version, -v")} Show version number
|
|
@@ -9340,6 +10317,6 @@ ${chalk.gray("Documentation: https://rebase.pro/docs")}
|
|
|
9340
10317
|
`);
|
|
9341
10318
|
}
|
|
9342
10319
|
//#endregion
|
|
9343
|
-
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, findUnusedServerEntry, foldStaticIntoBundle, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, isIdentifierLike, isPnpmAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, printInitHelp, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveExampleBaseUrl, resolveLocalBin, resolvePluginCliScript, resolveTsx, schemaCommand, startCommand, synthesizeManifest, validateManifest, validateProjectName, validateTsxInstallation, writeManifest };
|
|
10320
|
+
export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, DEV_PORT_FILENAME, MANIFEST_FILENAME, ManifestError, TEMPLATE_PLACEHOLDER_FILES, appsCommand, assessManagedCompatibility, authCommand, buildBundle, buildCommand, buildInitQuestions, buildStaticBundle, buildableApps, cloudCommand, collectDeclaredDependencies, configureEnvFile, createRebaseApp, dbCommand, detectFrameworkDepDrift, detectNativeDependencies, detectPackageManager, detectStorageAuthorize, devCommand, doctorCommand, ejectCommand, entry, findBackendApp, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, findUnusedServerEntry, foldStaticIntoBundle, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, getProjectPort, isIdentifierLike, isPnpmAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, positionals, printInitHelp, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveCliVersion, resolveExampleBaseUrl, resolveLocalBin, resolvePluginCliScript, resolveStartPort, resolveTsx, schemaCommand, startCommand, synthesizeManifest, validateManifest, validateProjectName, validateTsxInstallation, writeManifest };
|
|
9344
10321
|
|
|
9345
10322
|
//# sourceMappingURL=index.es.js.map
|