@rebasepro/cli 0.11.1-canary.gfd39654 → 0.12.1-canary.g06f263c
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/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 +1060 -258
- 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
|
*
|
|
@@ -829,7 +830,7 @@ function openUrl(target, label = "Opening") {
|
|
|
829
830
|
//#region src/commands/init.ts
|
|
830
831
|
var access = promisify(fs.access);
|
|
831
832
|
var __filename$1 = fileURLToPath(import.meta.url);
|
|
832
|
-
var __dirname$
|
|
833
|
+
var __dirname$2 = path.dirname(__filename$1);
|
|
833
834
|
function findParentDir(currentDir, targetName) {
|
|
834
835
|
const root = path.parse(currentDir).root;
|
|
835
836
|
while (currentDir && currentDir !== root) {
|
|
@@ -838,22 +839,56 @@ function findParentDir(currentDir, targetName) {
|
|
|
838
839
|
}
|
|
839
840
|
return null;
|
|
840
841
|
}
|
|
841
|
-
var cliRoot = findParentDir(__dirname$
|
|
842
|
+
var cliRoot = findParentDir(__dirname$2, "cli");
|
|
842
843
|
var PROJECT_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
844
|
+
/**
|
|
845
|
+
* Every scaffolded file that carries a `{{PLACEHOLDER}}`.
|
|
846
|
+
*
|
|
847
|
+
* Exported because it is the single source of truth: `init.test.ts` reads it and
|
|
848
|
+
* asserts that every template file containing `{{` appears here. It used to be a
|
|
849
|
+
* local, with the test harness keeping a *second* copy — so the two drifted, and
|
|
850
|
+
* a test built on the copy could not observe the production list at all.
|
|
851
|
+
*
|
|
852
|
+
* The drift shipped: `docker-compose.yml` arrived with the self-host work
|
|
853
|
+
* (26fd5259c) and was added to neither, so every scaffolded project got a literal
|
|
854
|
+
* `name: {{PROJECT_NAME}}`. In YAML `{{...}}` is a map, not a string, so the
|
|
855
|
+
* documented `docker compose up` path failed on the file before doing anything:
|
|
856
|
+
*
|
|
857
|
+
* yaml: unmarshal errors: line 28: cannot unmarshal !!map into string
|
|
858
|
+
*/
|
|
859
|
+
var TEMPLATE_PLACEHOLDER_FILES = [
|
|
860
|
+
"package.json",
|
|
861
|
+
"frontend/package.json",
|
|
862
|
+
"backend/package.json",
|
|
863
|
+
"config/package.json",
|
|
864
|
+
"frontend/index.html",
|
|
865
|
+
"pnpm-workspace.yaml",
|
|
866
|
+
"docker-compose.yml",
|
|
867
|
+
"README.md"
|
|
868
|
+
];
|
|
843
869
|
/** Returns an error message, or null when the name is a valid package name. */
|
|
844
870
|
function validateProjectName(name) {
|
|
845
871
|
if (!name.trim()) return "Project name is required";
|
|
846
872
|
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
873
|
return null;
|
|
848
874
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
875
|
+
/**
|
|
876
|
+
* Whether to scaffold the admin panel alongside the backend.
|
|
877
|
+
*
|
|
878
|
+
* A boolean, not a named pair. It was `--flavor cms|baas`, and neither word
|
|
879
|
+
* survived what they described: "cms" is a product category rather than a thing
|
|
880
|
+
* this tool builds, and "baas" was the same value that used to appear as
|
|
881
|
+
* `backend.mode` — which is now derived from whether collections are declared.
|
|
882
|
+
* What is left is one question: does this project get an admin UI?
|
|
883
|
+
*/
|
|
884
|
+
var HEADLESS_CHOICES = [{
|
|
885
|
+
name: "Backend + admin — API plus an admin UI, driven by collections you define (like Payload/Directus)",
|
|
886
|
+
value: false,
|
|
887
|
+
short: "Backend + admin"
|
|
853
888
|
}, {
|
|
854
|
-
name: "
|
|
855
|
-
value:
|
|
856
|
-
short: "
|
|
889
|
+
name: "Backend only — headless API over your database. No collections, no UI (like Supabase)",
|
|
890
|
+
value: true,
|
|
891
|
+
short: "Backend only"
|
|
857
892
|
}];
|
|
858
893
|
var PRESET_CHOICES = [
|
|
859
894
|
{
|
|
@@ -878,7 +913,7 @@ var PRESET_CHOICES = [
|
|
|
878
913
|
* types registered by the installed version of inquirer.
|
|
879
914
|
*/
|
|
880
915
|
function buildInitQuestions(params) {
|
|
881
|
-
const { nameArg, templateArg,
|
|
916
|
+
const { nameArg, templateArg, headlessArg, hasGitFlag, hasInstallFlag, pm } = params;
|
|
882
917
|
const questions = [];
|
|
883
918
|
if (!nameArg) questions.push({
|
|
884
919
|
type: "input",
|
|
@@ -887,12 +922,12 @@ function buildInitQuestions(params) {
|
|
|
887
922
|
default: "my-rebase-app",
|
|
888
923
|
validate: (input) => validateProjectName(input) ?? true
|
|
889
924
|
});
|
|
890
|
-
if (
|
|
925
|
+
if (headlessArg === void 0) questions.push({
|
|
891
926
|
type: "select",
|
|
892
|
-
name: "
|
|
927
|
+
name: "headless",
|
|
893
928
|
message: "What do you want to build?",
|
|
894
|
-
choices:
|
|
895
|
-
default:
|
|
929
|
+
choices: HEADLESS_CHOICES,
|
|
930
|
+
default: false
|
|
896
931
|
});
|
|
897
932
|
if (!templateArg) questions.push({
|
|
898
933
|
type: "select",
|
|
@@ -900,7 +935,7 @@ function buildInitQuestions(params) {
|
|
|
900
935
|
message: "Choose a starter template:",
|
|
901
936
|
choices: PRESET_CHOICES,
|
|
902
937
|
default: "blog",
|
|
903
|
-
when: (answers) => (
|
|
938
|
+
when: (answers) => !(headlessArg ?? answers.headless)
|
|
904
939
|
});
|
|
905
940
|
if (!hasGitFlag) questions.push({
|
|
906
941
|
type: "confirm",
|
|
@@ -956,7 +991,7 @@ ${chalk.bold("Usage")}
|
|
|
956
991
|
|
|
957
992
|
${chalk.bold("Options")}
|
|
958
993
|
${chalk.blue("-t, --template")} ${chalk.gray("<preset>")} blog | ecommerce | blank ${chalk.gray("(default: blog)")}
|
|
959
|
-
${chalk.blue("
|
|
994
|
+
${chalk.blue("--headless")} Backend only — no admin panel, no collections
|
|
960
995
|
${chalk.blue("-y, --yes")} Accept defaults, never prompt ${chalk.gray("(required for CI / non-TTY)")}
|
|
961
996
|
${chalk.blue("-i, --install")} Install dependencies after scaffolding
|
|
962
997
|
${chalk.blue("-g, --git")} Initialize a git repository and make an initial commit
|
|
@@ -965,14 +1000,14 @@ ${chalk.bold("Options")}
|
|
|
965
1000
|
${chalk.blue("--project")} ${chalk.gray("<slug>")} Link the scaffold to a Rebase Cloud project
|
|
966
1001
|
${chalk.blue("--setup-key")} ${chalk.gray("<key>")} One-time key authenticating the cloud link ${chalk.gray("(use with --project)")}
|
|
967
1002
|
|
|
968
|
-
${chalk.bold("
|
|
969
|
-
${chalk.
|
|
970
|
-
${chalk.blue("
|
|
971
|
-
|
|
1003
|
+
${chalk.bold("What gets scaffolded")}
|
|
1004
|
+
${chalk.gray("default")} Backend + an admin UI, driven by collections you define ${chalk.gray("(like Payload/Directus)")}
|
|
1005
|
+
${chalk.blue("--headless")} Backend only, over your existing database ${chalk.gray("(like Supabase)")}
|
|
1006
|
+
${chalk.gray("--template has no effect: there are no collections to seed.")}
|
|
972
1007
|
|
|
973
1008
|
${chalk.bold("Examples")}
|
|
974
1009
|
${chalk.gray("$")} rebase init my-shop --template ecommerce --install
|
|
975
|
-
${chalk.gray("$")} rebase init my-api --
|
|
1010
|
+
${chalk.gray("$")} rebase init my-api --headless --yes
|
|
976
1011
|
${chalk.gray("$")} rebase init . --yes --git
|
|
977
1012
|
`);
|
|
978
1013
|
}
|
|
@@ -993,14 +1028,13 @@ async function promptForOptions(rawArgs, pm) {
|
|
|
993
1028
|
"--database-url": String,
|
|
994
1029
|
"--introspect": Boolean,
|
|
995
1030
|
"--template": String,
|
|
996
|
-
"--
|
|
1031
|
+
"--headless": Boolean,
|
|
997
1032
|
"--project": String,
|
|
998
1033
|
"--setup-key": String,
|
|
999
1034
|
"--yes": Boolean,
|
|
1000
1035
|
"-g": "--git",
|
|
1001
1036
|
"-i": "--install",
|
|
1002
1037
|
"-t": "--template",
|
|
1003
|
-
"-f": "--flavor",
|
|
1004
1038
|
"-y": "--yes"
|
|
1005
1039
|
}, {
|
|
1006
1040
|
argv: rawArgs.slice(3),
|
|
@@ -1021,11 +1055,7 @@ async function promptForOptions(rawArgs, pm) {
|
|
|
1021
1055
|
console.error(chalk.red(`Unknown template "${templateArg}". Available: ${PRESET_CHOICES.map((p) => p.value).join(", ")}`));
|
|
1022
1056
|
process.exit(1);
|
|
1023
1057
|
}
|
|
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
|
-
}
|
|
1058
|
+
const headlessArg = args["--headless"] === true ? true : void 0;
|
|
1029
1059
|
if (isNonInteractive) {
|
|
1030
1060
|
const projectName = nameArg || "my-rebase-app";
|
|
1031
1061
|
const targetDirectory = path.resolve(process.cwd(), projectName);
|
|
@@ -1041,7 +1071,7 @@ async function promptForOptions(rawArgs, pm) {
|
|
|
1041
1071
|
introspect: args["--introspect"] || false,
|
|
1042
1072
|
preset: templateArg || "blog",
|
|
1043
1073
|
explicitPreset: !!templateArg,
|
|
1044
|
-
|
|
1074
|
+
headless: headlessArg ?? false,
|
|
1045
1075
|
pm,
|
|
1046
1076
|
pmCommands,
|
|
1047
1077
|
cloudProject: args["--project"] || void 0,
|
|
@@ -1052,14 +1082,14 @@ async function promptForOptions(rawArgs, pm) {
|
|
|
1052
1082
|
if (!process.stdin.isTTY) {
|
|
1053
1083
|
console.error(chalk.red("Cannot prompt: this is a non-interactive terminal (no TTY)."));
|
|
1054
1084
|
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> --
|
|
1085
|
+
console.error(chalk.yellow(` rebase init ${nameArg || "my-app"} --yes --template blog`));
|
|
1086
|
+
console.error(chalk.gray(" Options: --template <blog|ecommerce|blank> --headless --database-url <url> --install --git"));
|
|
1057
1087
|
process.exit(1);
|
|
1058
1088
|
}
|
|
1059
1089
|
const questions = buildInitQuestions({
|
|
1060
1090
|
nameArg,
|
|
1061
1091
|
templateArg,
|
|
1062
|
-
|
|
1092
|
+
headlessArg,
|
|
1063
1093
|
hasGitFlag: !!args["--git"],
|
|
1064
1094
|
hasInstallFlag: !!args["--install"],
|
|
1065
1095
|
pm
|
|
@@ -1079,7 +1109,7 @@ async function promptForOptions(rawArgs, pm) {
|
|
|
1079
1109
|
introspect: answers.introspect || false,
|
|
1080
1110
|
preset: templateArg || answers.preset || "blog",
|
|
1081
1111
|
explicitPreset: !!templateArg,
|
|
1082
|
-
|
|
1112
|
+
headless: headlessArg ?? Boolean(answers.headless),
|
|
1083
1113
|
pm,
|
|
1084
1114
|
pmCommands,
|
|
1085
1115
|
cloudProject: args["--project"] || void 0,
|
|
@@ -1163,12 +1193,12 @@ async function createProject$1(options) {
|
|
|
1163
1193
|
const shipped = path.join(options.targetDirectory, from);
|
|
1164
1194
|
if (fs.existsSync(shipped)) fs.renameSync(shipped, path.join(options.targetDirectory, to));
|
|
1165
1195
|
}
|
|
1166
|
-
if (options.
|
|
1167
|
-
if (options.
|
|
1196
|
+
if (options.headless && options.explicitPreset) console.log(chalk.yellow(` Ignoring --template ${options.preset}: a headless project declares no collections.`));
|
|
1197
|
+
if (!options.headless) {
|
|
1168
1198
|
if (options.introspect && options.preset !== "blank") console.log(chalk.gray(" Using the blank template: collections will come from your database."));
|
|
1169
1199
|
await applyPreset(options.targetDirectory, options.introspect ? "blank" : options.preset);
|
|
1170
1200
|
}
|
|
1171
|
-
await
|
|
1201
|
+
await applyHeadless(options.targetDirectory, options.headless);
|
|
1172
1202
|
await replacePlaceholders(options);
|
|
1173
1203
|
await configureEnvFile(options.targetDirectory, options.databaseUrl);
|
|
1174
1204
|
if (options.git) {
|
|
@@ -1266,7 +1296,7 @@ async function createProject$1(options) {
|
|
|
1266
1296
|
console.log("");
|
|
1267
1297
|
const runDev = pmCommands.run("dev");
|
|
1268
1298
|
const runDbPush = pmCommands.run("db:push");
|
|
1269
|
-
const isBaas = options.
|
|
1299
|
+
const isBaas = options.headless;
|
|
1270
1300
|
const cdTarget = formatCdTarget(process.cwd(), options.targetDirectory);
|
|
1271
1301
|
if (cdTarget) console.log(` ${chalk.cyan("cd")} ${cdTarget}`);
|
|
1272
1302
|
if (!options.installDeps) console.log(` ${chalk.cyan(installCmd.join(" "))}`);
|
|
@@ -1336,18 +1366,31 @@ async function createProject$1(options) {
|
|
|
1336
1366
|
* presets directory so the final project is clean.
|
|
1337
1367
|
*/
|
|
1338
1368
|
/**
|
|
1339
|
-
* Reduce the scaffolded project to the chosen
|
|
1369
|
+
* Reduce the scaffolded project to the chosen shape.
|
|
1370
|
+
*
|
|
1371
|
+
* The base template is the full triad. `--headless` drops the frontend and the
|
|
1372
|
+
* declared collections — there is nothing to define, since the server derives
|
|
1373
|
+
* its API from the database — and overlays the files that differ.
|
|
1340
1374
|
*
|
|
1341
|
-
* The
|
|
1342
|
-
*
|
|
1343
|
-
*
|
|
1375
|
+
* The config *package* stays, holding only `storageAuthorize`. Storage is not
|
|
1376
|
+
* under row-level security, so a deployment with file storage enabled and no
|
|
1377
|
+
* access model serves every user's files to every signed-in user; the server
|
|
1378
|
+
* refuses to boot in that state. Deleting the package outright would leave a
|
|
1379
|
+
* headless project with nowhere to put the hook, and the scaffold's own
|
|
1380
|
+
* docker-compose.yml enables storage — so the first `docker compose up` would
|
|
1381
|
+
* crash-loop.
|
|
1344
1382
|
*/
|
|
1345
|
-
async function
|
|
1346
|
-
if (
|
|
1347
|
-
|
|
1383
|
+
async function applyHeadless(targetDirectory, headless) {
|
|
1384
|
+
if (!headless) return;
|
|
1385
|
+
fs.rmSync(path.join(targetDirectory, "frontend"), {
|
|
1386
|
+
recursive: true,
|
|
1387
|
+
force: true
|
|
1388
|
+
});
|
|
1389
|
+
fs.rmSync(path.join(targetDirectory, "config", "collections"), {
|
|
1348
1390
|
recursive: true,
|
|
1349
1391
|
force: true
|
|
1350
1392
|
});
|
|
1393
|
+
for (const stray of ["admin.d.ts", "frontend-assets.d.ts"]) fs.rmSync(path.join(targetDirectory, "config", stray), { force: true });
|
|
1351
1394
|
fs.rmSync(path.join(targetDirectory, "backend", "src", "schema.generated.ts"), { force: true });
|
|
1352
1395
|
const overlayDir = path.resolve(cliRoot, "templates", "overlays", "baas");
|
|
1353
1396
|
if (!fs.existsSync(overlayDir)) {
|
|
@@ -1394,15 +1437,7 @@ function cleanupPresets(presetsDir) {
|
|
|
1394
1437
|
});
|
|
1395
1438
|
}
|
|
1396
1439
|
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
|
-
];
|
|
1440
|
+
const filesToProcess = TEMPLATE_PLACEHOLDER_FILES;
|
|
1406
1441
|
const packageJsonPath = path.resolve(cliRoot, "package.json");
|
|
1407
1442
|
let cliVersion = "latest";
|
|
1408
1443
|
if (fs.existsSync(packageJsonPath)) cliVersion = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")).version || "latest";
|
|
@@ -1427,9 +1462,10 @@ async function replacePlaceholders(options) {
|
|
|
1427
1462
|
versionToUse = stdout.trim();
|
|
1428
1463
|
} catch {
|
|
1429
1464
|
try {
|
|
1465
|
+
const tag = cliVersion.includes("canary") ? "canary" : "latest";
|
|
1430
1466
|
const { stdout } = await execa(viewBin, [
|
|
1431
1467
|
"view",
|
|
1432
|
-
`${pkgName}@${
|
|
1468
|
+
`${pkgName}@${tag}`,
|
|
1433
1469
|
"version"
|
|
1434
1470
|
]);
|
|
1435
1471
|
if (!stdout.trim()) throw new Error("Not found");
|
|
@@ -1497,6 +1533,26 @@ async function findAvailablePort(startPort) {
|
|
|
1497
1533
|
while (!await isPortAvailable(port)) port++;
|
|
1498
1534
|
return port;
|
|
1499
1535
|
}
|
|
1536
|
+
/**
|
|
1537
|
+
* The CLI's own version, which is the runtime image tag a scaffolded project
|
|
1538
|
+
* pins. They move together: the CLI, the packages and the runtime image are cut
|
|
1539
|
+
* from one release, so the version that installed the project is the version
|
|
1540
|
+
* whose runtime can boot its bundle.
|
|
1541
|
+
*
|
|
1542
|
+
* Falls back to `latest` only when the CLI cannot read its own manifest — a
|
|
1543
|
+
* floating tag is worse than a pinned one, but better than a compose file that
|
|
1544
|
+
* names a version that was never published.
|
|
1545
|
+
*/
|
|
1546
|
+
function readCliVersion() {
|
|
1547
|
+
try {
|
|
1548
|
+
const manifest = path.resolve(cliRoot, "package.json");
|
|
1549
|
+
if (fs.existsSync(manifest)) {
|
|
1550
|
+
const pkg = JSON.parse(fs.readFileSync(manifest, "utf-8"));
|
|
1551
|
+
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
1552
|
+
}
|
|
1553
|
+
} catch {}
|
|
1554
|
+
return "latest";
|
|
1555
|
+
}
|
|
1500
1556
|
async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
1501
1557
|
const envExamplePath = path.join(targetDirectory, ".env.example");
|
|
1502
1558
|
const envPath = path.join(targetDirectory, ".env");
|
|
@@ -1506,11 +1562,20 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
1506
1562
|
const dbPassword = crypto.randomBytes(16).toString("hex");
|
|
1507
1563
|
const serviceKey = crypto.randomBytes(48).toString("base64");
|
|
1508
1564
|
let envContent = fs.readFileSync(envPath, "utf-8");
|
|
1565
|
+
envContent = envContent.replace(/^(# ║ {2})(Copy this file to \.env and fill in the values)( +║)$/m, (_match, open, old, close) => {
|
|
1566
|
+
const replacement = "Generated by `rebase init` — the secrets below are already set";
|
|
1567
|
+
const width = old.length + close.length - 1;
|
|
1568
|
+
return open + replacement.slice(0, width).padEnd(width, " ") + "║";
|
|
1569
|
+
});
|
|
1509
1570
|
envContent = envContent.replace(/^JWT_SECRET=.*$/m, `JWT_SECRET=${jwtSecret}`);
|
|
1510
1571
|
envContent = envContent.replace(/^#\s*REBASE_SERVICE_KEY=.*$/m, `REBASE_SERVICE_KEY=${serviceKey}`);
|
|
1572
|
+
const runtimeVersion = readCliVersion();
|
|
1573
|
+
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
1574
|
if (databaseUrl) {
|
|
1512
1575
|
if (/[\r\n]/.test(databaseUrl)) throw new Error("Invalid DATABASE_URL: multiline values are not allowed.");
|
|
1513
|
-
|
|
1576
|
+
const { pinSearchPath } = await import("@rebasepro/server-postgres");
|
|
1577
|
+
const pinnedUrl = pinSearchPath(databaseUrl);
|
|
1578
|
+
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${pinnedUrl}\nDATABASE_PASSWORD=${dbPassword}`);
|
|
1514
1579
|
} else {
|
|
1515
1580
|
const dbPort = await findAvailablePort(5432);
|
|
1516
1581
|
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 +2079,51 @@ var ManifestError = class extends Error {
|
|
|
2014
2079
|
this.name = "ManifestError";
|
|
2015
2080
|
}
|
|
2016
2081
|
};
|
|
2017
|
-
var APP_TYPES = [
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2082
|
+
var APP_TYPES = ["backend", "static"];
|
|
2083
|
+
/**
|
|
2084
|
+
* App types that used to exist, and what replaced each one.
|
|
2085
|
+
*
|
|
2086
|
+
* Kept so a manifest written against the old vocabulary fails with the fix in
|
|
2087
|
+
* hand rather than with `must be one of: backend, static`, which tells a reader
|
|
2088
|
+
* what is wrong but not what to write instead.
|
|
2089
|
+
*/
|
|
2090
|
+
var REMOVED_APP_TYPES = {
|
|
2091
|
+
admin: "the admin panel is an ordinary static app now — declare it as { \"type\": \"static\", \"root\": \"frontend\", \"output\": \"frontend/dist\", \"path\": \"/admin\" }",
|
|
2092
|
+
custom: "who owns the server is a property of the backend now — declare { \"type\": \"backend\", \"runtime\": \"custom\" } instead of a separate app",
|
|
2093
|
+
mobile: "mobile apps are no longer declared in the manifest — nothing consumed this type. Remove the entry"
|
|
2094
|
+
};
|
|
2024
2095
|
/** Reserved because they name things in URLs and CLI output. */
|
|
2025
|
-
var RESERVED_APP_NAMES = new Set([
|
|
2096
|
+
var RESERVED_APP_NAMES = /* @__PURE__ */ new Set([
|
|
2026
2097
|
"api",
|
|
2027
2098
|
"health",
|
|
2028
2099
|
"metrics",
|
|
2029
2100
|
"livez",
|
|
2030
2101
|
"_rebase"
|
|
2031
2102
|
]);
|
|
2103
|
+
/**
|
|
2104
|
+
* Validate a static app's public base path.
|
|
2105
|
+
*
|
|
2106
|
+
* Normalizes to "no trailing slash, except for the root" so that mounting and
|
|
2107
|
+
* the `REBASE_APP_BASE` build variable have one shape to reason about.
|
|
2108
|
+
*/
|
|
2109
|
+
function checkAppPath(value, fieldPath, issues) {
|
|
2110
|
+
if (value === void 0) return void 0;
|
|
2111
|
+
if (typeof value !== "string" || !value.startsWith("/") || value.includes("..")) {
|
|
2112
|
+
issues.push({
|
|
2113
|
+
path: fieldPath,
|
|
2114
|
+
message: "must be an absolute path like \"/admin\""
|
|
2115
|
+
});
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
if (value !== "/" && value.endsWith("/")) {
|
|
2119
|
+
issues.push({
|
|
2120
|
+
path: fieldPath,
|
|
2121
|
+
message: "must not end with a slash — write \"/admin\", not \"/admin/\""
|
|
2122
|
+
});
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
return value;
|
|
2126
|
+
}
|
|
2032
2127
|
function isRecord(value) {
|
|
2033
2128
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2034
2129
|
}
|
|
@@ -2071,6 +2166,73 @@ function checkRelativePath(value, fieldPath, issues, { required }) {
|
|
|
2071
2166
|
}
|
|
2072
2167
|
return value;
|
|
2073
2168
|
}
|
|
2169
|
+
/** Fields each app type understands. Anything else is a typo or a newer CLI. */
|
|
2170
|
+
var KNOWN_APP_FIELDS = {
|
|
2171
|
+
backend: [
|
|
2172
|
+
"type",
|
|
2173
|
+
"runtime",
|
|
2174
|
+
"config",
|
|
2175
|
+
"functions",
|
|
2176
|
+
"crons",
|
|
2177
|
+
"schema",
|
|
2178
|
+
"usersCollection",
|
|
2179
|
+
"dockerfile",
|
|
2180
|
+
"context",
|
|
2181
|
+
"port"
|
|
2182
|
+
],
|
|
2183
|
+
static: [
|
|
2184
|
+
"type",
|
|
2185
|
+
"root",
|
|
2186
|
+
"build",
|
|
2187
|
+
"output",
|
|
2188
|
+
"path",
|
|
2189
|
+
"spa"
|
|
2190
|
+
]
|
|
2191
|
+
};
|
|
2192
|
+
/**
|
|
2193
|
+
* Report a field this CLI does not recognise.
|
|
2194
|
+
*
|
|
2195
|
+
* A warning rather than an error, and the distinction matters in both
|
|
2196
|
+
* directions. `"pathh": "/admin"` is a typo that would otherwise be silently
|
|
2197
|
+
* dropped — the app builds for `/`, mounts at `/`, and the only symptom is that
|
|
2198
|
+
* it is not where you put it. But an unknown field is also what an *older* CLI
|
|
2199
|
+
* sees when it opens a manifest written for a newer one, and refusing to build
|
|
2200
|
+
* over a field that is simply from the future would make every manifest addition
|
|
2201
|
+
* a breaking change.
|
|
2202
|
+
*
|
|
2203
|
+
* So: name it, suggest the near-miss, and carry on.
|
|
2204
|
+
*/
|
|
2205
|
+
function warnUnknownFields(name, raw, type) {
|
|
2206
|
+
const known = KNOWN_APP_FIELDS[type];
|
|
2207
|
+
if (!known) return;
|
|
2208
|
+
for (const field of Object.keys(raw)) {
|
|
2209
|
+
if (known.includes(field)) continue;
|
|
2210
|
+
const suggestion = known.find((candidate) => isNearMiss(candidate, field));
|
|
2211
|
+
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."));
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
/** One edit apart, ignoring case: enough for a typo, tight enough to stay quiet. */
|
|
2215
|
+
function isNearMiss(a, b) {
|
|
2216
|
+
const x = a.toLowerCase();
|
|
2217
|
+
const y = b.toLowerCase();
|
|
2218
|
+
if (x === y) return true;
|
|
2219
|
+
if (Math.abs(x.length - y.length) > 1) return false;
|
|
2220
|
+
const [shorter, longer] = x.length <= y.length ? [x, y] : [y, x];
|
|
2221
|
+
let i = 0;
|
|
2222
|
+
let j = 0;
|
|
2223
|
+
let edits = 0;
|
|
2224
|
+
while (i < shorter.length && j < longer.length) {
|
|
2225
|
+
if (shorter[i] === longer[j]) {
|
|
2226
|
+
i++;
|
|
2227
|
+
j++;
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
if (++edits > 1) return false;
|
|
2231
|
+
if (shorter.length === longer.length) i++;
|
|
2232
|
+
j++;
|
|
2233
|
+
}
|
|
2234
|
+
return edits + (longer.length - j) + (shorter.length - i) <= 1;
|
|
2235
|
+
}
|
|
2074
2236
|
function validateApp(name, raw, issues) {
|
|
2075
2237
|
const base = `apps.${name}`;
|
|
2076
2238
|
if (!isRecord(raw)) {
|
|
@@ -2081,6 +2243,13 @@ function validateApp(name, raw, issues) {
|
|
|
2081
2243
|
return;
|
|
2082
2244
|
}
|
|
2083
2245
|
const type = raw.type;
|
|
2246
|
+
if (typeof type === "string" && REMOVED_APP_TYPES[type]) {
|
|
2247
|
+
issues.push({
|
|
2248
|
+
path: `${base}.type`,
|
|
2249
|
+
message: `"${type}" is no longer an app type — ${REMOVED_APP_TYPES[type]}`
|
|
2250
|
+
});
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2084
2253
|
if (typeof type !== "string" || !APP_TYPES.includes(type)) {
|
|
2085
2254
|
issues.push({
|
|
2086
2255
|
path: `${base}.type`,
|
|
@@ -2088,18 +2257,39 @@ function validateApp(name, raw, issues) {
|
|
|
2088
2257
|
});
|
|
2089
2258
|
return;
|
|
2090
2259
|
}
|
|
2260
|
+
warnUnknownFields(name, raw, type);
|
|
2091
2261
|
switch (type) {
|
|
2092
|
-
case "backend":
|
|
2262
|
+
case "backend": {
|
|
2093
2263
|
checkRelativePath(raw.config, `${base}.config`, issues, { required: false });
|
|
2094
2264
|
checkRelativePath(raw.functions, `${base}.functions`, issues, { required: false });
|
|
2095
2265
|
checkRelativePath(raw.crons, `${base}.crons`, issues, { required: false });
|
|
2096
2266
|
checkRelativePath(raw.schema, `${base}.schema`, issues, { required: false });
|
|
2097
2267
|
checkRelativePath(raw.usersCollection, `${base}.usersCollection`, issues, { required: false });
|
|
2098
|
-
if (raw.mode !== void 0
|
|
2268
|
+
if (raw.mode !== void 0) issues.push({
|
|
2099
2269
|
path: `${base}.mode`,
|
|
2100
|
-
message: "
|
|
2270
|
+
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"
|
|
2271
|
+
});
|
|
2272
|
+
const custom = raw.runtime === "custom";
|
|
2273
|
+
if (raw.runtime !== "managed" && !custom) issues.push({
|
|
2274
|
+
path: `${base}.runtime`,
|
|
2275
|
+
message: "is required, and must be \"managed\" or \"custom\""
|
|
2276
|
+
});
|
|
2277
|
+
for (const field of [
|
|
2278
|
+
"dockerfile",
|
|
2279
|
+
"context",
|
|
2280
|
+
"port"
|
|
2281
|
+
]) if (raw[field] !== void 0 && !custom) issues.push({
|
|
2282
|
+
path: `${base}.${field}`,
|
|
2283
|
+
message: "only applies to a custom runtime — set \"runtime\": \"custom\" to build your own image"
|
|
2284
|
+
});
|
|
2285
|
+
checkRelativePath(raw.dockerfile, `${base}.dockerfile`, issues, { required: false });
|
|
2286
|
+
checkRelativePath(raw.context, `${base}.context`, issues, { required: false });
|
|
2287
|
+
if (raw.port !== void 0 && (typeof raw.port !== "number" || !Number.isInteger(raw.port))) issues.push({
|
|
2288
|
+
path: `${base}.port`,
|
|
2289
|
+
message: "must be an integer"
|
|
2101
2290
|
});
|
|
2102
2291
|
return raw;
|
|
2292
|
+
}
|
|
2103
2293
|
case "static":
|
|
2104
2294
|
checkRelativePath(raw.root, `${base}.root`, issues, { required: true });
|
|
2105
2295
|
checkRelativePath(raw.output, `${base}.output`, issues, { required: true });
|
|
@@ -2111,37 +2301,7 @@ function validateApp(name, raw, issues) {
|
|
|
2111
2301
|
path: `${base}.spa`,
|
|
2112
2302
|
message: "must be a boolean"
|
|
2113
2303
|
});
|
|
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
|
-
});
|
|
2304
|
+
checkAppPath(raw.path, `${base}.path`, issues);
|
|
2145
2305
|
return raw;
|
|
2146
2306
|
default: return;
|
|
2147
2307
|
}
|
|
@@ -2155,10 +2315,13 @@ function validateManifest(raw) {
|
|
|
2155
2315
|
path: "",
|
|
2156
2316
|
message: `${MANIFEST_FILENAME} must contain a JSON object`
|
|
2157
2317
|
}] };
|
|
2158
|
-
if (typeof raw.
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2318
|
+
if (typeof raw.rebase !== "string" || raw.rebase.trim() === "") {
|
|
2319
|
+
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"`;
|
|
2320
|
+
issues.push({
|
|
2321
|
+
path: "rebase",
|
|
2322
|
+
message
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2162
2325
|
if (!isRecord(raw.apps)) {
|
|
2163
2326
|
issues.push({
|
|
2164
2327
|
path: "apps",
|
|
@@ -2192,55 +2355,148 @@ function validateManifest(raw) {
|
|
|
2192
2355
|
path: "apps",
|
|
2193
2356
|
message: "a project may declare at most one backend app"
|
|
2194
2357
|
});
|
|
2358
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
2359
|
+
for (const [name, app] of Object.entries(apps)) {
|
|
2360
|
+
if (app.type !== "static") continue;
|
|
2361
|
+
const at = app.path ?? "/";
|
|
2362
|
+
const owner = byPath.get(at);
|
|
2363
|
+
if (owner) {
|
|
2364
|
+
issues.push({
|
|
2365
|
+
path: `apps.${name}.path`,
|
|
2366
|
+
message: `two apps cannot serve the same path — "${owner}" is already at "${at}"`
|
|
2367
|
+
});
|
|
2368
|
+
continue;
|
|
2369
|
+
}
|
|
2370
|
+
byPath.set(at, name);
|
|
2371
|
+
}
|
|
2372
|
+
const storage = validateStorageSources(raw.storage, issues);
|
|
2195
2373
|
if (issues.length > 0) return { issues };
|
|
2196
2374
|
return {
|
|
2197
2375
|
manifest: {
|
|
2198
2376
|
$schema: typeof raw.$schema === "string" ? raw.$schema : void 0,
|
|
2199
|
-
|
|
2200
|
-
apps
|
|
2377
|
+
rebase: raw.rebase,
|
|
2378
|
+
apps,
|
|
2379
|
+
...storage ? { storage } : {}
|
|
2201
2380
|
},
|
|
2202
2381
|
issues
|
|
2203
2382
|
};
|
|
2204
2383
|
}
|
|
2205
2384
|
/**
|
|
2385
|
+
* Validate the `storage` block — which buckets this project uses.
|
|
2386
|
+
*
|
|
2387
|
+
* Absent means one default source, so `undefined` is a valid answer and not an
|
|
2388
|
+
* issue. Everything else is checked strictly, because each key here becomes the
|
|
2389
|
+
* suffix of a set of environment variables: a key that cannot become a variable
|
|
2390
|
+
* name, or two keys that become the *same* one, are failures worth catching
|
|
2391
|
+
* while someone is looking at the file rather than at a tenant serving one
|
|
2392
|
+
* bucket's files with another's credentials.
|
|
2393
|
+
*/
|
|
2394
|
+
function validateStorageSources(raw, issues) {
|
|
2395
|
+
if (raw === void 0) return void 0;
|
|
2396
|
+
if (!isRecord(raw)) {
|
|
2397
|
+
issues.push({
|
|
2398
|
+
path: "storage",
|
|
2399
|
+
message: "must be an object keyed by storage source name"
|
|
2400
|
+
});
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
const sources = {};
|
|
2404
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2405
|
+
if (!isRecord(value)) {
|
|
2406
|
+
issues.push({
|
|
2407
|
+
path: `storage.${key}`,
|
|
2408
|
+
message: "must be an object"
|
|
2409
|
+
});
|
|
2410
|
+
continue;
|
|
2411
|
+
}
|
|
2412
|
+
if (typeof value.engine !== "string" || value.engine.trim() === "") {
|
|
2413
|
+
issues.push({
|
|
2414
|
+
path: `storage.${key}.engine`,
|
|
2415
|
+
message: "is required, e.g. \"s3\", \"gcs\" or \"local\""
|
|
2416
|
+
});
|
|
2417
|
+
continue;
|
|
2418
|
+
}
|
|
2419
|
+
if (value.transport !== void 0 && value.transport !== "server" && value.transport !== "direct") {
|
|
2420
|
+
issues.push({
|
|
2421
|
+
path: `storage.${key}.transport`,
|
|
2422
|
+
message: "must be \"server\" or \"direct\""
|
|
2423
|
+
});
|
|
2424
|
+
continue;
|
|
2425
|
+
}
|
|
2426
|
+
if (value.label !== void 0 && typeof value.label !== "string") {
|
|
2427
|
+
issues.push({
|
|
2428
|
+
path: `storage.${key}.label`,
|
|
2429
|
+
message: "must be a string"
|
|
2430
|
+
});
|
|
2431
|
+
continue;
|
|
2432
|
+
}
|
|
2433
|
+
try {
|
|
2434
|
+
storageEnvSuffix(key);
|
|
2435
|
+
} catch {
|
|
2436
|
+
issues.push({
|
|
2437
|
+
path: `storage.${key}`,
|
|
2438
|
+
message: "name cannot become an environment variable suffix — use a name containing at least one letter or digit"
|
|
2439
|
+
});
|
|
2440
|
+
continue;
|
|
2441
|
+
}
|
|
2442
|
+
sources[key] = {
|
|
2443
|
+
engine: value.engine,
|
|
2444
|
+
...value.transport !== void 0 ? { transport: value.transport } : {},
|
|
2445
|
+
...value.label !== void 0 ? { label: value.label } : {}
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
const collision = findStorageSuffixCollision(Object.keys(sources));
|
|
2449
|
+
if (collision) issues.push({
|
|
2450
|
+
path: `storage.${collision.b}`,
|
|
2451
|
+
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`
|
|
2452
|
+
});
|
|
2453
|
+
return Object.keys(sources).length > 0 ? sources : void 0;
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2206
2456
|
* Infer a manifest from a directory that does not have one.
|
|
2207
2457
|
*
|
|
2208
2458
|
* This mirrors exactly what the template scaffolds, which is what makes adopting
|
|
2209
2459
|
* the manifest a no-op for existing projects: the synthesized result is what
|
|
2210
2460
|
* they would have written by hand.
|
|
2211
2461
|
*
|
|
2212
|
-
*
|
|
2213
|
-
*
|
|
2214
|
-
*
|
|
2215
|
-
*
|
|
2462
|
+
* The backend's `runtime` is inferred from whether the repository declares a
|
|
2463
|
+
* **Dockerfile** — the thing that actually builds an image — and from nothing
|
|
2464
|
+
* else. It used to be inferred from the presence of `backend/src/index.ts`,
|
|
2465
|
+
* which every scaffolded project had whether or not it wanted its own server, so
|
|
2466
|
+
* projects predating the manifest silently landed on the custom runtime and paid
|
|
2467
|
+
* for it (see `docs/cloud-deploy-workspace-vendoring.md`).
|
|
2216
2468
|
*/
|
|
2217
2469
|
function synthesizeManifest(projectRoot) {
|
|
2218
2470
|
const exists = (relative) => fs.existsSync(path.join(projectRoot, relative));
|
|
2219
2471
|
const apps = {};
|
|
2220
2472
|
const hasConfig = exists(DEFAULT_CONFIG_DIR);
|
|
2221
2473
|
const hasBackend = exists("backend");
|
|
2222
|
-
const
|
|
2223
|
-
if (hasBackend
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2474
|
+
const dockerfile = ["Dockerfile", "backend/Dockerfile"].find(exists);
|
|
2475
|
+
if (hasBackend || hasConfig) {
|
|
2476
|
+
const backend = dockerfile ? {
|
|
2477
|
+
type: "backend",
|
|
2478
|
+
runtime: "custom",
|
|
2479
|
+
dockerfile,
|
|
2480
|
+
context: "."
|
|
2481
|
+
} : {
|
|
2482
|
+
type: "backend",
|
|
2483
|
+
runtime: "managed"
|
|
2484
|
+
};
|
|
2231
2485
|
if (exists("backend/functions")) backend.functions = DEFAULT_FUNCTIONS_DIR;
|
|
2232
2486
|
if (exists("backend/crons")) backend.crons = DEFAULT_CRONS_DIR;
|
|
2233
2487
|
apps.backend = backend;
|
|
2488
|
+
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
2489
|
}
|
|
2235
2490
|
if (exists("frontend")) apps.web = {
|
|
2236
2491
|
type: "static",
|
|
2237
2492
|
root: "frontend",
|
|
2238
2493
|
build: "npm run build --workspace frontend",
|
|
2239
2494
|
output: "frontend/dist",
|
|
2495
|
+
path: "/",
|
|
2240
2496
|
spa: true
|
|
2241
2497
|
};
|
|
2242
2498
|
return {
|
|
2243
|
-
|
|
2499
|
+
rebase: "^1",
|
|
2244
2500
|
apps
|
|
2245
2501
|
};
|
|
2246
2502
|
}
|
|
@@ -2282,7 +2538,7 @@ function writeManifest(projectRoot, manifest) {
|
|
|
2282
2538
|
const filePath = manifestPath(projectRoot);
|
|
2283
2539
|
const ordered = {
|
|
2284
2540
|
$schema: manifest.$schema ?? "https://rebase.pro/schemas/rebase.json",
|
|
2285
|
-
|
|
2541
|
+
rebase: manifest.rebase,
|
|
2286
2542
|
apps: manifest.apps
|
|
2287
2543
|
};
|
|
2288
2544
|
fs.writeFileSync(filePath, `${JSON.stringify(ordered, null, 4)}\n`, "utf8");
|
|
@@ -2301,13 +2557,8 @@ function buildableApps(manifest) {
|
|
|
2301
2557
|
name,
|
|
2302
2558
|
app
|
|
2303
2559
|
}));
|
|
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));
|
|
2560
|
+
const rank = (app) => app.type === "backend" ? 0 : 1;
|
|
2561
|
+
return entries.sort((a, b) => rank(a.app) - rank(b.app));
|
|
2311
2562
|
}
|
|
2312
2563
|
/**
|
|
2313
2564
|
* Decide whether a project can run on the managed runtime, and say why not.
|
|
@@ -2317,28 +2568,45 @@ function buildableApps(manifest) {
|
|
|
2317
2568
|
* verdict.
|
|
2318
2569
|
*/
|
|
2319
2570
|
function assessManagedCompatibility(manifest) {
|
|
2320
|
-
const reasons = [];
|
|
2321
2571
|
const backend = findBackendApp(manifest);
|
|
2322
|
-
if (!backend) {
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2572
|
+
if (!backend) return {
|
|
2573
|
+
eligible: false,
|
|
2574
|
+
reasons: ["No backend app is declared in this repository. Only the repository that declares the backend selects the runtime."]
|
|
2575
|
+
};
|
|
2576
|
+
if (backend.app.runtime === "custom") return {
|
|
2577
|
+
eligible: false,
|
|
2578
|
+
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.`]
|
|
2579
|
+
};
|
|
2328
2580
|
return {
|
|
2329
|
-
eligible:
|
|
2330
|
-
reasons
|
|
2581
|
+
eligible: true,
|
|
2582
|
+
reasons: []
|
|
2331
2583
|
};
|
|
2332
2584
|
}
|
|
2333
|
-
/**
|
|
2334
|
-
|
|
2585
|
+
/**
|
|
2586
|
+
* Resolve a backend app's directories against the conventions it omits.
|
|
2587
|
+
*
|
|
2588
|
+
* `hasCollections` replaces the old `mode: "cms" | "baas"` field. Where the
|
|
2589
|
+
* collections come from was never an independent choice: either they are
|
|
2590
|
+
* declared in code and the bundle ships them, or they are not and the runtime
|
|
2591
|
+
* introspects the live database at boot. Declaring it separately only created
|
|
2592
|
+
* the contradictory state — code-first declared, no collections anywhere.
|
|
2593
|
+
*
|
|
2594
|
+
* `hasConfig` is deliberately a **separate** question. A headless project has no
|
|
2595
|
+
* `config/collections`, but it may still ship a config package — that is where
|
|
2596
|
+
* the `storageAuthorize` hook lives, and storage is not under row-level
|
|
2597
|
+
* security, so without one the server refuses to boot with storage enabled.
|
|
2598
|
+
* Collapsing the two would leave a headless project nowhere to put it.
|
|
2599
|
+
*/
|
|
2600
|
+
function resolveBackendPaths(app, projectRoot) {
|
|
2601
|
+
const config = app.config ?? "config";
|
|
2335
2602
|
return {
|
|
2336
|
-
config
|
|
2603
|
+
config,
|
|
2337
2604
|
functions: app.functions ?? "backend/functions",
|
|
2338
2605
|
crons: app.crons ?? "backend/crons",
|
|
2339
2606
|
schema: app.schema ?? "backend/src/schema.generated.ts",
|
|
2340
2607
|
usersCollection: app.usersCollection ?? "collections/users",
|
|
2341
|
-
|
|
2608
|
+
hasConfig: fs.existsSync(path.join(projectRoot, config)),
|
|
2609
|
+
hasCollections: fs.existsSync(path.join(projectRoot, config, "collections"))
|
|
2342
2610
|
};
|
|
2343
2611
|
}
|
|
2344
2612
|
//#endregion
|
|
@@ -2401,22 +2669,19 @@ function devRuntimeEnv(projectRoot) {
|
|
|
2401
2669
|
REBASE_DEV_CONFIG: "config",
|
|
2402
2670
|
REBASE_DEV_FUNCTIONS: "backend/functions",
|
|
2403
2671
|
REBASE_DEV_CRONS: "backend/crons",
|
|
2404
|
-
REBASE_DEV_SCHEMA: "backend/src/schema.generated.ts"
|
|
2405
|
-
REBASE_DEV_MODE: "cms"
|
|
2672
|
+
REBASE_DEV_SCHEMA: "backend/src/schema.generated.ts"
|
|
2406
2673
|
};
|
|
2407
2674
|
try {
|
|
2408
2675
|
const backend = findBackendApp(loadManifest(projectRoot).manifest);
|
|
2409
2676
|
if (backend) {
|
|
2410
|
-
const paths = resolveBackendPaths(backend.app);
|
|
2677
|
+
const paths = resolveBackendPaths(backend.app, projectRoot);
|
|
2411
2678
|
result.REBASE_DEV_CONFIG = paths.config;
|
|
2412
2679
|
result.REBASE_DEV_FUNCTIONS = paths.functions;
|
|
2413
2680
|
result.REBASE_DEV_CRONS = paths.crons;
|
|
2414
2681
|
result.REBASE_DEV_SCHEMA = paths.schema;
|
|
2415
|
-
result.REBASE_DEV_MODE = paths.mode;
|
|
2416
2682
|
result.REBASE_DEV_APP = backend.name;
|
|
2417
2683
|
}
|
|
2418
2684
|
} catch {}
|
|
2419
|
-
if (!fs.existsSync(path.join(projectRoot, result.REBASE_DEV_CONFIG))) result.REBASE_DEV_MODE = "baas";
|
|
2420
2685
|
return result;
|
|
2421
2686
|
}
|
|
2422
2687
|
/** Well-known filename the backend writes its actual port to. */
|
|
@@ -2825,7 +3090,7 @@ ${chalk.green.bold("Description")}
|
|
|
2825
3090
|
*/
|
|
2826
3091
|
var DEFAULT_BUNDLE_DIR = "dist-bundle";
|
|
2827
3092
|
/** Packages whose presence means the bundle cannot run on a stock runtime image. */
|
|
2828
|
-
var KNOWN_NATIVE_PACKAGES = new Set([
|
|
3093
|
+
var KNOWN_NATIVE_PACKAGES = /* @__PURE__ */ new Set([
|
|
2829
3094
|
"sharp",
|
|
2830
3095
|
"canvas",
|
|
2831
3096
|
"bcrypt",
|
|
@@ -2841,7 +3106,7 @@ var KNOWN_NATIVE_PACKAGES = new Set([
|
|
|
2841
3106
|
"pg-native"
|
|
2842
3107
|
]);
|
|
2843
3108
|
/** Dependencies supplied by the runtime image itself, not by the bundle. */
|
|
2844
|
-
var RUNTIME_PROVIDED = new Set([
|
|
3109
|
+
var RUNTIME_PROVIDED = /* @__PURE__ */ new Set([
|
|
2845
3110
|
"@rebasepro/server",
|
|
2846
3111
|
"@rebasepro/types",
|
|
2847
3112
|
"@rebasepro/client",
|
|
@@ -3024,16 +3289,27 @@ async function writeBundleTsconfig(projectRoot, outDir, includes, skipTypeCheck)
|
|
|
3024
3289
|
* says exactly how to proceed, while a false positive would hand back the crash
|
|
3025
3290
|
* loop this exists to prevent.
|
|
3026
3291
|
*/
|
|
3027
|
-
function detectStorageAuthorize(compiledConfigDir) {
|
|
3292
|
+
function detectStorageAuthorize(compiledConfigDir, depth = 0) {
|
|
3028
3293
|
const indexPath = [
|
|
3029
3294
|
".js",
|
|
3030
3295
|
".mjs",
|
|
3031
3296
|
".ts"
|
|
3032
3297
|
].map((ext) => path.join(compiledConfigDir, `index${ext}`)).find((candidate) => fs.existsSync(candidate));
|
|
3033
3298
|
if (!indexPath) return false;
|
|
3299
|
+
return moduleExportsStorageAuthorize(indexPath, depth);
|
|
3300
|
+
}
|
|
3301
|
+
/**
|
|
3302
|
+
* Whether one compiled module re-exports or defines `storageAuthorize`.
|
|
3303
|
+
*
|
|
3304
|
+
* Split out from {@link detectStorageAuthorize} so a wildcard re-export can be
|
|
3305
|
+
* followed. `export * from "./storage.js"` is an ordinary way to write a config
|
|
3306
|
+
* barrel, and treating it as "no hook" rejected deploys that were correct — with
|
|
3307
|
+
* a message telling the developer to add a hook they had already written.
|
|
3308
|
+
*/
|
|
3309
|
+
function moduleExportsStorageAuthorize(modulePath, depth) {
|
|
3034
3310
|
let source;
|
|
3035
3311
|
try {
|
|
3036
|
-
source = fs.readFileSync(
|
|
3312
|
+
source = fs.readFileSync(modulePath, "utf8");
|
|
3037
3313
|
} catch {
|
|
3038
3314
|
return false;
|
|
3039
3315
|
}
|
|
@@ -3042,9 +3318,48 @@ function detectStorageAuthorize(compiledConfigDir) {
|
|
|
3042
3318
|
const parts = entry.split(/\bas\b/);
|
|
3043
3319
|
return parts[parts.length - 1].trim();
|
|
3044
3320
|
}).includes("storageAuthorize")) return true;
|
|
3321
|
+
if (depth < 3) for (const clause of source.matchAll(/\bexport\s*\*\s*from\s*["']([^"']+)["']/g)) {
|
|
3322
|
+
const specifier = clause[1];
|
|
3323
|
+
if (!specifier.startsWith(".")) continue;
|
|
3324
|
+
const resolved = resolveRelativeModule(path.dirname(modulePath), specifier);
|
|
3325
|
+
if (resolved && moduleExportsStorageAuthorize(resolved, depth + 1)) return true;
|
|
3326
|
+
}
|
|
3045
3327
|
return false;
|
|
3046
3328
|
}
|
|
3047
3329
|
/**
|
|
3330
|
+
* Resolve a relative ESM specifier to a file on disk.
|
|
3331
|
+
*
|
|
3332
|
+
* Compiled output carries explicit `.js` extensions, but the same function reads
|
|
3333
|
+
* TypeScript sources during a source boot, where the specifier may be
|
|
3334
|
+
* extensionless or point at a directory index.
|
|
3335
|
+
*/
|
|
3336
|
+
function resolveRelativeModule(fromDir, specifier) {
|
|
3337
|
+
const base = path.resolve(fromDir, specifier);
|
|
3338
|
+
const candidates = [
|
|
3339
|
+
base,
|
|
3340
|
+
`${base}.js`,
|
|
3341
|
+
`${base}.mjs`,
|
|
3342
|
+
`${base}.ts`,
|
|
3343
|
+
path.join(base, "index.js"),
|
|
3344
|
+
path.join(base, "index.mjs"),
|
|
3345
|
+
path.join(base, "index.ts")
|
|
3346
|
+
];
|
|
3347
|
+
for (const candidate of candidates) try {
|
|
3348
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
3349
|
+
} catch {
|
|
3350
|
+
continue;
|
|
3351
|
+
}
|
|
3352
|
+
if (/\.js$/.test(base)) {
|
|
3353
|
+
const asTs = base.replace(/\.js$/, ".ts");
|
|
3354
|
+
try {
|
|
3355
|
+
if (fs.existsSync(asTs) && fs.statSync(asTs).isFile()) return asTs;
|
|
3356
|
+
} catch {
|
|
3357
|
+
return null;
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
return null;
|
|
3361
|
+
}
|
|
3362
|
+
/**
|
|
3048
3363
|
* Detect native code in the dependency closure.
|
|
3049
3364
|
*
|
|
3050
3365
|
* Walks declared runtime dependencies breadth-first through `node_modules`,
|
|
@@ -3200,6 +3515,139 @@ function collectDeclaredDependencies(projectRoot) {
|
|
|
3200
3515
|
return declared;
|
|
3201
3516
|
}
|
|
3202
3517
|
/**
|
|
3518
|
+
* Lowest version a range could resolve to, or null if it is not a range.
|
|
3519
|
+
*
|
|
3520
|
+
* Kept deliberately tiny and local. The published range grammar here is a caret,
|
|
3521
|
+
* a tilde, an exact version or a `>=` floor, and the alternative — a semver
|
|
3522
|
+
* dependency in the CLI — buys breadth this does not need.
|
|
3523
|
+
*/
|
|
3524
|
+
function lowerBoundOf(range) {
|
|
3525
|
+
const raw = range.trim().replace(/^[\^~]/, "").replace(/^>=\s*/, "").replace(/^v/, "");
|
|
3526
|
+
if (!/^\d+(\.\d+){0,2}$/.test(raw)) return null;
|
|
3527
|
+
const [major, minor = 0, patch = 0] = raw.split(".").map(Number);
|
|
3528
|
+
return [
|
|
3529
|
+
major,
|
|
3530
|
+
minor,
|
|
3531
|
+
patch
|
|
3532
|
+
];
|
|
3533
|
+
}
|
|
3534
|
+
/** Highest version a range could resolve to (exclusive), or null. */
|
|
3535
|
+
function upperBoundOf(range) {
|
|
3536
|
+
const trimmed = range.trim();
|
|
3537
|
+
const min = lowerBoundOf(trimmed);
|
|
3538
|
+
if (!min) return null;
|
|
3539
|
+
if (trimmed.startsWith(">=")) return null;
|
|
3540
|
+
const [major, minor, patch] = min;
|
|
3541
|
+
if (trimmed.startsWith("^")) {
|
|
3542
|
+
if (major > 0) return [
|
|
3543
|
+
major + 1,
|
|
3544
|
+
0,
|
|
3545
|
+
0
|
|
3546
|
+
];
|
|
3547
|
+
if (minor > 0) return [
|
|
3548
|
+
0,
|
|
3549
|
+
minor + 1,
|
|
3550
|
+
0
|
|
3551
|
+
];
|
|
3552
|
+
return [
|
|
3553
|
+
0,
|
|
3554
|
+
0,
|
|
3555
|
+
patch + 1
|
|
3556
|
+
];
|
|
3557
|
+
}
|
|
3558
|
+
if (trimmed.startsWith("~")) return trimmed.replace(/^~v?/, "").split(".").length >= 2 ? [
|
|
3559
|
+
major,
|
|
3560
|
+
minor + 1,
|
|
3561
|
+
0
|
|
3562
|
+
] : [
|
|
3563
|
+
major + 1,
|
|
3564
|
+
0,
|
|
3565
|
+
0
|
|
3566
|
+
];
|
|
3567
|
+
const parts = trimmed.replace(/^v/, "").split(".").length;
|
|
3568
|
+
if (parts === 1) return [
|
|
3569
|
+
major + 1,
|
|
3570
|
+
0,
|
|
3571
|
+
0
|
|
3572
|
+
];
|
|
3573
|
+
if (parts === 2) return [
|
|
3574
|
+
major,
|
|
3575
|
+
minor + 1,
|
|
3576
|
+
0
|
|
3577
|
+
];
|
|
3578
|
+
return [
|
|
3579
|
+
major,
|
|
3580
|
+
minor,
|
|
3581
|
+
patch + 1
|
|
3582
|
+
];
|
|
3583
|
+
}
|
|
3584
|
+
function compareTriples(a, b) {
|
|
3585
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
|
|
3586
|
+
return 0;
|
|
3587
|
+
}
|
|
3588
|
+
/**
|
|
3589
|
+
* Whether a declared range could EVER resolve at or above `target`.
|
|
3590
|
+
*
|
|
3591
|
+
* The same question the control plane asks at intake, asked here first. Only a
|
|
3592
|
+
* range whose entire span sits below the target is reported — `^0.10.0` can
|
|
3593
|
+
* never cross to 0.12 whatever npm publishes — because a false alarm on a build
|
|
3594
|
+
* that would have worked trains people to ignore the warning that matters.
|
|
3595
|
+
*/
|
|
3596
|
+
function canReach(range, target) {
|
|
3597
|
+
const ceiling = upperBoundOf(range);
|
|
3598
|
+
const floor = lowerBoundOf(target);
|
|
3599
|
+
if (!floor || !lowerBoundOf(range)) return null;
|
|
3600
|
+
if (!ceiling) return true;
|
|
3601
|
+
return compareTriples(ceiling, floor) > 0;
|
|
3602
|
+
}
|
|
3603
|
+
/**
|
|
3604
|
+
* Find `@rebasepro/*` dependencies pinned to a version older than this CLI.
|
|
3605
|
+
*
|
|
3606
|
+
* This is the only place a developer can be told. In development, every
|
|
3607
|
+
* `@rebasepro/*` resolves through pnpm's `link:`/`workspace:` overrides to the
|
|
3608
|
+
* checkout, so the version STRINGS in package.json are never exercised — the
|
|
3609
|
+
* project runs fine locally on whatever is on disk, and the declared numbers are
|
|
3610
|
+
* first honoured when the runtime npm-installs them from a bundle in the cloud.
|
|
3611
|
+
* A project scaffolded at 0.10.0 therefore keeps working on a developer's
|
|
3612
|
+
* machine indefinitely while being, in the cloud, a 0.10.0 driver.
|
|
3613
|
+
*
|
|
3614
|
+
* That matters because the image supplies only `@rebasepro/server`; the database
|
|
3615
|
+
* driver comes from these declarations and a newer runtime never updates it.
|
|
3616
|
+
* Every package.json is scanned, `dependencies` and `devDependencies` both,
|
|
3617
|
+
* because they have to be bumped together and the one that gets forgotten is the
|
|
3618
|
+
* one nobody looks at.
|
|
3619
|
+
*/
|
|
3620
|
+
function detectFrameworkDepDrift(projectRoot, cliVersion) {
|
|
3621
|
+
const found = [];
|
|
3622
|
+
for (const relative of [
|
|
3623
|
+
"package.json",
|
|
3624
|
+
"backend/package.json",
|
|
3625
|
+
"config/package.json",
|
|
3626
|
+
"frontend/package.json"
|
|
3627
|
+
]) {
|
|
3628
|
+
const file = path.join(projectRoot, relative);
|
|
3629
|
+
if (!fs.existsSync(file)) continue;
|
|
3630
|
+
try {
|
|
3631
|
+
const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
3632
|
+
for (const block of [pkg.dependencies, pkg.devDependencies]) for (const [name, range] of Object.entries(block ?? {})) {
|
|
3633
|
+
if (!name.startsWith("@rebasepro/")) continue;
|
|
3634
|
+
if (typeof range !== "string") continue;
|
|
3635
|
+
found.push({
|
|
3636
|
+
name,
|
|
3637
|
+
range,
|
|
3638
|
+
file: relative
|
|
3639
|
+
});
|
|
3640
|
+
}
|
|
3641
|
+
} catch {}
|
|
3642
|
+
}
|
|
3643
|
+
const behind = found.filter((d) => canReach(d.range, cliVersion) === false);
|
|
3644
|
+
const bounds = new Set(found.map((d) => lowerBoundOf(d.range)).filter((b) => b != null).map((b) => b.join(".")));
|
|
3645
|
+
return {
|
|
3646
|
+
behind,
|
|
3647
|
+
disagreeing: bounds.size > 1 ? [...bounds].sort() : []
|
|
3648
|
+
};
|
|
3649
|
+
}
|
|
3650
|
+
/**
|
|
3203
3651
|
* Rewrite relative import specifiers in emitted JavaScript so Node can resolve them.
|
|
3204
3652
|
*
|
|
3205
3653
|
* TypeScript deliberately does not touch specifiers: `moduleResolution: "bundler"`
|
|
@@ -3328,11 +3776,10 @@ async function regenerateSchema(projectRoot, configDir, options) {
|
|
|
3328
3776
|
* deployed green, and answered 404 on every one of them, with the file still
|
|
3329
3777
|
* sitting in the repository looking exactly like the server.
|
|
3330
3778
|
*
|
|
3331
|
-
* A project that means to keep its own entrypoint
|
|
3332
|
-
*
|
|
3333
|
-
*
|
|
3334
|
-
*
|
|
3335
|
-
* a mistake.
|
|
3779
|
+
* A project that means to keep its own entrypoint runs `rebase eject`, which
|
|
3780
|
+
* writes the entrypoint, a Dockerfile and a compose file together and flips the
|
|
3781
|
+
* backend to `runtime: "custom"`. The warning names that route rather than
|
|
3782
|
+
* implying the file is a mistake.
|
|
3336
3783
|
*/
|
|
3337
3784
|
function findUnusedServerEntry(projectRoot, functionsDir) {
|
|
3338
3785
|
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 +3790,22 @@ function findUnusedServerEntry(projectRoot, functionsDir) {
|
|
|
3343
3790
|
*/
|
|
3344
3791
|
async function buildBundle(options) {
|
|
3345
3792
|
const { projectRoot, app, appName } = options;
|
|
3346
|
-
const paths = resolveBackendPaths(app);
|
|
3793
|
+
const paths = resolveBackendPaths(app, projectRoot);
|
|
3347
3794
|
const outDir = path.resolve(projectRoot, options.outDir ?? "dist-bundle");
|
|
3348
3795
|
const includes = [];
|
|
3349
3796
|
const addIfExists = (relative, pattern) => {
|
|
3350
3797
|
if (fs.existsSync(path.join(projectRoot, relative))) includes.push(pattern);
|
|
3351
3798
|
};
|
|
3352
|
-
if (paths.
|
|
3799
|
+
if (paths.hasConfig) addIfExists(paths.config, `${paths.config}/**/*.ts`);
|
|
3353
3800
|
addIfExists(paths.functions, `${paths.functions}/**/*.ts`);
|
|
3354
3801
|
addIfExists(paths.crons, `${paths.crons}/**/*.ts`);
|
|
3355
3802
|
if (fs.existsSync(path.join(projectRoot, paths.schema))) includes.push(paths.schema);
|
|
3356
3803
|
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.
|
|
3804
|
+
if (paths.hasCollections && options.skipSchema !== true) await regenerateSchema(projectRoot, paths.config, options);
|
|
3358
3805
|
const unusedEntry = findUnusedServerEntry(projectRoot, paths.functions);
|
|
3359
3806
|
if (unusedEntry) {
|
|
3360
3807
|
const parts = [
|
|
3361
|
-
...paths.
|
|
3808
|
+
...paths.hasCollections ? [`${paths.config}/`] : [],
|
|
3362
3809
|
`${paths.functions}/`,
|
|
3363
3810
|
"the schema"
|
|
3364
3811
|
];
|
|
@@ -3366,7 +3813,7 @@ async function buildBundle(options) {
|
|
|
3366
3813
|
console.log(chalk.yellow(` ⚠ ${unusedEntry} is not the bundle's entry point — it is not compiled or shipped.`));
|
|
3367
3814
|
console.log(chalk.dim(` The runtime boots the bundle itself and mounts ${compiled}.`));
|
|
3368
3815
|
console.log(chalk.dim(` Routes defined there will not exist once deployed: move them to ${paths.functions}/,`));
|
|
3369
|
-
console.log(chalk.dim(` or
|
|
3816
|
+
console.log(chalk.dim(` or run \`rebase eject\` to make this file the entrypoint and own the image.`));
|
|
3370
3817
|
}
|
|
3371
3818
|
log(options, chalk.dim(` compiling ${includes.length} source group(s) → ${path.relative(projectRoot, outDir)}/`));
|
|
3372
3819
|
cleanOutDir(projectRoot, outDir);
|
|
@@ -3391,14 +3838,17 @@ async function buildBundle(options) {
|
|
|
3391
3838
|
const compiledConfigDir = path.join(outDir, paths.config);
|
|
3392
3839
|
const compiledCollectionsDir = path.join(compiledConfigDir, "collections");
|
|
3393
3840
|
let collections = [];
|
|
3394
|
-
if (paths.
|
|
3841
|
+
if (paths.hasCollections) {
|
|
3395
3842
|
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")}.
|
|
3843
|
+
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
3844
|
if (!fs.existsSync(compiledCollectionsDir)) throw new Error(`Compilation produced no collections directory at ${path.relative(projectRoot, compiledCollectionsDir)}.`);
|
|
3398
3845
|
}
|
|
3399
3846
|
const declared = collectDeclaredDependencies(projectRoot);
|
|
3400
3847
|
const nativeModules = detectNativeDependencies(projectRoot, declared);
|
|
3401
3848
|
const declaresStorageAuthorize = detectStorageAuthorize(path.join(outDir, paths.config));
|
|
3849
|
+
const storageSources = normalizeStorageSources(options.storage, void 0);
|
|
3850
|
+
const collision = findStorageSuffixCollision(storageSources.map((s) => s.key));
|
|
3851
|
+
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
3852
|
const schemaOut = paths.schema.replace(/\.ts$/, ".js");
|
|
3403
3853
|
const relative = (target) => fs.existsSync(path.join(outDir, target)) ? target : void 0;
|
|
3404
3854
|
const manifest = {
|
|
@@ -3408,23 +3858,26 @@ async function buildBundle(options) {
|
|
|
3408
3858
|
builtAgainst: resolveServerVersion(projectRoot),
|
|
3409
3859
|
contract: RUNTIME_CONTRACT_VERSION
|
|
3410
3860
|
},
|
|
3411
|
-
schemaVersion: paths.
|
|
3861
|
+
schemaVersion: paths.hasCollections ? computeSchemaVersion(collections) : "",
|
|
3412
3862
|
app: appName,
|
|
3413
|
-
|
|
3863
|
+
kind: "backend",
|
|
3414
3864
|
entry: {
|
|
3415
|
-
config: paths.
|
|
3416
|
-
collections: paths.
|
|
3865
|
+
config: paths.hasConfig ? relative(paths.config) : void 0,
|
|
3866
|
+
collections: paths.hasCollections ? relative(path.join(paths.config, "collections")) : void 0,
|
|
3417
3867
|
functions: relative(paths.functions),
|
|
3418
3868
|
crons: relative(paths.crons),
|
|
3419
3869
|
schema: relative(schemaOut),
|
|
3420
|
-
usersCollection: paths.
|
|
3870
|
+
usersCollection: paths.hasCollections ? relative(path.join(paths.config, `${paths.usersCollection}.js`)) : void 0
|
|
3421
3871
|
},
|
|
3422
3872
|
collections: collections.map((collection) => collection.slug).filter((slug) => Boolean(slug)).sort(),
|
|
3423
3873
|
hooks: {
|
|
3424
3874
|
native: nativeModules.length > 0,
|
|
3425
3875
|
nativeModules: nativeModules.length > 0 ? nativeModules : void 0
|
|
3426
3876
|
},
|
|
3427
|
-
storage: {
|
|
3877
|
+
storage: {
|
|
3878
|
+
authorize: declaresStorageAuthorize,
|
|
3879
|
+
...storageSources.length > 0 ? { sources: storageSources } : {}
|
|
3880
|
+
},
|
|
3428
3881
|
deps: { declared },
|
|
3429
3882
|
build: {
|
|
3430
3883
|
cli: resolveCliVersion(),
|
|
@@ -3487,11 +3940,12 @@ async function buildBundle(options) {
|
|
|
3487
3940
|
* in one image already, that is exactly what it had.
|
|
3488
3941
|
*/
|
|
3489
3942
|
function foldStaticIntoBundle(options) {
|
|
3490
|
-
const { bundleDir, assetsDir } = options;
|
|
3943
|
+
const { bundleDir, assetsDir, appName, path: basePath, spa } = options;
|
|
3491
3944
|
const manifestPath = path.join(bundleDir, "manifest.json");
|
|
3492
3945
|
if (!fs.existsSync(manifestPath)) throw new Error(`No manifest at ${manifestPath} — build the backend bundle first.`);
|
|
3493
3946
|
if (!fs.existsSync(assetsDir)) throw new Error(`No built assets at ${assetsDir}.`);
|
|
3494
|
-
const
|
|
3947
|
+
const dir = path.posix.join("static", appName);
|
|
3948
|
+
const staticOut = path.join(bundleDir, "static", appName);
|
|
3495
3949
|
fs.rmSync(staticOut, {
|
|
3496
3950
|
recursive: true,
|
|
3497
3951
|
force: true
|
|
@@ -3499,21 +3953,30 @@ function foldStaticIntoBundle(options) {
|
|
|
3499
3953
|
fs.mkdirSync(staticOut, { recursive: true });
|
|
3500
3954
|
fs.cpSync(assetsDir, staticOut, { recursive: true });
|
|
3501
3955
|
let fileCount = 0;
|
|
3502
|
-
const count = (
|
|
3503
|
-
for (const entry of fs.readdirSync(
|
|
3956
|
+
const count = (target) => {
|
|
3957
|
+
for (const entry of fs.readdirSync(target, { withFileTypes: true })) if (entry.isDirectory()) count(path.join(target, entry.name));
|
|
3504
3958
|
else fileCount++;
|
|
3505
3959
|
};
|
|
3506
3960
|
count(staticOut);
|
|
3507
3961
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
3962
|
+
const existing = (manifest.entry?.static ?? []).filter((entry) => entry.dir !== dir);
|
|
3508
3963
|
manifest.entry = {
|
|
3509
3964
|
...manifest.entry,
|
|
3510
|
-
static:
|
|
3965
|
+
static: [...existing, {
|
|
3966
|
+
path: basePath,
|
|
3967
|
+
dir,
|
|
3968
|
+
spa
|
|
3969
|
+
}]
|
|
3511
3970
|
};
|
|
3512
3971
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
3513
|
-
return {
|
|
3972
|
+
return {
|
|
3973
|
+
fileCount,
|
|
3974
|
+
dir
|
|
3975
|
+
};
|
|
3514
3976
|
}
|
|
3515
3977
|
function buildStaticBundle(options) {
|
|
3516
3978
|
const { projectRoot, appName, assetsDir, outDir, runtimeRange } = options;
|
|
3979
|
+
const basePath = options.path ?? "/";
|
|
3517
3980
|
cleanOutDir(projectRoot, outDir);
|
|
3518
3981
|
const staticOut = path.join(outDir, "static");
|
|
3519
3982
|
fs.mkdirSync(staticOut, { recursive: true });
|
|
@@ -3533,8 +3996,12 @@ function buildStaticBundle(options) {
|
|
|
3533
3996
|
},
|
|
3534
3997
|
schemaVersion: "",
|
|
3535
3998
|
app: appName,
|
|
3536
|
-
|
|
3537
|
-
entry: { static:
|
|
3999
|
+
kind: "static",
|
|
4000
|
+
entry: { static: [{
|
|
4001
|
+
path: basePath,
|
|
4002
|
+
dir: "static",
|
|
4003
|
+
spa: options.spa ?? true
|
|
4004
|
+
}] },
|
|
3538
4005
|
hooks: { native: false },
|
|
3539
4006
|
deps: { declared: {} },
|
|
3540
4007
|
build: {
|
|
@@ -3636,7 +4103,7 @@ function resolveCliVersion() {
|
|
|
3636
4103
|
//#endregion
|
|
3637
4104
|
//#region src/fold-static.ts
|
|
3638
4105
|
/**
|
|
3639
|
-
* Folding a project's
|
|
4106
|
+
* Folding a project's static apps into its backend bundle.
|
|
3640
4107
|
*
|
|
3641
4108
|
* Shared by `rebase build` and `rebase cloud deploy` deliberately. It lived in
|
|
3642
4109
|
* the build *command* first, and `deploy` rebuilds the bundle itself — so a
|
|
@@ -3645,34 +4112,77 @@ function resolveCliVersion() {
|
|
|
3645
4112
|
* folding had never been written. Two callers building the same artefact must
|
|
3646
4113
|
* share the step that completes it.
|
|
3647
4114
|
*
|
|
3648
|
-
* Why fold at all: `bootFromBundle`
|
|
4115
|
+
* Why fold at all: `bootFromBundle` serves static apps from `entry.static`
|
|
3649
4116
|
* 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.
|
|
4117
|
+
* putting the built assets in the bundle gives it the shape a custom container
|
|
4118
|
+
* already had — site at `/`, admin at `/admin`, API at `/api` — which is the
|
|
4119
|
+
* only honest baseline for calling the managed runtime a drop-in replacement.
|
|
4120
|
+
*
|
|
4121
|
+
* **Every** static app is folded, each at its declared path. Folding used to
|
|
4122
|
+
* pick exactly one and refuse when it found two, which meant a project with a
|
|
4123
|
+
* site and an admin panel deployed with neither.
|
|
3653
4124
|
*/
|
|
3654
4125
|
/**
|
|
3655
|
-
*
|
|
4126
|
+
* Every static app in the manifest, in mount order.
|
|
3656
4127
|
*
|
|
3657
|
-
*
|
|
3658
|
-
*
|
|
3659
|
-
*
|
|
3660
|
-
* filesystem.
|
|
4128
|
+
* Longest path first, so the `/`-rooted app is registered last — its catch-all
|
|
4129
|
+
* would otherwise claim its siblings' URLs. Pure, so the ordering is testable
|
|
4130
|
+
* without a filesystem.
|
|
3661
4131
|
*/
|
|
3662
|
-
function
|
|
3663
|
-
const
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
4132
|
+
function foldableApps(manifest) {
|
|
4133
|
+
const apps = [];
|
|
4134
|
+
const skipped = [];
|
|
4135
|
+
for (const [name, app] of Object.entries(manifest.apps ?? {})) {
|
|
4136
|
+
if (app?.type !== "static") continue;
|
|
4137
|
+
if (!app.output) {
|
|
4138
|
+
skipped.push({
|
|
4139
|
+
name,
|
|
4140
|
+
reason: `"${name}" declares no output directory — not folded in.`
|
|
4141
|
+
});
|
|
4142
|
+
continue;
|
|
4143
|
+
}
|
|
4144
|
+
apps.push({
|
|
4145
|
+
name,
|
|
4146
|
+
build: app.build,
|
|
4147
|
+
output: app.output,
|
|
4148
|
+
path: app.path ?? "/",
|
|
4149
|
+
spa: app.spa ?? true
|
|
4150
|
+
});
|
|
4151
|
+
}
|
|
4152
|
+
apps.sort((a, b) => b.path.length - a.path.length);
|
|
4153
|
+
return {
|
|
4154
|
+
apps,
|
|
4155
|
+
skipped
|
|
4156
|
+
};
|
|
4157
|
+
}
|
|
4158
|
+
/**
|
|
4159
|
+
* Assert a built app's assets are actually rooted at the path it is served from.
|
|
4160
|
+
*
|
|
4161
|
+
* An app mounted at `/admin` but built with Vite's default `base: "/"` emits
|
|
4162
|
+
* `<script src="/assets/index-a1b2.js">`. The server serves `index.html` fine
|
|
4163
|
+
* and 404s every asset: a blank page, no server error, nothing in the logs. It
|
|
4164
|
+
* is the single most expensive silent failure in this design, so it is a build
|
|
4165
|
+
* error rather than a runtime surprise.
|
|
4166
|
+
*
|
|
4167
|
+
* Only `<script src>` and `<link href>` are inspected — those are what a bundler
|
|
4168
|
+
* rewrites through `base`. Author-written anchors and canonical URLs are not
|
|
4169
|
+
* evidence of a misbuild.
|
|
4170
|
+
*/
|
|
4171
|
+
function assertBuiltForPath(indexHtml, basePath, appName) {
|
|
4172
|
+
if (basePath === "/") return;
|
|
4173
|
+
const offenders = [];
|
|
4174
|
+
for (const match of indexHtml.matchAll(/<(?:script|link)\b[^>]*?\b(?:src|href)\s*=\s*["']([^"']+)["']/gi)) {
|
|
4175
|
+
const ref = match[1];
|
|
4176
|
+
if (!ref.startsWith("/")) continue;
|
|
4177
|
+
if (ref === `${basePath}` || ref.startsWith(`${basePath}/`)) continue;
|
|
4178
|
+
offenders.push(ref);
|
|
4179
|
+
}
|
|
4180
|
+
if (offenders.length === 0) return;
|
|
4181
|
+
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
|
|
4182
|
+
build config — see docs/apps-and-runtimes.md §4.2.`);
|
|
3673
4183
|
}
|
|
3674
4184
|
/**
|
|
3675
|
-
* Build the project's
|
|
4185
|
+
* Build the project's static apps and fold them into the backend bundle.
|
|
3676
4186
|
*
|
|
3677
4187
|
* Throws rather than exiting, so the caller decides whether a missing frontend
|
|
3678
4188
|
* should fail its command — a `build` may reasonably want to stop, and so should
|
|
@@ -3681,27 +4191,39 @@ function selectFoldableApp(manifest) {
|
|
|
3681
4191
|
async function foldFrontendIntoBundle(options) {
|
|
3682
4192
|
const { projectRoot, manifest, bundleDir, skipBuild } = options;
|
|
3683
4193
|
const log = options.log ?? ((m) => console.log(m));
|
|
3684
|
-
const {
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
4194
|
+
const { apps, skipped } = foldableApps(manifest);
|
|
4195
|
+
for (const { reason } of skipped) log(chalk.yellow(` ⚠ ${reason}`));
|
|
4196
|
+
if (apps.length === 0) return [];
|
|
4197
|
+
const outcomes = [];
|
|
4198
|
+
for (const app of apps) {
|
|
4199
|
+
if (app.build && !skipBuild) await execa(app.build, {
|
|
4200
|
+
cwd: projectRoot,
|
|
4201
|
+
stdio: "inherit",
|
|
4202
|
+
shell: true,
|
|
4203
|
+
env: {
|
|
4204
|
+
REBASE_APP_PATH: app.path,
|
|
4205
|
+
REBASE_APP_BASE: app.path === "/" ? "/" : `${app.path}/`,
|
|
4206
|
+
REBASE_APP_NAME: app.name
|
|
4207
|
+
}
|
|
4208
|
+
});
|
|
4209
|
+
const assetsDir = path.join(projectRoot, app.output);
|
|
4210
|
+
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.`);
|
|
4211
|
+
const indexHtml = path.join(assetsDir, "index.html");
|
|
4212
|
+
if (fs.existsSync(indexHtml)) assertBuiltForPath(fs.readFileSync(indexHtml, "utf8"), app.path, app.name);
|
|
4213
|
+
const { fileCount } = foldStaticIntoBundle({
|
|
4214
|
+
bundleDir,
|
|
4215
|
+
assetsDir,
|
|
4216
|
+
appName: app.name,
|
|
4217
|
+
path: app.path,
|
|
4218
|
+
spa: app.spa
|
|
4219
|
+
});
|
|
4220
|
+
outcomes.push({
|
|
4221
|
+
appName: app.name,
|
|
4222
|
+
fileCount,
|
|
4223
|
+
path: app.path
|
|
4224
|
+
});
|
|
3688
4225
|
}
|
|
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
|
-
};
|
|
4226
|
+
return outcomes;
|
|
3705
4227
|
}
|
|
3706
4228
|
//#endregion
|
|
3707
4229
|
//#region src/commands/build.ts
|
|
@@ -3719,7 +4241,7 @@ async function foldFrontendIntoBundle(options) {
|
|
|
3719
4241
|
* entrypoint, falls back to the previous behaviour: run every workspace's own
|
|
3720
4242
|
* `build` script. Nothing that built before stops building.
|
|
3721
4243
|
*/
|
|
3722
|
-
function printHelp$
|
|
4244
|
+
function printHelp$4() {
|
|
3723
4245
|
console.log(`
|
|
3724
4246
|
${chalk.bold("rebase build")} — build the apps declared in rebase.json
|
|
3725
4247
|
|
|
@@ -3754,7 +4276,7 @@ async function buildCommand(rawArgs = []) {
|
|
|
3754
4276
|
permissive: true
|
|
3755
4277
|
});
|
|
3756
4278
|
if (args["--help"]) {
|
|
3757
|
-
printHelp$
|
|
4279
|
+
printHelp$4();
|
|
3758
4280
|
return;
|
|
3759
4281
|
}
|
|
3760
4282
|
const projectRoot = requireProjectRoot();
|
|
@@ -3798,13 +4320,20 @@ async function buildCommand(rawArgs = []) {
|
|
|
3798
4320
|
console.log(`${chalk.bold("Rebase")} — building ${targets.length} app(s)\n`);
|
|
3799
4321
|
for (const { name, app } of targets) {
|
|
3800
4322
|
console.log(chalk.cyan(`▸ ${name}`) + chalk.dim(` (${app.type})`));
|
|
4323
|
+
if (app.type === "backend" && app.runtime === "custom") {
|
|
4324
|
+
console.log(chalk.dim(" custom runtime — this project builds its own image, not a bundle"));
|
|
4325
|
+
console.log(chalk.dim(` ${chalk.cyan(`npm run build --workspace ${name}`)} then ${chalk.cyan(`docker build -f ${app.dockerfile ?? "Dockerfile"} .`)}`));
|
|
4326
|
+
console.log("");
|
|
4327
|
+
continue;
|
|
4328
|
+
}
|
|
3801
4329
|
if (app.type === "backend") {
|
|
3802
4330
|
const result = await buildBundle({
|
|
3803
4331
|
projectRoot,
|
|
3804
4332
|
appName: name,
|
|
3805
4333
|
app,
|
|
3806
4334
|
outDir: args["--out"],
|
|
3807
|
-
runtimeRange: manifest.
|
|
4335
|
+
runtimeRange: manifest.rebase,
|
|
4336
|
+
storage: manifest.storage,
|
|
3808
4337
|
skipTypeCheck: args["--skip-type-check"],
|
|
3809
4338
|
skipSchema: args["--skip-schema"]
|
|
3810
4339
|
});
|
|
@@ -3816,6 +4345,16 @@ async function buildCommand(rawArgs = []) {
|
|
|
3816
4345
|
console.log(chalk.yellow(` ⚠ native dependencies detected: ${names}`));
|
|
3817
4346
|
console.log(chalk.dim(" These cannot run on the managed runtime. See `rebase doctor`."));
|
|
3818
4347
|
}
|
|
4348
|
+
const drift = detectFrameworkDepDrift(projectRoot, resolveCliVersion());
|
|
4349
|
+
if (drift.behind.length > 0) {
|
|
4350
|
+
console.log(chalk.yellow(` ⚠ framework dependencies older than this CLI (${resolveCliVersion()}):`));
|
|
4351
|
+
for (const dep of drift.behind) console.log(chalk.dim(` ${dep.name}@${dep.range} (${dep.file})`));
|
|
4352
|
+
console.log(chalk.dim(" The image supplies the server, but your bundle supplies the database"));
|
|
4353
|
+
console.log(chalk.dim(" driver — a newer runtime does not update it. Bump these and rebuild."));
|
|
4354
|
+
} else if (drift.disagreeing.length > 0) {
|
|
4355
|
+
console.log(chalk.yellow(` ⚠ mixed @rebasepro versions declared: ${drift.disagreeing.join(", ")}`));
|
|
4356
|
+
console.log(chalk.dim(" These are published together and expect to run together; pin them alike."));
|
|
4357
|
+
}
|
|
3819
4358
|
if (!args["--no-static"]) {
|
|
3820
4359
|
const folded = await foldFrontendIntoBundle({
|
|
3821
4360
|
projectRoot,
|
|
@@ -3827,28 +4366,24 @@ async function buildCommand(rawArgs = []) {
|
|
|
3827
4366
|
console.error(chalk.red(` ✗ ${err instanceof Error ? err.message : String(err)}`));
|
|
3828
4367
|
process.exit(1);
|
|
3829
4368
|
});
|
|
3830
|
-
|
|
4369
|
+
for (const outcome of folded ?? []) console.log(chalk.green(` ✓ ${outcome.appName} folded in`) + chalk.dim(` (${outcome.fileCount} file(s) → served at ${outcome.path})`));
|
|
3831
4370
|
}
|
|
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"));
|
|
4371
|
+
} else if (app.type === "static") await buildAssetApp(projectRoot, name, app, manifest.rebase, args["--out"]);
|
|
3834
4372
|
console.log("");
|
|
3835
4373
|
}
|
|
3836
4374
|
console.log(chalk.green("✓ Build complete."));
|
|
3837
4375
|
}
|
|
3838
4376
|
/**
|
|
3839
|
-
* Build a static
|
|
4377
|
+
* Build a static app and package it into a static bundle.
|
|
3840
4378
|
*
|
|
3841
4379
|
* Runs the app's own build command, checks it produced the declared output, then
|
|
3842
|
-
* packages that output into a `static`-
|
|
4380
|
+
* packages that output into a `static`-kind bundle — the same deployable shape as
|
|
3843
4381
|
* a backend bundle, so a frontend or admin app deploys through the identical
|
|
3844
4382
|
* path and runs on the identical image, just serving files instead of an API.
|
|
3845
4383
|
*/
|
|
3846
4384
|
async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride) {
|
|
3847
4385
|
const asset = app;
|
|
3848
|
-
|
|
3849
|
-
console.log(chalk.dim(" hosted admin panel — nothing to build"));
|
|
3850
|
-
return;
|
|
3851
|
-
}
|
|
4386
|
+
const basePath = asset.path ?? "/";
|
|
3852
4387
|
if (!asset.build) {
|
|
3853
4388
|
console.log(chalk.dim(" no build command declared — skipping"));
|
|
3854
4389
|
return;
|
|
@@ -3857,7 +4392,12 @@ async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride)
|
|
|
3857
4392
|
await execa(asset.build, {
|
|
3858
4393
|
cwd: projectRoot,
|
|
3859
4394
|
stdio: "inherit",
|
|
3860
|
-
shell: true
|
|
4395
|
+
shell: true,
|
|
4396
|
+
env: {
|
|
4397
|
+
REBASE_APP_PATH: basePath,
|
|
4398
|
+
REBASE_APP_BASE: basePath === "/" ? "/" : `${basePath}/`,
|
|
4399
|
+
REBASE_APP_NAME: name
|
|
4400
|
+
}
|
|
3861
4401
|
});
|
|
3862
4402
|
} catch {
|
|
3863
4403
|
console.error(chalk.red(` ✗ build command failed for "${name}"`));
|
|
@@ -3872,15 +4412,24 @@ async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride)
|
|
|
3872
4412
|
console.error(chalk.red(` ✗ declared output "${asset.output}" does not exist after building`));
|
|
3873
4413
|
process.exit(1);
|
|
3874
4414
|
}
|
|
4415
|
+
const indexHtml = path.join(outputPath, "index.html");
|
|
4416
|
+
if (fs.existsSync(indexHtml)) try {
|
|
4417
|
+
assertBuiltForPath(fs.readFileSync(indexHtml, "utf8"), basePath, name);
|
|
4418
|
+
} catch (err) {
|
|
4419
|
+
console.error(chalk.red(` ✗ ${err instanceof Error ? err.message : String(err)}`));
|
|
4420
|
+
process.exit(1);
|
|
4421
|
+
}
|
|
3875
4422
|
const result = buildStaticBundle({
|
|
3876
4423
|
projectRoot,
|
|
3877
4424
|
appName: name,
|
|
3878
4425
|
assetsDir: outputPath,
|
|
3879
4426
|
outDir: outOverride ? path.resolve(process.cwd(), outOverride) : path.join(projectRoot, `dist-bundle-${name}`),
|
|
3880
|
-
runtimeRange
|
|
4427
|
+
runtimeRange,
|
|
4428
|
+
path: basePath,
|
|
4429
|
+
spa: asset.spa ?? true
|
|
3881
4430
|
});
|
|
3882
4431
|
const rel = path.relative(projectRoot, result.outDir);
|
|
3883
|
-
console.log(chalk.green(` ✓ static bundle → ${rel}/`) + chalk.dim(` (${result.fileCount} file(s))`));
|
|
4432
|
+
console.log(chalk.green(` ✓ static bundle → ${rel}/`) + chalk.dim(` (${result.fileCount} file(s) → served at ${basePath})`));
|
|
3884
4433
|
}
|
|
3885
4434
|
/** The pre-manifest behaviour: build every workspace package. */
|
|
3886
4435
|
async function runWorkspaceBuilds(projectRoot) {
|
|
@@ -3898,6 +4447,234 @@ async function runWorkspaceBuilds(projectRoot) {
|
|
|
3898
4447
|
}
|
|
3899
4448
|
}
|
|
3900
4449
|
//#endregion
|
|
4450
|
+
//#region src/commands/eject.ts
|
|
4451
|
+
/**
|
|
4452
|
+
* CLI command: rebase eject
|
|
4453
|
+
*
|
|
4454
|
+
* The supported route from the managed runtime to a custom one.
|
|
4455
|
+
*
|
|
4456
|
+
* Without it, `runtime: "custom"` is a mode a user can only reach by
|
|
4457
|
+
* hand-writing an entrypoint they have never seen. The template used to solve
|
|
4458
|
+
* that by scaffolding `backend/src/index.ts` into every project — ~190 lines
|
|
4459
|
+
* configuring CORS, auth, cookies, storage and history, which the managed
|
|
4460
|
+
* runtime never loads. It was the most important-looking file in a new project
|
|
4461
|
+
* and editing it did nothing.
|
|
4462
|
+
*
|
|
4463
|
+
* So the file moved here. A managed project does not carry it; a project that
|
|
4464
|
+
* asks for it gets it together with the Dockerfile and the manifest change that
|
|
4465
|
+
* make it actually run.
|
|
4466
|
+
*
|
|
4467
|
+
* There is deliberately no `rebase uneject`. Going back is deleting two files
|
|
4468
|
+
* and editing one line, and a command that silently discarded a user's server
|
|
4469
|
+
* code would be worse than its absence.
|
|
4470
|
+
*/
|
|
4471
|
+
var __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
|
|
4472
|
+
/** Walk up to the package root, which holds `templates/`. */
|
|
4473
|
+
function findCliRoot(from) {
|
|
4474
|
+
const root = path.parse(from).root;
|
|
4475
|
+
let dir = from;
|
|
4476
|
+
while (dir && dir !== root) {
|
|
4477
|
+
if (fs.existsSync(path.join(dir, "templates", "eject"))) return dir;
|
|
4478
|
+
dir = path.dirname(dir);
|
|
4479
|
+
}
|
|
4480
|
+
return null;
|
|
4481
|
+
}
|
|
4482
|
+
/** Files the eject payload contributes, as `<source> → <destination>`. */
|
|
4483
|
+
var PAYLOAD = [
|
|
4484
|
+
{
|
|
4485
|
+
from: "backend/src/index.ts",
|
|
4486
|
+
to: "backend/src/index.ts",
|
|
4487
|
+
overwrite: true
|
|
4488
|
+
},
|
|
4489
|
+
{
|
|
4490
|
+
from: "backend/src/env.ts",
|
|
4491
|
+
to: "backend/src/env.ts",
|
|
4492
|
+
overwrite: true
|
|
4493
|
+
},
|
|
4494
|
+
{
|
|
4495
|
+
from: "Dockerfile",
|
|
4496
|
+
to: "Dockerfile",
|
|
4497
|
+
overwrite: false
|
|
4498
|
+
},
|
|
4499
|
+
{
|
|
4500
|
+
from: "docker-compose.custom.yml",
|
|
4501
|
+
to: "docker-compose.custom.yml",
|
|
4502
|
+
overwrite: false
|
|
4503
|
+
}
|
|
4504
|
+
];
|
|
4505
|
+
/**
|
|
4506
|
+
* The project's name, for the `{{PROJECT_NAME}}` the payload carries.
|
|
4507
|
+
*
|
|
4508
|
+
* Falls back to the directory name — a compose project name is cosmetic, and a
|
|
4509
|
+
* missing or unreadable package.json is not a reason to refuse to eject.
|
|
4510
|
+
*/
|
|
4511
|
+
function projectNameOf(projectRoot) {
|
|
4512
|
+
try {
|
|
4513
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"));
|
|
4514
|
+
if (typeof pkg.name === "string" && pkg.name.trim()) return pkg.name.trim();
|
|
4515
|
+
} catch {}
|
|
4516
|
+
return path.basename(projectRoot);
|
|
4517
|
+
}
|
|
4518
|
+
function printHelp$3() {
|
|
4519
|
+
console.log(`
|
|
4520
|
+
${chalk.bold("rebase eject")} — take ownership of the server process
|
|
4521
|
+
|
|
4522
|
+
Writes the backend entrypoint and a Dockerfile into this project, and flips its
|
|
4523
|
+
backend to ${chalk.cyan("runtime: \"custom\"")}. From then on this repository builds its own
|
|
4524
|
+
image: platform runtime upgrades no longer reach it, and CORS, auth wiring,
|
|
4525
|
+
storage and shutdown become yours to configure.
|
|
4526
|
+
|
|
4527
|
+
${chalk.bold("Usage")}
|
|
4528
|
+
rebase eject [app]
|
|
4529
|
+
|
|
4530
|
+
${chalk.bold("Options")}
|
|
4531
|
+
--dry-run List what would change, and change nothing
|
|
4532
|
+
-h, --help Show this help
|
|
4533
|
+
`.trim());
|
|
4534
|
+
}
|
|
4535
|
+
async function ejectCommand(rawArgs = []) {
|
|
4536
|
+
const args = arg({
|
|
4537
|
+
"--dry-run": Boolean,
|
|
4538
|
+
"--help": Boolean,
|
|
4539
|
+
"-h": "--help"
|
|
4540
|
+
}, {
|
|
4541
|
+
argv: rawArgs.slice(2),
|
|
4542
|
+
permissive: true
|
|
4543
|
+
});
|
|
4544
|
+
if (args["--help"]) {
|
|
4545
|
+
printHelp$3();
|
|
4546
|
+
return;
|
|
4547
|
+
}
|
|
4548
|
+
const projectRoot = requireProjectRoot();
|
|
4549
|
+
const dryRun = Boolean(args["--dry-run"]);
|
|
4550
|
+
const requested = args._.slice(1).find((value) => !value.startsWith("-"));
|
|
4551
|
+
let loaded;
|
|
4552
|
+
try {
|
|
4553
|
+
loaded = loadManifest(projectRoot);
|
|
4554
|
+
} catch (err) {
|
|
4555
|
+
if (err instanceof ManifestError) {
|
|
4556
|
+
console.error(chalk.red(`✗ ${err.message}`));
|
|
4557
|
+
for (const issue of err.issues) console.error(chalk.dim(` ${issue.path}: ${issue.message}`));
|
|
4558
|
+
process.exit(1);
|
|
4559
|
+
}
|
|
4560
|
+
throw err;
|
|
4561
|
+
}
|
|
4562
|
+
const { manifest } = loaded;
|
|
4563
|
+
let appName;
|
|
4564
|
+
let app;
|
|
4565
|
+
if (requested) {
|
|
4566
|
+
const declared = manifest.apps[requested];
|
|
4567
|
+
if (!declared) {
|
|
4568
|
+
console.error(chalk.red(`✗ No app named "${requested}" in rebase.json.`));
|
|
4569
|
+
console.error(chalk.dim(` Declared: ${Object.keys(manifest.apps).join(", ") || "(none)"}`));
|
|
4570
|
+
process.exit(1);
|
|
4571
|
+
}
|
|
4572
|
+
if (declared.type !== "backend") {
|
|
4573
|
+
console.error(chalk.red(`✗ "${requested}" is a ${declared.type} app — only a backend can be ejected.`));
|
|
4574
|
+
process.exit(1);
|
|
4575
|
+
}
|
|
4576
|
+
appName = requested;
|
|
4577
|
+
app = declared;
|
|
4578
|
+
} else {
|
|
4579
|
+
const backend = findBackendApp(manifest);
|
|
4580
|
+
if (!backend) {
|
|
4581
|
+
console.error(chalk.red("✗ This repository declares no backend app."));
|
|
4582
|
+
console.error(chalk.dim(" Only the repository that declares the backend chooses its runtime."));
|
|
4583
|
+
process.exit(1);
|
|
4584
|
+
}
|
|
4585
|
+
appName = backend.name;
|
|
4586
|
+
app = backend.app;
|
|
4587
|
+
}
|
|
4588
|
+
if (app.runtime === "custom") {
|
|
4589
|
+
console.error(chalk.red(`✗ "${appName}" is already ejected — it declares runtime: "custom".`));
|
|
4590
|
+
console.error(chalk.dim(` Its entrypoint is ${app.dockerfile ?? "Dockerfile"} and backend/src/index.ts.`));
|
|
4591
|
+
process.exit(1);
|
|
4592
|
+
}
|
|
4593
|
+
const cliRoot = findCliRoot(__dirname$1);
|
|
4594
|
+
if (!cliRoot) {
|
|
4595
|
+
console.error(chalk.red("✗ Could not locate the eject templates. Reinstall @rebasepro/cli."));
|
|
4596
|
+
process.exit(1);
|
|
4597
|
+
}
|
|
4598
|
+
const payloadDir = path.join(cliRoot, "templates", "eject");
|
|
4599
|
+
const planned = [];
|
|
4600
|
+
for (const file of PAYLOAD) {
|
|
4601
|
+
const source = path.join(payloadDir, file.from);
|
|
4602
|
+
if (!fs.existsSync(source)) {
|
|
4603
|
+
console.error(chalk.red(`✗ The eject template is missing ${file.from}. Reinstall @rebasepro/cli.`));
|
|
4604
|
+
process.exit(1);
|
|
4605
|
+
}
|
|
4606
|
+
const exists = fs.existsSync(path.join(projectRoot, file.to));
|
|
4607
|
+
planned.push({
|
|
4608
|
+
to: file.to,
|
|
4609
|
+
action: exists && !file.overwrite ? "keep" : "write"
|
|
4610
|
+
});
|
|
4611
|
+
}
|
|
4612
|
+
if (dryRun) {
|
|
4613
|
+
console.log(chalk.bold(`Would eject "${appName}" to a custom runtime:`));
|
|
4614
|
+
console.log("");
|
|
4615
|
+
for (const item of planned) console.log(item.action === "write" ? ` ${chalk.green("write")} ${item.to}` : ` ${chalk.dim("keep")} ${item.to} ${chalk.dim("(already exists)")}`);
|
|
4616
|
+
console.log(` ${chalk.green("write")} rebase.json ${chalk.dim("(runtime: \"custom\")")}`);
|
|
4617
|
+
console.log("");
|
|
4618
|
+
console.log(chalk.dim("Nothing was changed."));
|
|
4619
|
+
return;
|
|
4620
|
+
}
|
|
4621
|
+
const projectName = projectNameOf(projectRoot);
|
|
4622
|
+
for (const [index, file] of PAYLOAD.entries()) {
|
|
4623
|
+
if (planned[index].action === "keep") continue;
|
|
4624
|
+
const destination = path.join(projectRoot, file.to);
|
|
4625
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
4626
|
+
const contents = fs.readFileSync(path.join(payloadDir, file.from), "utf8").replace(/\{\{PROJECT_NAME\}\}/g, projectName);
|
|
4627
|
+
fs.writeFileSync(destination, contents, "utf8");
|
|
4628
|
+
}
|
|
4629
|
+
const dockerfile = app.dockerfile ?? "Dockerfile";
|
|
4630
|
+
manifest.apps[appName] = {
|
|
4631
|
+
...app,
|
|
4632
|
+
runtime: "custom",
|
|
4633
|
+
dockerfile,
|
|
4634
|
+
port: app.port ?? 8080
|
|
4635
|
+
};
|
|
4636
|
+
writeManifest(projectRoot, manifest);
|
|
4637
|
+
restoreBackendScripts(projectRoot);
|
|
4638
|
+
console.log("");
|
|
4639
|
+
console.log(chalk.green(`✓ Ejected "${appName}" to a custom runtime.`));
|
|
4640
|
+
console.log("");
|
|
4641
|
+
console.log(` ${chalk.cyan("backend/src/index.ts".padEnd(26))} your entrypoint — the runtime no longer boots the bundle`);
|
|
4642
|
+
console.log(` ${chalk.cyan("backend/src/env.ts".padEnd(26))} the environment it reads`);
|
|
4643
|
+
console.log(` ${chalk.cyan(dockerfile.padEnd(26))} your image`);
|
|
4644
|
+
console.log(` ${chalk.cyan("docker-compose.custom.yml".padEnd(26))} runs it`);
|
|
4645
|
+
console.log(` ${chalk.cyan("rebase.json".padEnd(26))} runtime: custom`);
|
|
4646
|
+
console.log("");
|
|
4647
|
+
console.log(chalk.yellow(" You now own CORS, auth wiring, storage and shutdown. Platform runtime"));
|
|
4648
|
+
console.log(chalk.yellow(" upgrades no longer reach this project."));
|
|
4649
|
+
console.log("");
|
|
4650
|
+
console.log(chalk.dim(` ${chalk.cyan("docker compose -f docker-compose.custom.yml up --build")}`));
|
|
4651
|
+
console.log(chalk.dim(" docker-compose.yml is untouched — it still runs the managed shape if you go back."));
|
|
4652
|
+
console.log("");
|
|
4653
|
+
}
|
|
4654
|
+
/**
|
|
4655
|
+
* Point the backend workspace's scripts at the entrypoint that now exists.
|
|
4656
|
+
*
|
|
4657
|
+
* A managed project's backend package deliberately declares no `main` and no
|
|
4658
|
+
* `start`: there is no entrypoint to name. Ejecting creates one.
|
|
4659
|
+
*/
|
|
4660
|
+
function restoreBackendScripts(projectRoot) {
|
|
4661
|
+
const packagePath = path.join(projectRoot, "backend", "package.json");
|
|
4662
|
+
if (!fs.existsSync(packagePath)) return;
|
|
4663
|
+
let parsed;
|
|
4664
|
+
try {
|
|
4665
|
+
parsed = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
4666
|
+
} catch {
|
|
4667
|
+
console.log(chalk.yellow(" ⚠ backend/package.json is not valid JSON — its scripts were left alone."));
|
|
4668
|
+
return;
|
|
4669
|
+
}
|
|
4670
|
+
const scripts = parsed.scripts ?? {};
|
|
4671
|
+
parsed.main ??= "src/index.ts";
|
|
4672
|
+
scripts.dev ??= "tsx watch --include=\"../config/**/*\" --include=\"./functions/**/*\" src/index.ts";
|
|
4673
|
+
scripts.start ??= "node dist/backend/src/index.js";
|
|
4674
|
+
parsed.scripts = scripts;
|
|
4675
|
+
fs.writeFileSync(packagePath, `${JSON.stringify(parsed, null, 4)}\n`, "utf8");
|
|
4676
|
+
}
|
|
4677
|
+
//#endregion
|
|
3901
4678
|
//#region src/commands/start.ts
|
|
3902
4679
|
/**
|
|
3903
4680
|
* CLI command: rebase start
|
|
@@ -4436,7 +5213,7 @@ function parseAgentFlags(rawArgs) {
|
|
|
4436
5213
|
return requested;
|
|
4437
5214
|
}
|
|
4438
5215
|
async function skillsInstall(rawArgs = []) {
|
|
4439
|
-
const projectDir = process.cwd();
|
|
5216
|
+
const projectDir = findProjectRoot() ?? process.cwd();
|
|
4440
5217
|
let skillsDir;
|
|
4441
5218
|
try {
|
|
4442
5219
|
skillsDir = getSkillsSourceDir();
|
|
@@ -4480,7 +5257,8 @@ async function skillsInstall(rawArgs = []) {
|
|
|
4480
5257
|
for (const agentKey of agents) {
|
|
4481
5258
|
const agent = AGENTS[agentKey];
|
|
4482
5259
|
const count = installForAgent(agentKey, skills, projectDir);
|
|
4483
|
-
|
|
5260
|
+
const shown = path.relative(process.cwd(), path.join(projectDir, agent.targetDir)) || agent.targetDir;
|
|
5261
|
+
console.log(` ${chalk.green("✓")} ${chalk.bold(agent.label)} — ${count} skills installed to ${chalk.gray(shown)}`);
|
|
4484
5262
|
}
|
|
4485
5263
|
console.log("");
|
|
4486
5264
|
console.log(chalk.gray(" Skills are project-local. Commit them to share with your team."));
|
|
@@ -5345,7 +6123,7 @@ function declaredAppsFrom(manifest) {
|
|
|
5345
6123
|
if (!apps || typeof apps !== "object") return [];
|
|
5346
6124
|
return Object.entries(apps).filter(([name]) => name.trim().length > 0).map(([name, value]) => ({
|
|
5347
6125
|
name,
|
|
5348
|
-
type:
|
|
6126
|
+
type: value?.type === "backend" ? "backend" : "static"
|
|
5349
6127
|
}));
|
|
5350
6128
|
}
|
|
5351
6129
|
/** Upload a bundle archive; returns the control-plane bundle id. */
|
|
@@ -5502,7 +6280,8 @@ async function deployBundle(opts) {
|
|
|
5502
6280
|
projectRoot,
|
|
5503
6281
|
appName: backend.name,
|
|
5504
6282
|
app: backend.app,
|
|
5505
|
-
runtimeRange: loaded.manifest.
|
|
6283
|
+
runtimeRange: loaded.manifest.rebase,
|
|
6284
|
+
storage: loaded.manifest.storage,
|
|
5506
6285
|
skipTypeCheck: opts.skipTypeCheck,
|
|
5507
6286
|
log: (m) => console.log(chalk.gray(m))
|
|
5508
6287
|
})).outDir;
|
|
@@ -5513,7 +6292,7 @@ async function deployBundle(opts) {
|
|
|
5513
6292
|
bundleDir,
|
|
5514
6293
|
log: (m) => console.log(m)
|
|
5515
6294
|
});
|
|
5516
|
-
|
|
6295
|
+
for (const outcome of folded) console.log(chalk.gray(` folded ${outcome.appName} in (${outcome.fileCount} file(s), served at ${outcome.path})`));
|
|
5517
6296
|
} catch (err) {
|
|
5518
6297
|
fail(err instanceof Error ? err.message : String(err), "Fix the frontend build, or pass --no-static to deploy the API alone.");
|
|
5519
6298
|
}
|
|
@@ -5650,6 +6429,23 @@ async function readDeployContext(client, projectId) {
|
|
|
5650
6429
|
return {};
|
|
5651
6430
|
}
|
|
5652
6431
|
}
|
|
6432
|
+
/**
|
|
6433
|
+
* Whether this repository's backend declares the managed runtime.
|
|
6434
|
+
*
|
|
6435
|
+
* Deliberately quiet: a directory that is not a Rebase project, or whose
|
|
6436
|
+
* manifest does not parse, simply does not route this way — `rebase build` is
|
|
6437
|
+
* where a broken manifest gets reported, and a deploy refusing on one before it
|
|
6438
|
+
* has even said what it is doing would be the wrong place to find out.
|
|
6439
|
+
*/
|
|
6440
|
+
function declaresManagedRuntime() {
|
|
6441
|
+
try {
|
|
6442
|
+
const projectRoot = findProjectRoot();
|
|
6443
|
+
if (!projectRoot) return false;
|
|
6444
|
+
return findBackendApp(loadManifest(projectRoot).manifest)?.app.runtime === "managed";
|
|
6445
|
+
} catch {
|
|
6446
|
+
return false;
|
|
6447
|
+
}
|
|
6448
|
+
}
|
|
5653
6449
|
async function deployCommand(rawArgs, projectRef) {
|
|
5654
6450
|
const args = arg({
|
|
5655
6451
|
"--no-follow": Boolean,
|
|
@@ -5666,8 +6462,10 @@ async function deployCommand(rawArgs, projectRef) {
|
|
|
5666
6462
|
});
|
|
5667
6463
|
const { client, url } = await requireClient(rawArgs);
|
|
5668
6464
|
const projectId = await resolveProjectRef(projectRef, client);
|
|
5669
|
-
|
|
5670
|
-
|
|
6465
|
+
const declaredManaged = !args["--source"] && !args["--bundle"] && declaresManagedRuntime();
|
|
6466
|
+
if (args["--bundle"] || declaredManaged) {
|
|
6467
|
+
if (args["--bundle"] && args["--source"]) fail("--bundle and --source cannot be combined: one is a managed bundle, the other a source build.");
|
|
6468
|
+
if (declaredManaged && !isJsonMode()) console.log(chalk.gray(" rebase.json declares runtime: managed — deploying a bundle."));
|
|
5671
6469
|
await deployBundle({
|
|
5672
6470
|
client,
|
|
5673
6471
|
url,
|
|
@@ -9039,11 +9837,8 @@ async function appsCommand(subcommand, rawArgs = []) {
|
|
|
9039
9837
|
}
|
|
9040
9838
|
function describeApp(app) {
|
|
9041
9839
|
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";
|
|
9840
|
+
case "backend": return app.runtime === "custom" ? `custom runtime — ${app.dockerfile ?? "Dockerfile"}` : `managed runtime, config: ${app.config ?? "config"}`;
|
|
9841
|
+
case "static": return `${app.root} → ${app.output} @ ${app.path ?? "/"}`;
|
|
9047
9842
|
default: return "";
|
|
9048
9843
|
}
|
|
9049
9844
|
}
|
|
@@ -9053,7 +9848,7 @@ async function listApps(asJson) {
|
|
|
9053
9848
|
if (asJson) {
|
|
9054
9849
|
console.log(JSON.stringify({
|
|
9055
9850
|
source: loaded.source,
|
|
9056
|
-
|
|
9851
|
+
rebase: loaded.manifest.rebase,
|
|
9057
9852
|
apps: loaded.manifest.apps,
|
|
9058
9853
|
managed: compatibility
|
|
9059
9854
|
}, null, 2));
|
|
@@ -9063,7 +9858,7 @@ async function listApps(asJson) {
|
|
|
9063
9858
|
console.log(chalk.dim("No rebase.json — showing the layout inferred from this project."));
|
|
9064
9859
|
console.log(chalk.dim(`Run ${chalk.cyan("rebase apps init")} to write it down.\n`));
|
|
9065
9860
|
}
|
|
9066
|
-
console.log(chalk.bold(`
|
|
9861
|
+
console.log(chalk.bold(`Rebase ${loaded.manifest.rebase}`));
|
|
9067
9862
|
console.log("");
|
|
9068
9863
|
const entries = Object.entries(loaded.manifest.apps);
|
|
9069
9864
|
if (entries.length === 0) {
|
|
@@ -9209,6 +10004,7 @@ async function entry(args) {
|
|
|
9209
10004
|
"api-keys",
|
|
9210
10005
|
"cloud",
|
|
9211
10006
|
"apps",
|
|
10007
|
+
"eject",
|
|
9212
10008
|
"generate-sdk"
|
|
9213
10009
|
].includes(command)) {
|
|
9214
10010
|
printHelp();
|
|
@@ -9233,9 +10029,10 @@ async function entry(args) {
|
|
|
9233
10029
|
argv: args.slice(3),
|
|
9234
10030
|
permissive: true
|
|
9235
10031
|
});
|
|
10032
|
+
const sdkRoot = sdkArgs["--help"] ? process.cwd() : requireProjectRoot();
|
|
9236
10033
|
await generateSdkCommand({
|
|
9237
|
-
collectionsDir: sdkArgs["--collections-dir"] || "
|
|
9238
|
-
output: sdkArgs["--output"] || "
|
|
10034
|
+
collectionsDir: sdkArgs["--collections-dir"] || path.join(sdkRoot, "config/collections"),
|
|
10035
|
+
output: sdkArgs["--output"] || path.join(sdkRoot, "generated/sdk"),
|
|
9239
10036
|
from: sdkArgs["--from"],
|
|
9240
10037
|
token: sdkArgs["--token"],
|
|
9241
10038
|
help: sdkArgs["--help"],
|
|
@@ -9261,6 +10058,9 @@ async function entry(args) {
|
|
|
9261
10058
|
case "apps":
|
|
9262
10059
|
await appsCommand(effectiveSubcommand, args);
|
|
9263
10060
|
break;
|
|
10061
|
+
case "eject":
|
|
10062
|
+
await ejectCommand(args);
|
|
10063
|
+
break;
|
|
9264
10064
|
case "auth":
|
|
9265
10065
|
await authCommand(effectiveSubcommand, args);
|
|
9266
10066
|
break;
|
|
@@ -9295,6 +10095,8 @@ ${chalk.green.bold("Commands")}
|
|
|
9295
10095
|
${chalk.blue.bold("dev")} Start the development server
|
|
9296
10096
|
${chalk.blue.bold("build")} Build all workspace packages
|
|
9297
10097
|
${chalk.blue.bold("start")} Start the backend server ${chalk.gray("(production)")}
|
|
10098
|
+
${chalk.blue.bold("apps list")} Show the apps this repository declares
|
|
10099
|
+
${chalk.blue.bold("eject")} Take ownership of the server process and image
|
|
9298
10100
|
|
|
9299
10101
|
${chalk.green.bold("Schema")}
|
|
9300
10102
|
${chalk.blue.bold("schema generate")} Generate Drizzle schema from collections
|
|
@@ -9312,25 +10114,25 @@ ${chalk.green.bold("SDK")}
|
|
|
9312
10114
|
|
|
9313
10115
|
${chalk.green.bold("Auth")}
|
|
9314
10116
|
${chalk.blue.bold("auth reset-password")} Reset a user's password
|
|
9315
|
-
${chalk.blue.bold("auth")} ${chalk.gray("--help")}
|
|
10117
|
+
${chalk.blue.bold("auth")} ${chalk.gray("--help")} Show auth command help
|
|
9316
10118
|
|
|
9317
10119
|
${chalk.green.bold("Diagnostics")}
|
|
9318
10120
|
${chalk.blue.bold("doctor")} Detect schema drift between collections, schema, and DB
|
|
9319
10121
|
|
|
9320
10122
|
${chalk.green.bold("AI Agent Skills")}
|
|
9321
|
-
${chalk.blue.bold("skills install")}
|
|
10123
|
+
${chalk.blue.bold("skills install")} Install Rebase agent skills for your AI coding assistant
|
|
9322
10124
|
|
|
9323
10125
|
${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")}
|
|
10126
|
+
${chalk.blue.bold("api-keys list")} List all service API keys
|
|
10127
|
+
${chalk.blue.bold("api-keys create")} Create a new scoped API key
|
|
10128
|
+
${chalk.blue.bold("api-keys revoke")} Revoke an existing API key
|
|
10129
|
+
${chalk.blue.bold("api-keys")} ${chalk.gray("--help")} Show API key command help
|
|
9328
10130
|
|
|
9329
10131
|
${chalk.green.bold("Rebase Cloud")}
|
|
9330
10132
|
${chalk.blue.bold("cloud login")} Sign in to the hosted control plane
|
|
9331
10133
|
${chalk.blue.bold("cloud link")} Link this directory to a cloud project
|
|
9332
10134
|
${chalk.blue.bold("cloud deploy")} Deploy the linked project + stream logs
|
|
9333
|
-
${chalk.blue.bold("cloud")} ${chalk.gray("--help")}
|
|
10135
|
+
${chalk.blue.bold("cloud")} ${chalk.gray("--help")} Show all cloud commands
|
|
9334
10136
|
|
|
9335
10137
|
${chalk.green.bold("Options")}
|
|
9336
10138
|
${chalk.blue("--version, -v")} Show version number
|
|
@@ -9340,6 +10142,6 @@ ${chalk.gray("Documentation: https://rebase.pro/docs")}
|
|
|
9340
10142
|
`);
|
|
9341
10143
|
}
|
|
9342
10144
|
//#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 };
|
|
10145
|
+
export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, 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, isIdentifierLike, isPnpmAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, printInitHelp, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveCliVersion, resolveExampleBaseUrl, resolveLocalBin, resolvePluginCliScript, resolveTsx, schemaCommand, startCommand, synthesizeManifest, validateManifest, validateProjectName, validateTsxInstallation, writeManifest };
|
|
9344
10146
|
|
|
9345
10147
|
//# sourceMappingURL=index.es.js.map
|