@rebasepro/cli 0.25.1 → 0.26.0
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/README.md +2 -1
- package/bin/rebase.js +143 -4
- package/dist/{agent-setup-BYb2Tbdz.js → agent-setup-D9mHRZ5R.js} +125 -3
- package/dist/agent-setup-D9mHRZ5R.js.map +1 -0
- package/dist/{api-keys-CSMbsmAn.js → api-keys-CyJ26KEJ.js} +41 -9
- package/dist/api-keys-CyJ26KEJ.js.map +1 -0
- package/dist/{apps-iBXNZhVN.js → apps-CnLDV5b6.js} +6 -6
- package/dist/{apps-iBXNZhVN.js.map → apps-CnLDV5b6.js.map} +1 -1
- package/dist/{atlas-dev-database-BusbHP6n.js → atlas-dev-database-BODqdCWt.js} +2 -2
- package/dist/{atlas-dev-database-BusbHP6n.js.map → atlas-dev-database-BODqdCWt.js.map} +1 -1
- package/dist/{auth-DRsTWPky.js → auth-dII2jCO1.js} +10 -7
- package/dist/auth-dII2jCO1.js.map +1 -0
- package/dist/{branch-pointer-Bn_FjNld.js → branch-pointer-BQUF6lYa.js} +1 -1
- package/dist/{build-et7Rccxi.js → build-Cza7ZQrp.js} +6 -6
- package/dist/{build-et7Rccxi.js.map → build-Cza7ZQrp.js.map} +1 -1
- package/dist/{bundle-DlqkDYSe.js → bundle-C6yEtMJ-.js} +53 -5
- package/dist/bundle-C6yEtMJ-.js.map +1 -0
- package/dist/bundle.d.ts +14 -0
- package/dist/{cloud-B-ZIjHrA.js → cloud-BdnmqQaZ.js} +8527 -4340
- package/dist/cloud-BdnmqQaZ.js.map +1 -0
- package/dist/collections-dir-DlvDhime.js +69 -0
- package/dist/collections-dir-DlvDhime.js.map +1 -0
- package/dist/commands/agent-setup.d.ts +58 -0
- package/dist/commands/api-keys.d.ts +13 -0
- package/dist/commands/cloud/activity.d.ts +25 -0
- package/dist/commands/cloud/app-key.d.ts +18 -0
- package/dist/commands/cloud/auth.d.ts +1 -0
- package/dist/commands/cloud/billing-invoices.d.ts +39 -0
- package/dist/commands/cloud/claim.d.ts +7 -0
- package/dist/commands/cloud/context.d.ts +115 -6
- package/dist/commands/cloud/deploy-summary.d.ts +162 -0
- package/dist/commands/cloud/deploy.d.ts +64 -0
- package/dist/commands/cloud/env.d.ts +24 -0
- package/dist/commands/cloud/index.d.ts +13 -1
- package/dist/commands/cloud/init.d.ts +143 -0
- package/dist/commands/cloud/login-flow.d.ts +51 -0
- package/dist/commands/cloud/notifications.d.ts +28 -0
- package/dist/commands/cloud/orgs.d.ts +13 -0
- package/dist/commands/cloud/previews.d.ts +123 -0
- package/dist/commands/cloud/resources.d.ts +68 -14
- package/dist/commands/cloud/runtime-logs.d.ts +176 -0
- package/dist/commands/cloud/service-key.d.ts +15 -0
- package/dist/commands/cloud/token-capabilities.d.ts +61 -8
- package/dist/commands/cloud/tokens.d.ts +18 -3
- package/dist/commands/cloud/usage.d.ts +63 -0
- package/dist/commands/dev.d.ts +10 -0
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/generate-view.d.ts +94 -0
- package/dist/commands/generate_sdk.d.ts +15 -0
- package/dist/commands/skills.d.ts +4 -3
- package/dist/commands/storage.d.ts +4 -0
- package/dist/commands/verify.d.ts +96 -0
- package/dist/{consent-BaB-eQKG.js → consent-BD7F29T3.js} +2 -2
- package/dist/{consent-BaB-eQKG.js.map → consent-BD7F29T3.js.map} +1 -1
- package/dist/{console-BlD9kjFW.js → console-BXB6-8GT.js} +12 -6
- package/dist/console-BXB6-8GT.js.map +1 -0
- package/dist/{context-DHiiiJ5C.js → context-GUxkZm-S.js} +481 -24
- package/dist/context-GUxkZm-S.js.map +1 -0
- package/dist/{daemon-DDMRbz4T.js → daemon-Cp7oNXth.js} +2 -2
- package/dist/{daemon-DDMRbz4T.js.map → daemon-Cp7oNXth.js.map} +1 -1
- package/dist/{db-9P6MnmdC.js → db-KzlqddGD.js} +37 -30
- package/dist/db-KzlqddGD.js.map +1 -0
- package/dist/{derive-MTdClnkc.js → derive-ae9UOoOB.js} +2 -2
- package/dist/{derive-MTdClnkc.js.map → derive-ae9UOoOB.js.map} +1 -1
- package/dist/{dev-BDN_OKXA.js → dev-DTqVMgB3.js} +59 -32
- package/dist/dev-DTqVMgB3.js.map +1 -0
- package/dist/{dev-preflight-FT9GbJVj.js → dev-preflight-Cxv3TiD1.js} +2 -2
- package/dist/{dev-preflight-FT9GbJVj.js.map → dev-preflight-Cxv3TiD1.js.map} +1 -1
- package/dist/{dev-server-CIZwAfZa.js → dev-server-OJsUGZEB.js} +2 -2
- package/dist/{dev-server-CIZwAfZa.js.map → dev-server-OJsUGZEB.js.map} +1 -1
- package/dist/{doctor-D8YXz7df.js → doctor-BRWOlqeG.js} +13 -11
- package/dist/doctor-BRWOlqeG.js.map +1 -0
- package/dist/{eject-izG2KjMo.js → eject-DABcDnPN.js} +3 -3
- package/dist/{eject-izG2KjMo.js.map → eject-DABcDnPN.js.map} +1 -1
- package/dist/{fold-static-CybSay_W.js → fold-static-BnAK-anQ.js} +3 -3
- package/dist/{fold-static-CybSay_W.js.map → fold-static-BnAK-anQ.js.map} +1 -1
- package/dist/generate-view-IFwCADvb.js +662 -0
- package/dist/generate-view-IFwCADvb.js.map +1 -0
- package/dist/{generate_sdk-B4R65oUV.js → generate_sdk-BvcWvoUh.js} +33 -7
- package/dist/generate_sdk-BvcWvoUh.js.map +1 -0
- package/dist/index.es.js +57 -38
- package/dist/index.es.js.map +1 -1
- package/dist/{init-DYaeppEN.js → init-CaU6GhwI.js} +8 -8
- package/dist/{init-DYaeppEN.js.map → init-CaU6GhwI.js.map} +1 -1
- package/dist/last-dev-database-DpW5dfoX.js +2 -0
- package/dist/{manifest-nvPdLlJY.js → manifest-BtzDy7KQ.js} +36 -3
- package/dist/{manifest-nvPdLlJY.js.map → manifest-BtzDy7KQ.js.map} +1 -1
- package/dist/{normalize-imports-BYw8ad87.js → normalize-imports-D0zXlM6g.js} +3 -3
- package/dist/{normalize-imports-BYw8ad87.js.map → normalize-imports-D0zXlM6g.js.map} +1 -1
- package/dist/{path-flags-BFgG9V6y.js → path-flags-DNvtEi2-.js} +5 -4
- package/dist/path-flags-DNvtEi2-.js.map +1 -0
- package/dist/{prepare-1rVNd2PF.js → prepare-CTw9NEkh.js} +4 -4
- package/dist/{prepare-1rVNd2PF.js.map → prepare-CTw9NEkh.js.map} +1 -1
- package/dist/{project-g4K6kV1t.js → project-CasK56vN.js} +2 -2
- package/dist/{project-g4K6kV1t.js.map → project-CasK56vN.js.map} +1 -1
- package/dist/resolve-D647F1LA.js +2 -0
- package/dist/{resources-CovP8AZ-.js → resources-CPSC5Kcu.js} +4 -4
- package/dist/{resources-CovP8AZ-.js.map → resources-CPSC5Kcu.js.map} +1 -1
- package/dist/{schema-nuxvvc50.js → schema-BSXwZSiI.js} +9 -7
- package/dist/schema-BSXwZSiI.js.map +1 -0
- package/dist/{skills-DeUIsZcT.js → skills-BU2flMOy.js} +52 -7
- package/dist/skills-BU2flMOy.js.map +1 -0
- package/dist/{start-RzZqDroW.js → start-mVb6upwY.js} +5 -5
- package/dist/{start-RzZqDroW.js.map → start-mVb6upwY.js.map} +1 -1
- package/dist/static-syntax-check.d.ts +64 -0
- package/dist/{status-D9Zm88NZ.js → status-BREvtzJm.js} +7 -7
- package/dist/{status-D9Zm88NZ.js.map → status-BREvtzJm.js.map} +1 -1
- package/dist/{storage-BB3umRfw.js → storage-DtOXGflp.js} +159 -15
- package/dist/storage-DtOXGflp.js.map +1 -0
- package/dist/{telemetry-HfU5uAh1.js → telemetry-CgwJPVMO.js} +2 -2
- package/dist/{telemetry-HfU5uAh1.js.map → telemetry-CgwJPVMO.js.map} +1 -1
- package/dist/{telemetry-TseNLk6X.js → telemetry-DwK5RQJl.js} +4 -4
- package/dist/{telemetry-TseNLk6X.js.map → telemetry-DwK5RQJl.js.map} +1 -1
- package/dist/{upgrade-apiPKKE_.js → upgrade-B7sVwL8S.js} +4 -4
- package/dist/{upgrade-apiPKKE_.js.map → upgrade-B7sVwL8S.js.map} +1 -1
- package/dist/utils/collections-dir.d.ts +14 -0
- package/dist/utils/path-flags.d.ts +2 -1
- package/dist/utils/tool-stdio.d.ts +1 -1
- package/dist/verify-D7UsV_qe.js +730 -0
- package/dist/verify-D7UsV_qe.js.map +1 -0
- package/package.json +9 -7
- package/templates/eject/backend/src/index.ts +9 -1
- package/templates/generated-schemas/ecommerce.ts +3 -0
- package/templates/overlays/baas/ai-instructions.md +13 -0
- package/templates/template/.claude/hooks/post-edit-collection.sh +99 -0
- package/templates/template/.claude/settings.json +17 -0
- package/templates/template/.env.example +10 -1
- package/templates/template/ai-instructions.md +21 -1
- package/templates/template/config/collections/presets/ecommerce/index.ts +23 -0
- package/templates/template/docker-compose.yml +4 -1
- package/templates/template/frontend/package.json +1 -0
- package/templates/views/AppView.tsx.tmpl +138 -0
- package/templates/views/EntityView.tsx.tmpl +64 -0
- package/templates/views/index.ts.tmpl +8 -0
- package/dist/agent-setup-BYb2Tbdz.js.map +0 -1
- package/dist/api-keys-CSMbsmAn.js.map +0 -1
- package/dist/auth-DRsTWPky.js.map +0 -1
- package/dist/bundle-DlqkDYSe.js.map +0 -1
- package/dist/cloud-B-ZIjHrA.js.map +0 -1
- package/dist/console-BlD9kjFW.js.map +0 -1
- package/dist/context-DHiiiJ5C.js.map +0 -1
- package/dist/db-9P6MnmdC.js.map +0 -1
- package/dist/dev-BDN_OKXA.js.map +0 -1
- package/dist/doctor-D8YXz7df.js.map +0 -1
- package/dist/generate_sdk-B4R65oUV.js.map +0 -1
- package/dist/last-dev-database-B_75fxZC.js +0 -2
- package/dist/path-flags-BFgG9V6y.js.map +0 -1
- package/dist/resolve-BQUAC3Wi.js +0 -2
- package/dist/schema-nuxvvc50.js.map +0 -1
- package/dist/skills-DeUIsZcT.js.map +0 -1
- package/dist/storage-BB3umRfw.js.map +0 -1
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import { b as UsageError, p as requireProjectRoot, w as wantsHelp, x as parseCommandArgs } from "./project-CasK56vN.js";
|
|
2
|
+
import { l as cmsMountOf, p as loadManifest } from "./manifest-BtzDy7KQ.js";
|
|
3
|
+
import { resolveCollectionsDir } from "./collections-dir-DlvDhime.js";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
//#region src/commands/generate-view.ts
|
|
9
|
+
/**
|
|
10
|
+
* `rebase generate view <slug>` — scaffold a custom admin screen and register it.
|
|
11
|
+
*
|
|
12
|
+
* Most back-office screens are a table, a form or a board, and the admin draws
|
|
13
|
+
* those from the collection. The rest — a dispatch board, a reconciliation
|
|
14
|
+
* screen, a record's audit trail — are a hundred lines of React. Written inside
|
|
15
|
+
* the admin they inherit the sign-in, the security rules, the sidebar, the UI
|
|
16
|
+
* kit and the data layer; written as a separate app they have to rebuild every
|
|
17
|
+
* one of those. This command writes the first version of that file where the
|
|
18
|
+
* admin will find it, so the screen starts native and registered.
|
|
19
|
+
*
|
|
20
|
+
* Two places, one file each:
|
|
21
|
+
*
|
|
22
|
+
* - **A screen** (default): `<frontend>/src/views/<Name>View.tsx`, listed in
|
|
23
|
+
* `<frontend>/src/views/index.ts` as an `AppView`, which `App.tsx` hands to
|
|
24
|
+
* `<RebaseCMS views>`. The first run wires `App.tsx`; later runs only append
|
|
25
|
+
* to the list.
|
|
26
|
+
* - **A record tab** (`--collection <slug>`): the same file location, with a
|
|
27
|
+
* component that takes `EntityCustomViewParams`, added to that collection's
|
|
28
|
+
* `admin.entityViews` *by path*. The collection file is shared with the
|
|
29
|
+
* backend, and a path is the one form of component reference the backend
|
|
30
|
+
* never evaluates — the admin's Vite plugin turns it into a lazy import.
|
|
31
|
+
*
|
|
32
|
+
* The frontend is where `rebase.json` says the CMS is (the static app with a
|
|
33
|
+
* `cms` mount), which is `frontend/` in everything `rebase init` writes. A
|
|
34
|
+
* project with no admin panel (`rebase init --headless`) is refused: there is
|
|
35
|
+
* nowhere for a view to render.
|
|
36
|
+
*
|
|
37
|
+
* Edits to existing files are textual and conservative. When `App.tsx` already
|
|
38
|
+
* passes its own `views`, or a collection's shape is not one this can read, the
|
|
39
|
+
* view file is still written and the command prints the one line to add by
|
|
40
|
+
* hand instead of guessing.
|
|
41
|
+
*/
|
|
42
|
+
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
/** What `rebase generate` dispatches. Read by `cli.ts` to name the subcommand. */
|
|
44
|
+
var GENERATE_SUBCOMMANDS = ["view"];
|
|
45
|
+
var VIEW_FLAGS = {
|
|
46
|
+
"--collection": String,
|
|
47
|
+
"--name": String,
|
|
48
|
+
"--icon": String,
|
|
49
|
+
"--dry-run": Boolean,
|
|
50
|
+
"--force": Boolean
|
|
51
|
+
};
|
|
52
|
+
/** A slug is a URL segment and an entity view key: kebab-case, starting with a letter. */
|
|
53
|
+
var SLUG_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
54
|
+
/**
|
|
55
|
+
* Top-level paths the admin's own routes answer (`RebaseRouteDefs` in
|
|
56
|
+
* `@rebasepro/cms`): `/c/*` the collections, `/s` Studio, `/settings`. They are
|
|
57
|
+
* declared before custom views, so a screen at one of them was listed in the
|
|
58
|
+
* sidebar and opened the built-in page instead.
|
|
59
|
+
*/
|
|
60
|
+
var RESERVED_SCREEN_SLUGS = [
|
|
61
|
+
"c",
|
|
62
|
+
"s",
|
|
63
|
+
"settings"
|
|
64
|
+
];
|
|
65
|
+
/** `orders-board` → `OrdersBoardView`; `sales-view` → `SalesView`, not `SalesViewView`. */
|
|
66
|
+
function viewComponentName(slug) {
|
|
67
|
+
const pascal = slug.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
68
|
+
return pascal.endsWith("View") ? pascal : `${pascal}View`;
|
|
69
|
+
}
|
|
70
|
+
/** `orders-board` → `Orders board`: labels are sentence case in the admin. */
|
|
71
|
+
function viewLabel(slug) {
|
|
72
|
+
const words = slug.split("-").join(" ");
|
|
73
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The source with every string, template literal, regular expression and
|
|
77
|
+
* comment blanked out, character for character, so brackets and keys can be
|
|
78
|
+
* found by position without being fooled by a `{` inside a string. String
|
|
79
|
+
* contents become `_` (they are still "something"), comments become spaces.
|
|
80
|
+
*
|
|
81
|
+
* Exported for its tests.
|
|
82
|
+
*/
|
|
83
|
+
function maskCode(source) {
|
|
84
|
+
const out = source.split("");
|
|
85
|
+
let i = 0;
|
|
86
|
+
const blank = (from, to, fill) => {
|
|
87
|
+
for (let k = from; k < to; k++) if (out[k] !== "\n") out[k] = fill;
|
|
88
|
+
};
|
|
89
|
+
const previousSignificant = (at) => {
|
|
90
|
+
for (let k = at - 1; k >= 0; k--) {
|
|
91
|
+
const ch = out[k];
|
|
92
|
+
if (ch !== " " && ch !== " " && ch !== "\n" && ch !== "\r") return ch;
|
|
93
|
+
}
|
|
94
|
+
return "";
|
|
95
|
+
};
|
|
96
|
+
while (i < source.length) {
|
|
97
|
+
const ch = source[i];
|
|
98
|
+
const next = source[i + 1];
|
|
99
|
+
if (ch === "/" && next === "/") {
|
|
100
|
+
const end = source.indexOf("\n", i);
|
|
101
|
+
const stop = end === -1 ? source.length : end;
|
|
102
|
+
blank(i, stop, " ");
|
|
103
|
+
i = stop;
|
|
104
|
+
} else if (ch === "/" && next === "*") {
|
|
105
|
+
const end = source.indexOf("*/", i + 2);
|
|
106
|
+
const stop = end === -1 ? source.length : end + 2;
|
|
107
|
+
blank(i, stop, " ");
|
|
108
|
+
i = stop;
|
|
109
|
+
} else if (ch === "\"" || ch === "'" || ch === "`") {
|
|
110
|
+
let k = i + 1;
|
|
111
|
+
while (k < source.length && source[k] !== ch) {
|
|
112
|
+
if (source[k] === "\\") k++;
|
|
113
|
+
k++;
|
|
114
|
+
}
|
|
115
|
+
blank(i + 1, k, "_");
|
|
116
|
+
i = k + 1;
|
|
117
|
+
} else if (ch === "/" && (previousSignificant(i) === "" || "(,=:[!&|?{;".includes(previousSignificant(i)))) {
|
|
118
|
+
let k = i + 1;
|
|
119
|
+
let inClass = false;
|
|
120
|
+
while (k < source.length && source[k] !== "\n") {
|
|
121
|
+
if (source[k] === "\\") k++;
|
|
122
|
+
else if (source[k] === "[") inClass = true;
|
|
123
|
+
else if (source[k] === "]") inClass = false;
|
|
124
|
+
else if (source[k] === "/" && !inClass) break;
|
|
125
|
+
k++;
|
|
126
|
+
}
|
|
127
|
+
blank(i + 1, k, "_");
|
|
128
|
+
i = k + 1;
|
|
129
|
+
} else i++;
|
|
130
|
+
}
|
|
131
|
+
return out.join("");
|
|
132
|
+
}
|
|
133
|
+
var CLOSERS = {
|
|
134
|
+
"{": "}",
|
|
135
|
+
"[": "]",
|
|
136
|
+
"(": ")"
|
|
137
|
+
};
|
|
138
|
+
/** The index of the bracket closing the one at `open`, in masked source. */
|
|
139
|
+
function matchClose(masked, open) {
|
|
140
|
+
const stack = [];
|
|
141
|
+
for (let i = open; i < masked.length; i++) {
|
|
142
|
+
const ch = masked[i];
|
|
143
|
+
if (ch in CLOSERS) stack.push(CLOSERS[ch]);
|
|
144
|
+
else if (ch === "}" || ch === "]" || ch === ")") {
|
|
145
|
+
if (stack.pop() !== ch) return -1;
|
|
146
|
+
if (stack.length === 0) return i;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return -1;
|
|
150
|
+
}
|
|
151
|
+
/** The index of the innermost `{` still open at `at`, in masked source. */
|
|
152
|
+
function enclosingBrace(masked, at) {
|
|
153
|
+
const stack = [];
|
|
154
|
+
for (let i = 0; i < at; i++) {
|
|
155
|
+
const ch = masked[i];
|
|
156
|
+
if (ch === "{" || ch === "[" || ch === "(") stack.push(i);
|
|
157
|
+
else if (ch === "}" || ch === "]" || ch === ")") stack.pop();
|
|
158
|
+
}
|
|
159
|
+
for (let k = stack.length - 1; k >= 0; k--) if (masked[stack[k]] === "{") return stack[k];
|
|
160
|
+
return -1;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The position of `key:` as a direct property of the object whose `{` is at
|
|
164
|
+
* `open`, followed by `opener` (`{` or `[`) — or -1.
|
|
165
|
+
*/
|
|
166
|
+
function directProperty(masked, open, key, opener) {
|
|
167
|
+
const close = matchClose(masked, open);
|
|
168
|
+
if (close === -1) return void 0;
|
|
169
|
+
const pattern = new RegExp(`(^|[\\s,{])${key}\\s*:\\s*\\${opener}`, "g");
|
|
170
|
+
const body = masked.slice(open + 1, close);
|
|
171
|
+
for (const match of body.matchAll(pattern)) {
|
|
172
|
+
const keyAt = open + 1 + (match.index ?? 0) + match[1].length;
|
|
173
|
+
if (enclosingBrace(masked, keyAt) !== open) continue;
|
|
174
|
+
return {
|
|
175
|
+
keyAt,
|
|
176
|
+
valueOpen: open + 1 + (match.index ?? 0) + match[0].length - 1
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function lineStart(source, at) {
|
|
181
|
+
return source.lastIndexOf("\n", at - 1) + 1;
|
|
182
|
+
}
|
|
183
|
+
function indentAt(source, at) {
|
|
184
|
+
const start = lineStart(source, at);
|
|
185
|
+
return /^[ \t]*/.exec(source.slice(start))?.[0] ?? "";
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The indentation step a file uses: the smallest non-zero leading run of
|
|
189
|
+
* spaces, not counting the ` * ` lines of a block comment.
|
|
190
|
+
*/
|
|
191
|
+
function indentUnit(source) {
|
|
192
|
+
if (/^\t/m.test(source)) return " ";
|
|
193
|
+
let unit = Infinity;
|
|
194
|
+
for (const match of source.matchAll(/^( +)[^\s*]/gm)) unit = Math.min(unit, match[1].length);
|
|
195
|
+
return " ".repeat(Number.isFinite(unit) ? unit : 4);
|
|
196
|
+
}
|
|
197
|
+
function reindent(block, indent) {
|
|
198
|
+
return block.split("\n").map((line) => line ? indent + line : line).join("\n");
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Insert `element` (already indented one level past `indent`) as the last item
|
|
202
|
+
* of the array whose `[` is at `open`.
|
|
203
|
+
*/
|
|
204
|
+
function appendToArray(source, masked, open, element) {
|
|
205
|
+
const close = matchClose(masked, open);
|
|
206
|
+
const inner = masked.slice(open + 1, close);
|
|
207
|
+
const indent = indentAt(source, open);
|
|
208
|
+
if (inner.trim() === "") return `${source.slice(0, open + 1)}\n${element}\n${indent}${source.slice(close)}`;
|
|
209
|
+
let last = close - 1;
|
|
210
|
+
while (last > open && /\s/.test(masked[last])) last--;
|
|
211
|
+
const hasTrailingComma = masked[last] === ",";
|
|
212
|
+
const insertAt = last + 1;
|
|
213
|
+
return `${source.slice(0, insertAt)}${hasTrailingComma ? "" : ","}\n${element}${source.slice(insertAt)}`;
|
|
214
|
+
}
|
|
215
|
+
/** Add an import line after the file's last import statement. */
|
|
216
|
+
function addImport(source, line) {
|
|
217
|
+
const masked = maskCode(source);
|
|
218
|
+
let end = 0;
|
|
219
|
+
for (const match of masked.matchAll(/^import\b[\s\S]*?["'][^"'\n]*["'];?[ \t]*$/gm)) end = (match.index ?? 0) + match[0].length;
|
|
220
|
+
if (end === 0) return `${line}\n${source}`;
|
|
221
|
+
return `${source.slice(0, end)}\n${line}${source.slice(end)}`;
|
|
222
|
+
}
|
|
223
|
+
/** A path import from `fromDir` to `toFile`, without extension, always starting with `.`. */
|
|
224
|
+
function relativeImport(fromDir, toFile) {
|
|
225
|
+
const relative = path.relative(fromDir, toFile).split(path.sep).join("/").replace(/\.tsx?$/, "");
|
|
226
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
227
|
+
}
|
|
228
|
+
/** The admin's frontend directory: the static app that mounts the CMS, or `frontend/`. */
|
|
229
|
+
function resolveFrontendDir(projectRoot) {
|
|
230
|
+
try {
|
|
231
|
+
const cms = cmsMountOf(loadManifest(projectRoot).manifest);
|
|
232
|
+
if (cms) return path.join(projectRoot, cms.app.root);
|
|
233
|
+
} catch {}
|
|
234
|
+
const fallback = path.join(projectRoot, "frontend");
|
|
235
|
+
return fs.existsSync(path.join(fallback, "src")) ? fallback : void 0;
|
|
236
|
+
}
|
|
237
|
+
function collectionFiles(dir) {
|
|
238
|
+
if (!fs.existsSync(dir)) return [];
|
|
239
|
+
const out = [];
|
|
240
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
241
|
+
for (const entry of entries) {
|
|
242
|
+
const full = path.join(dir, entry.name);
|
|
243
|
+
if (entry.isDirectory()) {
|
|
244
|
+
if (entry.name !== "node_modules" && entry.name !== "presets") out.push(...collectionFiles(full));
|
|
245
|
+
} else if (/\.tsx?$/.test(entry.name) && !/\.(test|spec|d)\.tsx?$/.test(entry.name)) out.push(full);
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
var SLUG_PROPERTY = /(^|[\s,{])slug\s*:\s*(["'])([^"'\n]+)\2/g;
|
|
250
|
+
/** Every `slug: "…"` a collection file declares, with where. */
|
|
251
|
+
function declaredSlugs(dir) {
|
|
252
|
+
const found = [];
|
|
253
|
+
for (const file of collectionFiles(dir)) {
|
|
254
|
+
const source = fs.readFileSync(file, "utf8");
|
|
255
|
+
const masked = maskCode(source);
|
|
256
|
+
for (const match of source.matchAll(SLUG_PROPERTY)) {
|
|
257
|
+
const at = (match.index ?? 0) + match[1].length;
|
|
258
|
+
if (masked[at] !== "s") continue;
|
|
259
|
+
found.push({
|
|
260
|
+
slug: match[3],
|
|
261
|
+
file,
|
|
262
|
+
at
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return found;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* The scaffolds live in `templates/views/` rather than in template literals
|
|
270
|
+
* here: they import React and the admin packages, and this package's sources
|
|
271
|
+
* must not name those, even inside a string (`pnpm check:types-headless`).
|
|
272
|
+
*/
|
|
273
|
+
function viewTemplate(name) {
|
|
274
|
+
const cliRoot = findCliRoot(__dirname);
|
|
275
|
+
if (!cliRoot) throw new Error(`The CLI's templates/views folder is missing from ${__dirname}.`);
|
|
276
|
+
return fs.readFileSync(path.join(cliRoot, "templates", "views", name), "utf8");
|
|
277
|
+
}
|
|
278
|
+
/** Walk up to the package root, which holds `templates/views`. */
|
|
279
|
+
function findCliRoot(from) {
|
|
280
|
+
const root = path.parse(from).root;
|
|
281
|
+
let dir = from;
|
|
282
|
+
while (dir && dir !== root) {
|
|
283
|
+
if (fs.existsSync(path.join(dir, "templates", "views"))) return dir;
|
|
284
|
+
dir = path.dirname(dir);
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* A label as JSX text: written as is when it is plain, as a string expression
|
|
290
|
+
* when it holds a character JSX reads — `{`, `}`, `<`, `>`, `&`. Pasted raw,
|
|
291
|
+
* `--name "KPIs {beta}"` wrote `<Typography>KPIs {beta}</Typography>`, and the
|
|
292
|
+
* view did not compile ("Cannot find name 'beta'").
|
|
293
|
+
*/
|
|
294
|
+
function jsxText(text) {
|
|
295
|
+
return /[{}<>&]/.test(text) ? `{${JSON.stringify(text)}}` : text;
|
|
296
|
+
}
|
|
297
|
+
/** A label inside the leading `/** … *\/` comment: one line, and no `*\/` to close it early. */
|
|
298
|
+
function commentText(text) {
|
|
299
|
+
return text.replace(/[\r\n]+/g, " ").replaceAll("*/", "*\\/");
|
|
300
|
+
}
|
|
301
|
+
function fillViewTemplate(template, options) {
|
|
302
|
+
const { slug, componentName, label, collection } = options;
|
|
303
|
+
return template.replaceAll("\"__COLLECTION__\"", JSON.stringify(collection)).replaceAll("__COLLECTION__", collection).replaceAll("__COMPONENT__", componentName).replaceAll("__label_jsx__", jsxText(label.toLowerCase())).replaceAll("__LABEL_JSX__", jsxText(label)).replaceAll("__label__", commentText(label.toLowerCase())).replaceAll("__LABEL__", commentText(label)).replaceAll("__SLUG__", slug);
|
|
304
|
+
}
|
|
305
|
+
function appViewSource(options) {
|
|
306
|
+
return fillViewTemplate(viewTemplate("AppView.tsx.tmpl"), options);
|
|
307
|
+
}
|
|
308
|
+
function entityViewSource(options) {
|
|
309
|
+
return fillViewTemplate(viewTemplate("EntityView.tsx.tmpl"), options);
|
|
310
|
+
}
|
|
311
|
+
function viewsIndexHeader() {
|
|
312
|
+
return viewTemplate("index.ts.tmpl");
|
|
313
|
+
}
|
|
314
|
+
function appViewEntry(options, unit) {
|
|
315
|
+
return [
|
|
316
|
+
"{",
|
|
317
|
+
`${unit}slug: ${JSON.stringify(options.slug)},`,
|
|
318
|
+
`${unit}name: ${JSON.stringify(options.label)},`,
|
|
319
|
+
`${unit}icon: ${JSON.stringify(options.icon)},`,
|
|
320
|
+
`${unit}view: ${options.componentName}`,
|
|
321
|
+
"}"
|
|
322
|
+
].join("\n");
|
|
323
|
+
}
|
|
324
|
+
function readIfExists(file) {
|
|
325
|
+
return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : void 0;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* The frontend's package.json gains `@rebasepro/cms-types` when it lacks it.
|
|
329
|
+
*
|
|
330
|
+
* The view files import their prop types from it, and under pnpm a package the
|
|
331
|
+
* frontend does not declare does not resolve from the frontend — the frontend's
|
|
332
|
+
* `tsc` would fail on the first generated view. The version is the one the
|
|
333
|
+
* frontend already uses for `@rebasepro/cms`, so the two cannot drift.
|
|
334
|
+
*/
|
|
335
|
+
function planCmsTypesDependency(frontendDir, files, manualSteps) {
|
|
336
|
+
const manifestFile = path.join(frontendDir, "package.json");
|
|
337
|
+
const raw = readIfExists(manifestFile);
|
|
338
|
+
if (raw === void 0) return;
|
|
339
|
+
let manifest;
|
|
340
|
+
try {
|
|
341
|
+
manifest = JSON.parse(raw);
|
|
342
|
+
} catch {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const declared = {
|
|
346
|
+
...manifest.devDependencies,
|
|
347
|
+
...manifest.dependencies
|
|
348
|
+
};
|
|
349
|
+
if (declared["@rebasepro/cms-types"]) return;
|
|
350
|
+
const version = declared["@rebasepro/cms"] ?? declared["@rebasepro/app"];
|
|
351
|
+
if (!version) return;
|
|
352
|
+
const dependencies = {
|
|
353
|
+
...manifest.dependencies,
|
|
354
|
+
"@rebasepro/cms-types": version
|
|
355
|
+
};
|
|
356
|
+
const sorted = Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b)));
|
|
357
|
+
const indent = /^(\s+)"/m.exec(raw)?.[1] ?? " ";
|
|
358
|
+
files.push({
|
|
359
|
+
path: manifestFile,
|
|
360
|
+
contents: `${JSON.stringify({
|
|
361
|
+
...manifest,
|
|
362
|
+
dependencies: sorted
|
|
363
|
+
}, null, indent)}\n`,
|
|
364
|
+
action: "update"
|
|
365
|
+
});
|
|
366
|
+
manualSteps.push(`Run your package manager's install: ${path.basename(frontendDir)}/package.json now depends on @rebasepro/cms-types, which the view imports its types from.`);
|
|
367
|
+
}
|
|
368
|
+
function defaultCollectionFor(projectRoot) {
|
|
369
|
+
const slugs = declaredSlugs(resolveCollectionsDir(projectRoot)).map((entry) => entry.slug);
|
|
370
|
+
return slugs.find((slug) => slug !== "users") ?? slugs[0] ?? "users";
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Everything `rebase generate view` would write, without writing it.
|
|
374
|
+
*
|
|
375
|
+
* @throws UsageError when the slug is not a slug, there is no admin panel, the
|
|
376
|
+
* view already exists (without `force`), or `--collection` names nothing.
|
|
377
|
+
*/
|
|
378
|
+
function planGenerateView(options) {
|
|
379
|
+
const { projectRoot, slug } = options;
|
|
380
|
+
if (!SLUG_PATTERN.test(slug)) throw new UsageError(`"${slug}" is not a view slug: use lowercase letters, digits and dashes, starting with a letter (e.g. orders-board).`);
|
|
381
|
+
if (!options.collection && RESERVED_SCREEN_SLUGS.includes(slug)) throw new UsageError(`"${slug}" is reserved: the admin's own routes answer /${slug}, so a screen there would never show. Pick another slug (e.g. ${slug === "settings" ? "app-settings" : `${slug}-view`}).`);
|
|
382
|
+
const frontendDir = resolveFrontendDir(projectRoot);
|
|
383
|
+
if (!frontendDir) throw new UsageError("This project has no admin panel to add a view to: rebase.json declares no app with a `cms` mount, and there is no frontend/src. A project created with `rebase init --headless` has no admin.");
|
|
384
|
+
const componentName = viewComponentName(slug);
|
|
385
|
+
const label = options.name?.trim() || viewLabel(slug);
|
|
386
|
+
const viewsDir = path.join(frontendDir, "src", "views");
|
|
387
|
+
const viewFile = path.join(viewsDir, `${componentName}.tsx`);
|
|
388
|
+
const files = [];
|
|
389
|
+
const manualSteps = [];
|
|
390
|
+
const viewExists = fs.existsSync(viewFile);
|
|
391
|
+
if (viewExists && !options.force) throw new UsageError(`${path.relative(projectRoot, viewFile)} already exists. Pass --force to replace it; its registration is kept.`);
|
|
392
|
+
const viewAction = viewExists ? "replace" : "create";
|
|
393
|
+
if (options.collection) {
|
|
394
|
+
const kind = "entity";
|
|
395
|
+
const collectionsDir = resolveCollectionsDir(projectRoot);
|
|
396
|
+
const declared = declaredSlugs(collectionsDir);
|
|
397
|
+
const matches = declared.filter((entry) => entry.slug === options.collection);
|
|
398
|
+
if (matches.length === 0) {
|
|
399
|
+
const known = [...new Set(declared.map((entry) => entry.slug))].sort();
|
|
400
|
+
throw new UsageError(`No collection with slug "${options.collection}" under ${path.relative(projectRoot, collectionsDir) || "."}.` + (known.length ? ` Declared: ${known.join(", ")}.` : ""));
|
|
401
|
+
}
|
|
402
|
+
if (matches.length > 1) throw new UsageError(`More than one collection declares slug "${options.collection}": ${matches.map((entry) => path.relative(projectRoot, entry.file)).join(", ")}.`);
|
|
403
|
+
const [{ file: collectionFile, at: slugAt }] = matches;
|
|
404
|
+
files.push({
|
|
405
|
+
path: viewFile,
|
|
406
|
+
contents: entityViewSource({
|
|
407
|
+
slug,
|
|
408
|
+
componentName,
|
|
409
|
+
label,
|
|
410
|
+
collection: options.collection
|
|
411
|
+
}),
|
|
412
|
+
action: viewAction
|
|
413
|
+
});
|
|
414
|
+
const builder = relativeImport(path.dirname(collectionFile), viewFile);
|
|
415
|
+
const updated = registerEntityView(fs.readFileSync(collectionFile, "utf8"), slugAt, {
|
|
416
|
+
key: slug,
|
|
417
|
+
name: label,
|
|
418
|
+
builder
|
|
419
|
+
});
|
|
420
|
+
const where = `${path.relative(projectRoot, collectionFile)}`;
|
|
421
|
+
if (updated === "already") {} else if (updated === void 0) manualSteps.push(`Add this to admin.entityViews in ${where}: { key: ${JSON.stringify(slug)}, name: ${JSON.stringify(label)}, Builder: ${JSON.stringify(builder)} }`);
|
|
422
|
+
else files.push({
|
|
423
|
+
path: collectionFile,
|
|
424
|
+
contents: updated,
|
|
425
|
+
action: "update"
|
|
426
|
+
});
|
|
427
|
+
planCmsTypesDependency(frontendDir, files, manualSteps);
|
|
428
|
+
return {
|
|
429
|
+
kind,
|
|
430
|
+
componentName,
|
|
431
|
+
viewFile,
|
|
432
|
+
files,
|
|
433
|
+
manualSteps
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
files.push({
|
|
437
|
+
path: viewFile,
|
|
438
|
+
contents: appViewSource({
|
|
439
|
+
slug,
|
|
440
|
+
componentName,
|
|
441
|
+
label,
|
|
442
|
+
collection: defaultCollectionFor(projectRoot)
|
|
443
|
+
}),
|
|
444
|
+
action: viewAction
|
|
445
|
+
});
|
|
446
|
+
const indexFile = path.join(viewsDir, "index.ts");
|
|
447
|
+
const existingIndex = readIfExists(indexFile);
|
|
448
|
+
const icon = options.icon ?? "LayoutDashboard";
|
|
449
|
+
if (existingIndex === void 0) {
|
|
450
|
+
const unit = " ";
|
|
451
|
+
files.push({
|
|
452
|
+
path: indexFile,
|
|
453
|
+
contents: `${viewsIndexHeader()}import { ${componentName} } from "./${componentName}";\n\nexport const views: AppView[] = [\n${reindent(appViewEntry({
|
|
454
|
+
slug,
|
|
455
|
+
label,
|
|
456
|
+
icon,
|
|
457
|
+
componentName
|
|
458
|
+
}, unit), unit)}\n];\n`,
|
|
459
|
+
action: "create"
|
|
460
|
+
});
|
|
461
|
+
} else {
|
|
462
|
+
const updated = registerAppView(existingIndex, {
|
|
463
|
+
slug,
|
|
464
|
+
label,
|
|
465
|
+
icon,
|
|
466
|
+
componentName
|
|
467
|
+
});
|
|
468
|
+
if (updated === void 0) manualSteps.push(`Add { slug: ${JSON.stringify(slug)}, name: ${JSON.stringify(label)}, view: ${componentName} } to the views in ${path.relative(projectRoot, indexFile)}.`);
|
|
469
|
+
else if (updated !== "already") files.push({
|
|
470
|
+
path: indexFile,
|
|
471
|
+
contents: updated,
|
|
472
|
+
action: "update"
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
const appFile = path.join(frontendDir, "src", "App.tsx");
|
|
476
|
+
const app = readIfExists(appFile);
|
|
477
|
+
const wired = app === void 0 ? void 0 : wireViewsIntoApp(app);
|
|
478
|
+
if (wired === void 0) manualSteps.push(`Pass the screens to the admin: import { views } from "./views" and add views={views} to <RebaseCMS> in ${path.relative(projectRoot, appFile)} (or spread ...views into the views it already passes).`);
|
|
479
|
+
else if (wired !== "already") files.push({
|
|
480
|
+
path: appFile,
|
|
481
|
+
contents: wired,
|
|
482
|
+
action: "update"
|
|
483
|
+
});
|
|
484
|
+
planCmsTypesDependency(frontendDir, files, manualSteps);
|
|
485
|
+
return {
|
|
486
|
+
kind: "app",
|
|
487
|
+
componentName,
|
|
488
|
+
viewFile,
|
|
489
|
+
files,
|
|
490
|
+
manualSteps
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* `views/index.ts` with one more entry, `"already"` when the slug is listed, or
|
|
495
|
+
* `undefined` when the file has no `export const views … = [`.
|
|
496
|
+
*
|
|
497
|
+
* Exported for its tests.
|
|
498
|
+
*/
|
|
499
|
+
function registerAppView(source, options) {
|
|
500
|
+
const masked = maskCode(source);
|
|
501
|
+
const declaration = /export\s+const\s+views\b[^=]*=\s*\[/.exec(masked);
|
|
502
|
+
if (!declaration) return void 0;
|
|
503
|
+
const open = declaration.index + declaration[0].length - 1;
|
|
504
|
+
if (matchClose(masked, open) === -1) return void 0;
|
|
505
|
+
const close = matchClose(masked, open);
|
|
506
|
+
const listed = source.slice(open, close);
|
|
507
|
+
if (new RegExp(`slug\\s*:\\s*["']${options.slug}["']`).test(listed)) return "already";
|
|
508
|
+
const unit = indentUnit(source);
|
|
509
|
+
const indent = indentAt(source, open) + unit;
|
|
510
|
+
return addImport(appendToArray(source, masked, open, reindent(appViewEntry(options, unit), indent)), `import { ${options.componentName} } from "./${options.componentName}";`);
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* `App.tsx` with `views={views}` on `<RebaseCMS>` and its import, `"already"`
|
|
514
|
+
* when it passes exactly that, or `undefined` when it cannot be done safely:
|
|
515
|
+
* no `<RebaseCMS`, a `views` prop of its own, or a `views` name already in scope.
|
|
516
|
+
*
|
|
517
|
+
* Exported for its tests.
|
|
518
|
+
*/
|
|
519
|
+
function wireViewsIntoApp(source) {
|
|
520
|
+
const masked = maskCode(source);
|
|
521
|
+
const tag = /<RebaseCMS\b/.exec(masked);
|
|
522
|
+
if (!tag) return void 0;
|
|
523
|
+
const tagEnd = masked.indexOf(">", tag.index);
|
|
524
|
+
if (tagEnd === -1) return void 0;
|
|
525
|
+
const attributes = masked.slice(tag.index, tagEnd);
|
|
526
|
+
const importsViews = /import\s*\{\s*views\s*\}\s*from\s*["']\.\/views(?:\/index)?["']/.test(source);
|
|
527
|
+
if (/\sviews\s*=/.test(attributes)) return importsViews && /\sviews\s*=\s*\{\s*views\s*\}/.test(attributes) ? "already" : void 0;
|
|
528
|
+
if (/\bviews\b/.test(masked)) return void 0;
|
|
529
|
+
const insertAt = tag.index + 10;
|
|
530
|
+
const afterTag = source.slice(insertAt);
|
|
531
|
+
const nextLine = /^[ \t]*\r?\n([ \t]*)\S/.exec(afterTag);
|
|
532
|
+
const attribute = nextLine ? `\n${nextLine[1]}views={views}` : " views={views}";
|
|
533
|
+
return addImport(source.slice(0, insertAt) + attribute + source.slice(insertAt), "import { views } from \"./views\";");
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* The collection file with a `{ key, name, Builder }` entry in the
|
|
537
|
+
* `admin.entityViews` of the collection whose `slug:` is at `slugAt`;
|
|
538
|
+
* `"already"` when that key is there; `undefined` when the shape cannot be read.
|
|
539
|
+
*
|
|
540
|
+
* Exported for its tests.
|
|
541
|
+
*/
|
|
542
|
+
function registerEntityView(source, slugAt, view) {
|
|
543
|
+
const masked = maskCode(source);
|
|
544
|
+
const collectionOpen = enclosingBrace(masked, slugAt);
|
|
545
|
+
if (collectionOpen === -1) return void 0;
|
|
546
|
+
const unit = indentUnit(source);
|
|
547
|
+
const entry = [
|
|
548
|
+
"{",
|
|
549
|
+
`${unit}key: ${JSON.stringify(view.key)},`,
|
|
550
|
+
`${unit}name: ${JSON.stringify(view.name)},`,
|
|
551
|
+
`${unit}Builder: ${JSON.stringify(view.builder)}`,
|
|
552
|
+
"}"
|
|
553
|
+
].join("\n");
|
|
554
|
+
const admin = directProperty(masked, collectionOpen, "admin", "{");
|
|
555
|
+
if (admin) {
|
|
556
|
+
const entityViews = directProperty(masked, admin.valueOpen, "entityViews", "[");
|
|
557
|
+
if (entityViews) {
|
|
558
|
+
const close = matchClose(masked, entityViews.valueOpen);
|
|
559
|
+
if (close === -1) return void 0;
|
|
560
|
+
const listed = source.slice(entityViews.valueOpen, close);
|
|
561
|
+
if (new RegExp(`key\\s*:\\s*["']${view.key}["']|["']${view.key}["']`).test(listed)) return "already";
|
|
562
|
+
const indent = indentAt(source, entityViews.keyAt) + unit;
|
|
563
|
+
return appendToArray(source, masked, entityViews.valueOpen, reindent(entry, indent));
|
|
564
|
+
}
|
|
565
|
+
const indent = indentAt(source, admin.keyAt) + unit;
|
|
566
|
+
const block = `${indent}entityViews: [\n${reindent(entry, indent + unit)}\n${indent}],`;
|
|
567
|
+
const adminClose = matchClose(masked, admin.valueOpen);
|
|
568
|
+
const empty = masked.slice(admin.valueOpen + 1, adminClose).trim() === "";
|
|
569
|
+
const insertAt = admin.valueOpen + 1;
|
|
570
|
+
return empty ? `${source.slice(0, insertAt)}\n${block.replace(/,$/, "")}\n${indentAt(source, admin.keyAt)}${source.slice(adminClose)}` : `${source.slice(0, insertAt)}\n${block}${source.slice(insertAt)}`;
|
|
571
|
+
}
|
|
572
|
+
const slugLineEnd = source.indexOf("\n", slugAt);
|
|
573
|
+
if (slugLineEnd === -1) return void 0;
|
|
574
|
+
const slugLine = masked.slice(slugAt, slugLineEnd);
|
|
575
|
+
if (!/,\s*$/.test(slugLine)) return void 0;
|
|
576
|
+
const indent = indentAt(source, slugAt);
|
|
577
|
+
const block = [
|
|
578
|
+
`${indent}admin: {`,
|
|
579
|
+
`${indent}${unit}entityViews: [`,
|
|
580
|
+
reindent(entry, indent + unit + unit),
|
|
581
|
+
`${indent}${unit}]`,
|
|
582
|
+
`${indent}},`
|
|
583
|
+
].join("\n");
|
|
584
|
+
return `${source.slice(0, slugLineEnd + 1)}${block}\n${source.slice(slugLineEnd + 1)}`;
|
|
585
|
+
}
|
|
586
|
+
function printHelp() {
|
|
587
|
+
console.log(`
|
|
588
|
+
${chalk.bold("rebase generate")} — scaffold code into this project
|
|
589
|
+
|
|
590
|
+
${chalk.bold("Usage")}
|
|
591
|
+
rebase generate view <slug> A screen of the admin, in the sidebar
|
|
592
|
+
rebase generate view <slug> --collection <slug> A tab on every record of a collection
|
|
593
|
+
|
|
594
|
+
${chalk.bold("What it writes")}
|
|
595
|
+
<frontend>/src/views/<Name>View.tsx The component, drawn with @rebasepro/ui
|
|
596
|
+
<frontend>/src/views/index.ts The screens list (AppView[]), wired into
|
|
597
|
+
<RebaseCMS views> in App.tsx on first run
|
|
598
|
+
With --collection, the collection file instead gains an admin.entityViews
|
|
599
|
+
entry that points at the component by path.
|
|
600
|
+
|
|
601
|
+
${chalk.bold("Options")}
|
|
602
|
+
--collection <slug> Register as a record tab of this collection
|
|
603
|
+
--name <label> Sidebar or tab label ${chalk.gray("(default: the slug in sentence case)")}
|
|
604
|
+
--icon <Lucide name> Sidebar icon for a screen ${chalk.gray("(default: LayoutDashboard)")}
|
|
605
|
+
--dry-run List what would change, and change nothing
|
|
606
|
+
--force Replace an existing view file
|
|
607
|
+
-h, --help Show this help
|
|
608
|
+
`.trim());
|
|
609
|
+
}
|
|
610
|
+
async function generateCommand(subcommand, rawArgs = []) {
|
|
611
|
+
if (subcommand === void 0 || subcommand === "--help" || subcommand !== "view" && wantsHelp(rawArgs)) {
|
|
612
|
+
printHelp();
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
switch (subcommand) {
|
|
616
|
+
case "view":
|
|
617
|
+
await generateViewCommand(rawArgs);
|
|
618
|
+
return;
|
|
619
|
+
default: throw new UsageError(`Unknown generate command "${subcommand}". Available: ${GENERATE_SUBCOMMANDS.join(", ")}. See \`rebase generate --help\`.`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async function generateViewCommand(rawArgs) {
|
|
623
|
+
if (wantsHelp(rawArgs)) {
|
|
624
|
+
printHelp();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const { flags, positionals } = parseCommandArgs({
|
|
628
|
+
spec: VIEW_FLAGS,
|
|
629
|
+
rawArgs,
|
|
630
|
+
commandWords: 2,
|
|
631
|
+
command: "generate view",
|
|
632
|
+
maxPositionals: 1
|
|
633
|
+
});
|
|
634
|
+
const slug = positionals[0];
|
|
635
|
+
if (!slug) throw new UsageError("Name the view: `rebase generate view <slug>`, e.g. `rebase generate view orders-board`.");
|
|
636
|
+
if (flags["--icon"] && flags["--collection"]) throw new UsageError("--icon is for a screen in the sidebar; a record tab (--collection) has no icon here.");
|
|
637
|
+
const projectRoot = requireProjectRoot();
|
|
638
|
+
const plan = planGenerateView({
|
|
639
|
+
projectRoot,
|
|
640
|
+
slug,
|
|
641
|
+
collection: flags["--collection"],
|
|
642
|
+
name: flags["--name"],
|
|
643
|
+
icon: flags["--icon"],
|
|
644
|
+
force: Boolean(flags["--force"])
|
|
645
|
+
});
|
|
646
|
+
const dryRun = Boolean(flags["--dry-run"]);
|
|
647
|
+
for (const file of plan.files) {
|
|
648
|
+
if (!dryRun) {
|
|
649
|
+
fs.mkdirSync(path.dirname(file.path), { recursive: true });
|
|
650
|
+
fs.writeFileSync(file.path, file.contents);
|
|
651
|
+
}
|
|
652
|
+
const verb = file.action === "create" ? "created" : file.action === "replace" ? "replaced" : "updated";
|
|
653
|
+
console.log(`${chalk.green("✓")} ${dryRun ? `would be ${verb}` : verb} ${path.relative(projectRoot, file.path)}`);
|
|
654
|
+
}
|
|
655
|
+
for (const step of plan.manualSteps) console.log(`${chalk.yellow("!")} ${step}`);
|
|
656
|
+
const where = plan.kind === "app" ? `/${slug} in the admin` : `a "${flags["--name"]?.trim() || viewLabel(slug)}" tab on each ${flags["--collection"]} record`;
|
|
657
|
+
console.log(chalk.gray(` ${plan.componentName} renders at ${where}. Edit ${path.relative(projectRoot, plan.viewFile)} to build the screen.`));
|
|
658
|
+
}
|
|
659
|
+
//#endregion
|
|
660
|
+
export { GENERATE_SUBCOMMANDS, generateCommand };
|
|
661
|
+
|
|
662
|
+
//# sourceMappingURL=generate-view-IFwCADvb.js.map
|