makersclaw 0.6.0 → 0.8.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 +56 -9
- package/package.json +1 -1
- package/r/annotate.json +4 -4
- package/r/approval-request.json +3 -3
- package/r/audio-player.json +3 -3
- package/r/auth-form.json +2 -2
- package/r/board.json +4 -4
- package/r/calendar-view.json +4 -4
- package/r/cart.json +3 -3
- package/r/chart.json +6 -6
- package/r/chat.json +3 -3
- package/r/code-editor.json +4 -4
- package/r/comment-box.json +4 -4
- package/r/contact-section.json +3 -3
- package/r/core.json +10 -4
- package/r/csv-import.json +4 -4
- package/r/data-table.json +3 -3
- package/r/diagram.json +3 -3
- package/r/doc-editor.json +4 -4
- package/r/doc-page.json +4 -4
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +3 -3
- package/r/flow-canvas.json +4 -4
- package/r/footer.json +3 -3
- package/r/gantt.json +4 -4
- package/r/job-panel.json +3 -3
- package/r/json-editor.json +4 -4
- package/r/map.json +5 -5
- package/r/menubar.json +2 -2
- package/r/mind-map.json +4 -4
- package/r/navigation-menu.json +2 -2
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +4 -4
- package/r/page-builder.json +6 -6
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +6 -6
- package/r/product-detail.json +3 -3
- package/r/product-grid.json +4 -4
- package/r/record-form.json +3 -3
- package/r/record-notes.json +5 -5
- package/r/registry.json +126 -121
- package/r/search-dialog.json +3 -3
- package/r/settings-list.json +3 -3
- package/r/setup-checklist.json +3 -3
- package/r/share-dialog.json +3 -3
- package/r/sidebar.json +3 -3
- package/r/signature-pad.json +3 -3
- package/r/sketch-canvas.json +5 -5
- package/r/spreadsheet.json +3 -3
- package/r/stat-cards.json +4 -4
- package/r/task-list.json +4 -4
- package/r/task-model.json +3 -3
- package/r/testimonials.json +2 -2
- package/r/transaction-list.json +3 -3
- package/r/whiteboard.json +4 -4
- package/registry/blocks/annotate/README.md +24 -12
- package/registry/blocks/approval-request/README.md +15 -8
- package/registry/blocks/audio-player/README.md +17 -10
- package/registry/blocks/board/README.md +13 -9
- package/registry/blocks/board/board.jsx +1 -1
- package/registry/blocks/calendar-view/README.md +28 -4
- package/registry/blocks/cart/README.md +14 -10
- package/registry/blocks/chart/README.md +14 -7
- package/registry/blocks/chart/cartesian.jsx +1 -1
- package/registry/blocks/chart/core.jsx +33 -23
- package/registry/blocks/chart/pie.jsx +1 -1
- package/registry/blocks/chat/README.md +47 -14
- package/registry/blocks/chat/demo.jsx +2 -2
- package/registry/blocks/code-editor/README.md +4 -0
- package/registry/blocks/code-editor/code-editor.jsx +1 -1
- package/registry/blocks/comment-box/README.md +20 -13
- package/registry/blocks/contact-section/README.md +7 -6
- package/registry/blocks/csv-import/README.md +34 -12
- package/registry/blocks/data-table/README.md +41 -8
- package/registry/blocks/diagram/diagram.jsx +1 -1
- package/registry/blocks/doc-editor/README.md +18 -6
- package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
- package/registry/blocks/doc-page/README.md +16 -6
- package/registry/blocks/file-upload/README.md +7 -1
- package/registry/blocks/flow-canvas/README.md +30 -12
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
- package/registry/blocks/footer/README.md +7 -4
- package/registry/blocks/gantt/README.md +18 -12
- package/registry/blocks/job-panel/README.md +1 -1
- package/registry/blocks/json-editor/README.md +16 -4
- package/registry/blocks/map/README.md +19 -3
- package/registry/blocks/map/map-view.jsx +1 -1
- package/registry/blocks/map/maplibre.js +1 -1
- package/registry/blocks/mind-map/README.md +20 -7
- package/registry/blocks/org-chart/README.md +14 -9
- package/registry/blocks/page-builder/README.md +14 -7
- package/registry/blocks/page-builder/demo.jsx +3 -3
- package/registry/blocks/page-builder/page-builder.jsx +1 -1
- package/registry/blocks/page-builder/page-view.jsx +1 -1
- package/registry/blocks/page-builder/page.js +1 -1
- package/registry/blocks/pdf-viewer/README.md +26 -21
- package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
- package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
- package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
- package/registry/blocks/product-detail/README.md +13 -7
- package/registry/blocks/product-detail/demo.jsx +1 -1
- package/registry/blocks/product-grid/README.md +12 -10
- package/registry/blocks/record-form/README.md +26 -9
- package/registry/blocks/record-notes/README.md +42 -30
- package/registry/blocks/search-dialog/README.md +38 -7
- package/registry/blocks/settings-list/README.md +29 -17
- package/registry/blocks/setup-checklist/README.md +12 -14
- package/registry/blocks/share-dialog/README.md +1 -1
- package/registry/blocks/sidebar/README.md +23 -13
- package/registry/blocks/signature-pad/README.md +15 -4
- package/registry/blocks/sketch-canvas/README.md +25 -10
- package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
- package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
- package/registry/blocks/spreadsheet/README.md +17 -8
- package/registry/blocks/stat-cards/README.md +17 -8
- package/registry/blocks/task-list/README.md +14 -11
- package/registry/blocks/task-model/README.md +3 -3
- package/registry/blocks/transaction-list/README.md +15 -7
- package/registry/blocks/whiteboard/README.md +34 -22
- package/src/cli.mjs +33 -6
- package/src/install.mjs +285 -26
- package/src/registry.mjs +109 -1
- package/templates/next/browser/app/auto-refresh.jsx +67 -0
- package/templates/next/browser/app/layout.jsx +94 -0
- package/templates/next/browser/app/page.jsx +60 -0
- package/templates/next/browser/app/s/[key]/page.jsx +95 -0
- package/templates/next/browser/lib/actions.js +16 -0
- package/templates/next/browser/lib/data.js +43 -0
- package/templates/next/files/README.md +1100 -89
- package/templates/next/files/app/frame.js +128 -4
- package/templates/next/files/app/shell.jsx +54 -37
- package/templates/next/files/check/run.mjs +1514 -45
- package/templates/next/files/components/ui/index.js +1 -0
- package/templates/next/files/components/ui/item.jsx +2 -2
- package/templates/next/files/components/ui/layout.jsx +307 -0
- package/templates/next/files/components/ui/notice.jsx +1 -1
- package/templates/next/files/components/ui/separator.jsx +1 -1
- package/templates/next/files/lib/actions.js +170 -51
- package/templates/next/files/lib/brain-server.js +96 -0
- package/templates/next/files/lib/brain.js +413 -0
- package/templates/next/files/lib/claw.js +15 -1
- package/templates/next/files/lib/data.js +119 -29
- package/templates/next/files/lib/db.js +97 -17
- package/templates/next/files/lib/door.js +680 -0
- package/templates/next/files/lib/hooks.js +71 -2
- package/templates/next/files/lib/platform.js +93 -4
- package/templates/next/files/next.config.mjs +36 -2
- package/templates/next/files/surfaces.js +3 -2
- package/templates/next/template.json +18 -3
- package/templates/next/files/schema.sql +0 -22
package/src/install.mjs
CHANGED
|
@@ -2,8 +2,15 @@
|
|
|
2
2
|
// result and prints nothing: src/cli.mjs says it, as text or as --json.
|
|
3
3
|
//
|
|
4
4
|
// Two files record what an app holds:
|
|
5
|
-
// makersclaw.json the template and version it was laid from,
|
|
6
|
-
// block with the version it came from
|
|
5
|
+
// makersclaw.json the template and version it was laid from, each
|
|
6
|
+
// block with the version it came from, where its
|
|
7
|
+
// tables live (`tables`: "own" or "public") and which
|
|
8
|
+
// fixed files it runs on (`mode`: "server" or
|
|
9
|
+
// "browser"). The rest of its keys are the app's
|
|
10
|
+
// (`brain`: the platform's declaration; `moves`: a
|
|
11
|
+
// store package's move map); every command keeps
|
|
12
|
+
// them. `tables` is never "moved": that is the
|
|
13
|
+
// platform's mode, and no command writes it
|
|
7
14
|
// check/pins.json sha256 of every fixed file, the template's and the
|
|
8
15
|
// blocks' — `npm run check` refuses one that differs
|
|
9
16
|
// and a third says which package versions it installs:
|
|
@@ -21,7 +28,7 @@ import { join } from "node:path";
|
|
|
21
28
|
import { hashFile, readJson, removeFile, sha256, walk, writeFile, writeJson } from "./fsutil.mjs";
|
|
22
29
|
import { LOCKFILE, LockError, deriveLock, parseLock } from "./lockfile.mjs";
|
|
23
30
|
import { NPM_LOCKFILE, PNPM_VERSION, installDeps } from "./pm.mjs";
|
|
24
|
-
import { BLOCK_DIR, loadBlocks, loadItems, loadTemplate, packageInfo, readmeOf, suggest, templateNames } from "./registry.mjs";
|
|
31
|
+
import { BASE_MODE, BLOCK_DIR, loadBlocks, loadItems, loadTemplate, modeDropScripts, modeFiles, modePackage, packageInfo, readmeOf, suggest, templateNames } from "./registry.mjs";
|
|
25
32
|
|
|
26
33
|
export const MANIFEST = "makersclaw.json";
|
|
27
34
|
export const PINS = "check/pins.json";
|
|
@@ -68,6 +75,7 @@ function readState(dir) {
|
|
|
68
75
|
|
|
69
76
|
function requireManifest(dir) {
|
|
70
77
|
const state = readState(dir);
|
|
78
|
+
if (state.manifest?.tables === MOVED) throw movedManifest();
|
|
71
79
|
if (!state.manifest) {
|
|
72
80
|
const hasApp = existsSync(join(dir, "package.json"));
|
|
73
81
|
throw new CliError(
|
|
@@ -80,6 +88,142 @@ function requireManifest(dir) {
|
|
|
80
88
|
return state;
|
|
81
89
|
}
|
|
82
90
|
|
|
91
|
+
/** Where an app's tables live: "own", its own schema (app_<name>), or
|
|
92
|
+
* "public", the workspace schema every app shared before 0.7.0. */
|
|
93
|
+
const TABLES = ["own", "public"];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The platform's third mode (claw's MIGRATE-SPEC M17 and §10): a legacy
|
|
97
|
+
* app whose tables the workspace's move put in its own schema, while its
|
|
98
|
+
* code still names public. The platform holds it (the app's blueprint,
|
|
99
|
+
* claw.app_roles); the app's makersclaw.json keeps "public" (or no key)
|
|
100
|
+
* until its rebuild writes "own". So no command writes "moved": `init
|
|
101
|
+
* --tables moved` is refused, and so is every command over a manifest
|
|
102
|
+
* that says it, rather than write it back.
|
|
103
|
+
*/
|
|
104
|
+
const MOVED = "moved";
|
|
105
|
+
const MOVED_WHY = "moved is the platform's mode, never a manifest's";
|
|
106
|
+
function movedManifest() {
|
|
107
|
+
return new CliError(
|
|
108
|
+
`${MANIFEST} says tables "moved": ${MOVED_WHY}`,
|
|
109
|
+
`set "tables" to "public" (or take the key out, for an app laid before 0.7.0): a moved app's code still names public, and its rebuild sets "own"`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A makersclaw.json a lay wrote. Every lay since 0.2.0 records the
|
|
115
|
+
* `version` it laid, so a manifest without one was written by someone
|
|
116
|
+
* else first: the platform writes its `brain` declaration into a
|
|
117
|
+
* work-first app's new manifest before that app's first `init`
|
|
118
|
+
* (APPS-SPEC 4.2). Such a manifest's keys are kept like any other's, but
|
|
119
|
+
* it is no record of an app from before 0.7.0.
|
|
120
|
+
*/
|
|
121
|
+
function writtenByLay(manifest) {
|
|
122
|
+
return Boolean(manifest) && Object.hasOwn(manifest, "version");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The `tables` init writes. It never flips an app: an app's tables move
|
|
127
|
+
* from public to their own schema only with its workspace (the platform's
|
|
128
|
+
* move), never because a refresh ran `init --force`. So an app's
|
|
129
|
+
* makersclaw.json keeps its value — an absent key in a manifest a lay
|
|
130
|
+
* wrote is an app from before 0.7.0, on public — and a `--tables` that
|
|
131
|
+
* says otherwise is refused. Where no lay wrote a makersclaw.json (there
|
|
132
|
+
* is none, or it holds only the platform's keys) and it names no tables,
|
|
133
|
+
* there is no record to keep: a new app is "own" unless told, and an app
|
|
134
|
+
* laid before makersclaw kept one is "public" unless told (the platform,
|
|
135
|
+
* which holds the truth, always tells).
|
|
136
|
+
*/
|
|
137
|
+
function tablesFor(dir, before, asked) {
|
|
138
|
+
if (asked === MOVED) throw new CliError(`--tables moved: ${MOVED_WHY}`, `leave --tables out: a moved app keeps its makersclaw.json's "public" until its rebuild sets "own"`);
|
|
139
|
+
if (before.manifest?.tables === MOVED) throw movedManifest();
|
|
140
|
+
if (asked !== undefined && !TABLES.includes(asked)) {
|
|
141
|
+
throw new CliError(`--tables takes ${TABLES.join(" or ")}, not "${asked}"`, "own: the app's tables in its own schema (every new app) · public: an app from before 0.7.0");
|
|
142
|
+
}
|
|
143
|
+
const named = Boolean(before.manifest) && Object.hasOwn(before.manifest, "tables");
|
|
144
|
+
if (!named && !writtenByLay(before.manifest)) return asked ?? (existsSync(join(dir, "package.json")) ? "public" : "own");
|
|
145
|
+
const had = named ? before.manifest.tables : "public";
|
|
146
|
+
if (!TABLES.includes(had)) {
|
|
147
|
+
throw new CliError(`${MANIFEST} says tables ${JSON.stringify(had)}; an app's tables are ${TABLES.join(" or ")}`, `set "tables" in ${MANIFEST} to the one the platform holds for this app`);
|
|
148
|
+
}
|
|
149
|
+
if (asked !== undefined && asked !== had) {
|
|
150
|
+
throw new CliError(
|
|
151
|
+
(named ? `this app's tables are ${had}, not ${asked}` : `${MANIFEST} names no tables, so this app's are public (laid before 0.7.0), not ${asked}`) +
|
|
152
|
+
(had === "public" ? ": an app's tables move from public to their own schema only with its workspace" : ": an app on its own schema never goes back to public"),
|
|
153
|
+
`leave --tables out, or pass --tables ${had}` + (named ? "" : `; if its tables are already in its own schema, put "tables": "own" back in ${MANIFEST}`),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return had;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Which fixed files an app runs on: "server", a Next server in the
|
|
160
|
+
* workspace (files/ as it is; an absent key, every app before 0.7.0), or
|
|
161
|
+
* "browser", code that runs in the member's browser: files the platform
|
|
162
|
+
* serves, reading and writing through its data door (template.json
|
|
163
|
+
* `modes.browser`). */
|
|
164
|
+
const MODES = [BASE_MODE, "browser"];
|
|
165
|
+
|
|
166
|
+
/** The app's mode, from its makersclaw.json. Anything but the two is
|
|
167
|
+
* refused: which fixed files to lay depends on it. */
|
|
168
|
+
function modeOf(manifest) {
|
|
169
|
+
const mode = manifest && Object.hasOwn(manifest, "mode") ? manifest.mode : BASE_MODE;
|
|
170
|
+
if (!MODES.includes(mode)) {
|
|
171
|
+
throw new CliError(`${MANIFEST} says mode ${JSON.stringify(mode)}; an app's mode is ${MODES.join(" or ")}`, `makersclaw mode <${MODES.join("|")}> sets the one the platform holds for this app`);
|
|
172
|
+
}
|
|
173
|
+
return mode;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The `mode` init writes. Like `tables`, init never changes an app's: a
|
|
178
|
+
* refresh is claw's `init --force` over the app it has, and a flip there
|
|
179
|
+
* would turn a server app into a browser one whose Server Actions then fail
|
|
180
|
+
* its export. So a manifest that names a mode keeps it, whoever wrote it,
|
|
181
|
+
* and so does one a lay wrote (absent: server); a --mode that differs is
|
|
182
|
+
* refused, and `makersclaw mode` is the one switch. A new manifest takes
|
|
183
|
+
* --mode's value, and with none no key: an absent mode is server, the
|
|
184
|
+
* CLI's own default, because outside Makersclaw no door answers a browser
|
|
185
|
+
* app. { mode, key }: the app's mode, and the value to write (undefined:
|
|
186
|
+
* the key stays as it is).
|
|
187
|
+
*/
|
|
188
|
+
function modeFor(before, asked) {
|
|
189
|
+
if (asked !== undefined && !MODES.includes(asked)) {
|
|
190
|
+
throw new CliError(`--mode takes ${MODES.join(" or ")}, not "${asked}"`, "server: a Next server in the workspace (the default) · browser: its code runs in the member's browser, and every read and write goes through the platform's data door");
|
|
191
|
+
}
|
|
192
|
+
const named = Boolean(before.manifest) && Object.hasOwn(before.manifest, "mode");
|
|
193
|
+
if (!named && !writtenByLay(before.manifest)) return { mode: asked ?? BASE_MODE, key: asked };
|
|
194
|
+
const had = modeOf(before.manifest);
|
|
195
|
+
if (asked !== undefined && asked !== had) {
|
|
196
|
+
throw new CliError(
|
|
197
|
+
`this app is a ${had} app, not ${asked}: an app changes mode only with makersclaw mode`,
|
|
198
|
+
`leave --mode out, or run makersclaw mode ${asked}: it lays that mode's fixed files and says what the app's own code must change`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return { mode: had, key: undefined };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** A browser app's tables are its own: the door serves an app's own schema
|
|
205
|
+
* and nothing else, so a browser app on public would read nothing. `how`
|
|
206
|
+
* says where the pair came from, for the refusal's words. */
|
|
207
|
+
function requireOwnTables(mode, tables, how) {
|
|
208
|
+
if (mode !== "browser" || tables === "own") return;
|
|
209
|
+
const why = "a browser app reads and writes only through the platform's data door, which serves an app's own schema and nothing else";
|
|
210
|
+
if (how === "flags") throw new CliError(`a browser app's tables are own, not ${tables}: ${why}`, "leave --tables out: --mode browser writes tables own");
|
|
211
|
+
if (how === "switch") {
|
|
212
|
+
throw new CliError(
|
|
213
|
+
`${tables === undefined ? `${MANIFEST} names no tables, so this app's are public (laid before 0.7.0)` : `this app's tables are ${tables}`}, not own: ${why}`,
|
|
214
|
+
"an app's tables move to their own schema only with its workspace (the platform's move); until then it stays a server app",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
throw new CliError(`${MANIFEST} says mode browser and tables ${tables}: ${why}`, `if its tables are in its own schema, set "tables": "own"; if not, makersclaw mode server lays it as a server app`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The files the template lays in this mode (registry modeFiles). */
|
|
221
|
+
function modeFilesOrFail(tpl, mode) {
|
|
222
|
+
const files = modeFiles(tpl, mode);
|
|
223
|
+
if (!files) throw new CliError(`template ${tpl.name} has no ${mode} mode in makersclaw ${version()}`, `its modes: ${[BASE_MODE, ...Object.keys(tpl.modes ?? {})].join(", ")}`);
|
|
224
|
+
return files;
|
|
225
|
+
}
|
|
226
|
+
|
|
83
227
|
function loadTemplateOrFail(name) {
|
|
84
228
|
const tpl = loadTemplate(name);
|
|
85
229
|
if (!tpl) throw new CliError(`this makersclaw has no template "${name}"`, `templates: ${templateNames().join(", ")}`);
|
|
@@ -87,8 +231,11 @@ function loadTemplateOrFail(name) {
|
|
|
87
231
|
}
|
|
88
232
|
|
|
89
233
|
/** package.json: the template's scripts and dependencies win for the keys
|
|
90
|
-
* it names; every other key is the app's.
|
|
91
|
-
|
|
234
|
+
* it names; every other key is the app's. `dropScripts` are the scripts
|
|
235
|
+
* the app's mode has none of (registry modeDropScripts): removed whatever
|
|
236
|
+
* they say, so a server app switched to browser loses its `start`; a
|
|
237
|
+
* removal is a change `to: null`. */
|
|
238
|
+
function mergePackage(app, tpl, dropScripts = []) {
|
|
92
239
|
const out = { ...app };
|
|
93
240
|
const changes = [];
|
|
94
241
|
for (const key of ["scripts", "dependencies", "devDependencies"]) {
|
|
@@ -100,6 +247,12 @@ function mergePackage(app, tpl) {
|
|
|
100
247
|
out[key][name] = value;
|
|
101
248
|
}
|
|
102
249
|
}
|
|
250
|
+
for (const name of dropScripts) {
|
|
251
|
+
if (out.scripts?.[name] === undefined) continue;
|
|
252
|
+
out.scripts = { ...out.scripts };
|
|
253
|
+
changes.push({ key: "scripts", name, from: out.scripts[name], to: null });
|
|
254
|
+
delete out.scripts[name];
|
|
255
|
+
}
|
|
103
256
|
return { out, changes };
|
|
104
257
|
}
|
|
105
258
|
|
|
@@ -149,11 +302,12 @@ function writeLock(dir, cut, written) {
|
|
|
149
302
|
|
|
150
303
|
const lockSummary = (cut) => (cut ? { covered: cut.covered.length, carried: cut.carried, left: cut.left } : null);
|
|
151
304
|
|
|
152
|
-
/** Paths a template may remove when a newer version stops shipping them
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
305
|
+
/** Paths a template may remove when a newer version stops shipping them,
|
|
306
|
+
* or the app's mode stops laying them: its fixed files (the base's, so
|
|
307
|
+
* the ones a mode drops too), the ones it names as retired (middleware.js
|
|
308
|
+
* since 0.6.0: Next 16 fails a build that has it beside proxy.js), and
|
|
309
|
+
* the kit, parts and block folders. Anything else a pins.json names is
|
|
310
|
+
* left alone — it is not ours to delete. */
|
|
157
311
|
function ownedPath(path, tpl) {
|
|
158
312
|
if (/^components\/(?:ui|parts|blocks)\//.test(path) || /^lib\/[^/]+\.js$/.test(path)) return true;
|
|
159
313
|
if ((tpl.retired ?? []).includes(path)) return true;
|
|
@@ -162,12 +316,24 @@ function ownedPath(path, tpl) {
|
|
|
162
316
|
|
|
163
317
|
/**
|
|
164
318
|
* Lay a template — and the blocks the app already has — over `dir`: the
|
|
165
|
-
* one routine behind init, init --force and
|
|
166
|
-
* are replaced; starters are laid only when missing; package.json is
|
|
319
|
+
* one routine behind init, init --force, update and mode. Fixed files and
|
|
320
|
+
* docs are replaced; starters are laid only when missing; package.json is
|
|
167
321
|
* merged; blocks come again from this version; what an older version
|
|
168
|
-
* pinned and this one no longer ships is removed.
|
|
322
|
+
* pinned and this one no longer ships is removed. makersclaw.json is
|
|
323
|
+
* rewritten over what it held: `template`, `version` and `blocks` are
|
|
324
|
+
* this lay's, `tables` is init's (tablesFor; update leaves it as it was),
|
|
325
|
+
* `mode` is a new manifest's init's or `makersclaw mode`'s (modeFor), and
|
|
326
|
+
* every other key — the platform's `brain` declaration, one a later
|
|
327
|
+
* version adds — survives a refresh and an update. Until 0.7.0 it was
|
|
328
|
+
* rewritten as exactly those three, and a declaration was lost.
|
|
329
|
+
*
|
|
330
|
+
* The files are the app's mode's (modeFiles): a browser app gets the
|
|
331
|
+
* browser overlay's copy of a fixed file where there is one, and none of
|
|
332
|
+
* the files browser mode drops. The pins follow: a file the mode does not
|
|
333
|
+
* lay is not pinned, and one that was pinned is removed as a retired one
|
|
334
|
+
* is, so a switch of mode leaves only the new mode's fixed files.
|
|
169
335
|
*/
|
|
170
|
-
function lay(dir, tpl, before) {
|
|
336
|
+
function lay(dir, tpl, before, { tables, mode } = {}) {
|
|
171
337
|
const v = version();
|
|
172
338
|
const written = [];
|
|
173
339
|
const kept = [];
|
|
@@ -177,14 +343,22 @@ function lay(dir, tpl, before) {
|
|
|
177
343
|
const pins = {};
|
|
178
344
|
const fresh = !existsSync(join(dir, "package.json"));
|
|
179
345
|
|
|
180
|
-
|
|
346
|
+
// Decided before a file is written: a refused mode leaves the app as it was.
|
|
347
|
+
const keys = { ...(tables ? { tables } : {}), ...(mode ? { mode } : {}) };
|
|
348
|
+
const appMode = modeOf({ ...before.manifest, ...keys });
|
|
349
|
+
requireOwnTables(appMode, keys.tables ?? before.manifest?.tables ?? "public", "manifest");
|
|
350
|
+
const files = modeFilesOrFail(tpl, appMode);
|
|
351
|
+
|
|
352
|
+
for (const f of files) {
|
|
181
353
|
const to = join(dir, f.path);
|
|
182
354
|
if (f.class === "starter" && existsSync(to)) {
|
|
183
355
|
kept.push(f.path);
|
|
184
356
|
continue;
|
|
185
357
|
}
|
|
358
|
+
// package.json is the mode's (registry modePackage): a browser app's
|
|
359
|
+
// has no start script, whether it is laid new or merged.
|
|
186
360
|
if (f.class === "merged" && existsSync(to)) {
|
|
187
|
-
const { out, changes } = mergePackage(readJson(to),
|
|
361
|
+
const { out, changes } = mergePackage(readJson(to), modePackage(tpl, appMode), modeDropScripts(tpl, appMode));
|
|
188
362
|
if (changes.length) {
|
|
189
363
|
writeJson(to, out);
|
|
190
364
|
written.push(f.path);
|
|
@@ -192,6 +366,11 @@ function lay(dir, tpl, before) {
|
|
|
192
366
|
pkg = changes;
|
|
193
367
|
continue;
|
|
194
368
|
}
|
|
369
|
+
if (f.class === "merged") {
|
|
370
|
+
writeJson(to, modePackage(tpl, appMode));
|
|
371
|
+
written.push(f.path);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
195
374
|
const bytes = readFileSync(f.from);
|
|
196
375
|
const hash = sha256(bytes);
|
|
197
376
|
if (hashFile(to) !== hash) {
|
|
@@ -259,12 +438,24 @@ function lay(dir, tpl, before) {
|
|
|
259
438
|
removed.push(p);
|
|
260
439
|
}
|
|
261
440
|
}
|
|
441
|
+
// A file the mode leaves out that no pin names is not the template's to
|
|
442
|
+
// delete (the app wrote it, or a hand laid it), but it is in the way:
|
|
443
|
+
// under output: "export" Next ignores a proxy.js, and lib/db.js is pg.
|
|
444
|
+
const laid = new Set(files.map((f) => f.path));
|
|
445
|
+
for (const f of tpl.files) {
|
|
446
|
+
if (f.class !== "fixed" || laid.has(f.path) || f.path in before.pins || !existsSync(join(dir, f.path))) continue;
|
|
447
|
+
notes.push(`${f.path}: ${appMode} mode lays none, and this one is not pinned — left alone; delete it unless the app's own code needs it`);
|
|
448
|
+
}
|
|
262
449
|
|
|
263
450
|
writeJson(join(dir, PINS), sortKeys(pins));
|
|
264
|
-
|
|
451
|
+
const manifest = { ...before.manifest, template: tpl.name, version: v, blocks: sortKeys(blocks), ...keys };
|
|
452
|
+
writeJson(join(dir, MANIFEST), manifest);
|
|
265
453
|
return {
|
|
266
454
|
template: tpl.name,
|
|
267
455
|
version: v,
|
|
456
|
+
// An absent key is an app from before 0.7.0: its tables are in public.
|
|
457
|
+
tables: manifest.tables ?? "public",
|
|
458
|
+
mode: appMode,
|
|
268
459
|
from: before.manifest?.version ?? null,
|
|
269
460
|
written,
|
|
270
461
|
kept,
|
|
@@ -278,14 +469,14 @@ function lay(dir, tpl, before) {
|
|
|
278
469
|
};
|
|
279
470
|
}
|
|
280
471
|
|
|
281
|
-
/** `init [dir] [-t next] [--force]` */
|
|
282
|
-
export function init(dir, { template = "next", force = false } = {}) {
|
|
472
|
+
/** `init [dir] [-t next] [--force] [--tables own|public] [--mode browser|server]` */
|
|
473
|
+
export function init(dir, { template = "next", force = false, tables, mode: askedMode } = {}) {
|
|
283
474
|
const tpl = loadTemplateOrFail(template);
|
|
284
475
|
const before = readState(dir);
|
|
285
476
|
if (existsSync(join(dir, "package.json")) && !force) {
|
|
286
477
|
throw new CliError(
|
|
287
478
|
`${dir} already holds an app`,
|
|
288
|
-
before.manifest
|
|
479
|
+
writtenByLay(before.manifest)
|
|
289
480
|
? "makersclaw update lays this version over it"
|
|
290
481
|
: "makersclaw init --force lays the template over it: fixed files and docs are replaced, your own files stay",
|
|
291
482
|
);
|
|
@@ -293,7 +484,13 @@ export function init(dir, { template = "next", force = false } = {}) {
|
|
|
293
484
|
if (before.manifest?.template && before.manifest.template !== template) {
|
|
294
485
|
throw new CliError(`this app was laid from template "${before.manifest.template}", not "${template}"`, `makersclaw init --force -t ${before.manifest.template}`);
|
|
295
486
|
}
|
|
296
|
-
|
|
487
|
+
// Decided before a file is written: a refused --tables or --mode leaves
|
|
488
|
+
// the app as it was. A new browser app's tables are own unless --tables
|
|
489
|
+
// says otherwise, and then it is refused.
|
|
490
|
+
const m = modeFor(before, askedMode);
|
|
491
|
+
const t = tablesFor(dir, before, tables ?? (m.key === "browser" ? "own" : undefined));
|
|
492
|
+
if (tables !== undefined) requireOwnTables(m.mode, t, "flags");
|
|
493
|
+
return lay(dir, tpl, before, { tables: t, mode: m.key });
|
|
297
494
|
}
|
|
298
495
|
|
|
299
496
|
/** `update [--major] [--adopt]` — this version's fixed files, docs and
|
|
@@ -330,6 +527,57 @@ export function update(dir, { major = false, adopt = false } = {}) {
|
|
|
330
527
|
return lay(dir, tpl, before);
|
|
331
528
|
}
|
|
332
529
|
|
|
530
|
+
/** What an app's own code must change when it moves to a mode. The
|
|
531
|
+
* template lays the fixed files; the surfaces are the app's, and move in
|
|
532
|
+
* the same build (claw's STATIC-SPEC §3, "What the Builder is told"). */
|
|
533
|
+
export const MODE_CHANGES = {
|
|
534
|
+
browser: [
|
|
535
|
+
'every surface a client component ("use client") that reads through the lib/data hooks (useRows, useRow, useCount, useCountBy, useActivity) and writes through lib/actions; a hook compares its options on every render (a time in filter or since to the minute), so anything else worked out per render (a Date.now() number) is worked out once, with useMemo',
|
|
536
|
+
"view state (?row=) from useSearchParams(): a browser app's page is a file built once and has no request, so a surface gets no searchParams",
|
|
537
|
+
"a write of more than one row is a function in the app's schema ((args jsonb) returns jsonb), called with runAction and named in references/actions.md",
|
|
538
|
+
'no server code of its own: no "use server" module, no route.js under app/, no proxy.js or middleware.js, no import of next/headers, next/cache, next/server, pg or lib/db',
|
|
539
|
+
"no export const dynamic, revalidate, fetchCache, runtime or dynamicParams in the app's own files",
|
|
540
|
+
"no process.env in the app's own code but CLAW_BASE_PATH, CLAW_PRODUCT and NEXT_PUBLIC_*: a prerendered page prints what it reads into a file anyone who opens the app can fetch",
|
|
541
|
+
],
|
|
542
|
+
server: [
|
|
543
|
+
"lib/data.js is the server's again (pg, next/headers): a client surface takes its hooks from lib/door.js, or becomes a server component that calls loadRows",
|
|
544
|
+
"lib/actions.js is Server Actions again: a client surface calls them as it did",
|
|
545
|
+
"server code the app needs (a route handler, a Server Action of its own) may come back",
|
|
546
|
+
],
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* `mode <browser|server>` — the one switch of an app's mode. It sets
|
|
551
|
+
* `mode` in makersclaw.json, lays that mode's fixed files, removes the
|
|
552
|
+
* other mode's (as update removes a retired file), and says what the
|
|
553
|
+
* app's own code must change. Browser needs `tables: "own"`: the door
|
|
554
|
+
* serves only an app's own schema. The app must be on this version, as
|
|
555
|
+
* for add: the switch lays this version's fixed files.
|
|
556
|
+
*/
|
|
557
|
+
export function mode(dir, to) {
|
|
558
|
+
if (to === undefined) throw new CliError("name a mode: browser or server", "makersclaw mode browser · makersclaw mode server");
|
|
559
|
+
if (!MODES.includes(to)) throw new CliError(`an app's mode is ${MODES.join(" or ")}, not "${to}"`, "server: a Next server in the workspace · browser: its code runs in the member's browser, and every read and write goes through the platform's data door");
|
|
560
|
+
const before = requireManifest(dir);
|
|
561
|
+
const { manifest } = before;
|
|
562
|
+
const v = version();
|
|
563
|
+
if (manifest.version !== v) {
|
|
564
|
+
throw new CliError(
|
|
565
|
+
`this app is on makersclaw ${manifest.version}; this is ${v}, and a switch of mode lays this version's fixed files`,
|
|
566
|
+
newer(manifest.version, v)
|
|
567
|
+
? `npx makersclaw@${manifest.version} mode ${to}`
|
|
568
|
+
: compatible(manifest.version, v)
|
|
569
|
+
? `makersclaw update first (${manifest.version} → ${v} is compatible), then mode ${to}`
|
|
570
|
+
: `makersclaw update --major first (makersclaw diff shows what changes), then mode ${to}`,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
const tpl = loadTemplateOrFail(manifest.template);
|
|
574
|
+
// Not checked: a mode this CLI cannot read is what the switch mends.
|
|
575
|
+
const was = Object.hasOwn(manifest, "mode") ? manifest.mode : BASE_MODE;
|
|
576
|
+
requireOwnTables(to, manifest.tables, "switch");
|
|
577
|
+
const r = lay(dir, tpl, before, { mode: to });
|
|
578
|
+
return { ...r, was, changed: was !== to, change: MODE_CHANGES[to] };
|
|
579
|
+
}
|
|
580
|
+
|
|
333
581
|
function importFor(item) {
|
|
334
582
|
if (item.type === "block") {
|
|
335
583
|
const names = item.exports?.length ? item.exports.join(", ") : "…";
|
|
@@ -356,6 +604,9 @@ export function add(dir, names) {
|
|
|
356
604
|
}
|
|
357
605
|
const tpl = loadTemplateOrFail(manifest.template);
|
|
358
606
|
const items = loadItems(tpl);
|
|
607
|
+
// A kit component is restored from the app's mode's copy of its file.
|
|
608
|
+
const appMode = modeOf(manifest);
|
|
609
|
+
const laid = new Map(modeFilesOrFail(tpl, appMode).map((f) => [f.path, f.from]));
|
|
359
610
|
const order = [];
|
|
360
611
|
const seen = new Set();
|
|
361
612
|
const visit = (name, by) => {
|
|
@@ -380,7 +631,9 @@ export function add(dir, names) {
|
|
|
380
631
|
const files = [];
|
|
381
632
|
let changed = 0;
|
|
382
633
|
for (const f of item.files) {
|
|
383
|
-
const
|
|
634
|
+
const from = item.type === "block" ? f.from : laid.get(f.to);
|
|
635
|
+
if (!from) throw new CliError(`${item.name}: ${f.to} is not laid in ${appMode} mode`);
|
|
636
|
+
const bytes = readFileSync(from);
|
|
384
637
|
const hash = sha256(bytes);
|
|
385
638
|
if (hashFile(join(dir, f.to)) !== hash) {
|
|
386
639
|
writeFile(join(dir, f.to), bytes);
|
|
@@ -490,6 +743,8 @@ export function diff(dir) {
|
|
|
490
743
|
const { manifest, pins } = requireManifest(dir);
|
|
491
744
|
const tpl = loadTemplateOrFail(manifest.template);
|
|
492
745
|
const v = version();
|
|
746
|
+
const appMode = modeOf(manifest);
|
|
747
|
+
const files = modeFilesOrFail(tpl, appMode);
|
|
493
748
|
const entries = [];
|
|
494
749
|
const statusOf = (path, have, want, pinned) => {
|
|
495
750
|
if (have === want) return null;
|
|
@@ -498,12 +753,14 @@ export function diff(dir) {
|
|
|
498
753
|
return "changed";
|
|
499
754
|
};
|
|
500
755
|
|
|
501
|
-
|
|
756
|
+
// The app's mode's files: update lays those, and removes a pinned file
|
|
757
|
+
// the mode does not lay ("dropped").
|
|
758
|
+
for (const f of files) {
|
|
502
759
|
if (f.class !== "fixed" && f.class !== "docs") continue;
|
|
503
760
|
const status = statusOf(f.path, hashFile(join(dir, f.path)), sha256(readFileSync(f.from)), f.class === "fixed" ? pins[f.path] : null);
|
|
504
761
|
if (status) entries.push({ item: "template", path: f.path, status });
|
|
505
762
|
}
|
|
506
|
-
const shipped = new Set(
|
|
763
|
+
const shipped = new Set(files.map((f) => f.path));
|
|
507
764
|
for (const p of Object.keys(pins)) {
|
|
508
765
|
if (!p.startsWith(`${BLOCK_DIR}/`) && !shipped.has(p) && ownedPath(p, tpl)) entries.push({ item: "template", path: p, status: "dropped" });
|
|
509
766
|
}
|
|
@@ -527,13 +784,14 @@ export function diff(dir) {
|
|
|
527
784
|
|
|
528
785
|
const pkgPath = join(dir, "package.json");
|
|
529
786
|
const tplPkg = tpl.files.find((f) => f.class === "merged");
|
|
530
|
-
const
|
|
787
|
+
const drop = modeDropScripts(tpl, appMode);
|
|
788
|
+
const pkg = existsSync(pkgPath) && tplPkg ? mergePackage(readJson(pkgPath), modePackage(tpl, appMode), drop).changes : [];
|
|
531
789
|
// …and the installed blocks' npm packages, which update lays with them.
|
|
532
790
|
const blockDeps = Object.assign({}, ...Object.keys(manifest.blocks ?? {}).map((n) => known.find((b) => b.name === n)?.dependencies ?? {}));
|
|
533
791
|
if (existsSync(pkgPath) && Object.keys(blockDeps).length) pkg.push(...mergePackage(readJson(pkgPath), { dependencies: blockDeps }).changes);
|
|
534
792
|
// …and the lock update would cut for the package.json it would leave.
|
|
535
793
|
if (existsSync(pkgPath) && tplPkg) {
|
|
536
|
-
const after = mergePackage(mergePackage(readJson(pkgPath),
|
|
794
|
+
const after = mergePackage(mergePackage(readJson(pkgPath), modePackage(tpl, appMode), drop).out, { dependencies: blockDeps }).out;
|
|
537
795
|
const cut = cutLock(dir, tpl, after);
|
|
538
796
|
if (cut && cut.text !== cut.before) entries.push({ item: "template", path: LOCKFILE, status: cut.before === null ? "new" : "changed" });
|
|
539
797
|
}
|
|
@@ -541,6 +799,7 @@ export function diff(dir) {
|
|
|
541
799
|
from: manifest.version,
|
|
542
800
|
to: v,
|
|
543
801
|
compatible: compatible(manifest.version, v) && !newer(manifest.version, v),
|
|
802
|
+
mode: appMode,
|
|
544
803
|
entries,
|
|
545
804
|
package: pkg,
|
|
546
805
|
blocks: manifest.blocks ?? {},
|
package/src/registry.mjs
CHANGED
|
@@ -56,6 +56,113 @@ export const FILE_CLASSES = ["fixed", "docs", "starter", "merged"];
|
|
|
56
56
|
// Next 16 renamed proxy.js). `update` removes them from an app that has
|
|
57
57
|
// them pinned; nothing lays them.
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The mode files/ is: a Next server in the workspace. Every other mode is
|
|
61
|
+
* files/ with some fixed files replaced and some left out (template.json
|
|
62
|
+
* `modes`), and an app says which in makersclaw.json `mode` (absent: this
|
|
63
|
+
* one). Browser mode (0.7.0, claw's STATIC-SPEC §3): the app's code runs
|
|
64
|
+
* in the member's browser, from files the platform serves, and every read
|
|
65
|
+
* and write goes through the platform's data door. So it lays no
|
|
66
|
+
* proxy.js, no /claw-alive, no pg (lib/db.js) and no server reads of the
|
|
67
|
+
* brain (lib/brain-server.js).
|
|
68
|
+
*/
|
|
69
|
+
export const BASE_MODE = "server";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A mode's overlay and drop list, checked against the base's files. Pure:
|
|
73
|
+
* names in, the first thing wrong thrown, nothing returned. `base` is the
|
|
74
|
+
* base's files ({ path, class }), `overlay` the paths under the mode's
|
|
75
|
+
* overlay folder, `drop` the base paths the mode leaves out.
|
|
76
|
+
* - No file is both dropped and overlaid: a mode lays a file or leaves
|
|
77
|
+
* it out, never both.
|
|
78
|
+
* - Each overlay file is a fixed file of the base. The overlay replaces
|
|
79
|
+
* what the check pins (and update lays again); it adds no file, and
|
|
80
|
+
* never a starter or a doc, which are the app's or read only.
|
|
81
|
+
* - Each dropped file is a fixed file of the base: a drop the base does
|
|
82
|
+
* not ship drops nothing, without a word, and only a pinned file is
|
|
83
|
+
* the template's to remove from an app.
|
|
84
|
+
* - Each dropped script (`dropScripts`, 0.8.0) is a script of the base's
|
|
85
|
+
* package.json (`scripts`, its names), for the same reason.
|
|
86
|
+
*/
|
|
87
|
+
export function checkMode(template, mode, { base, overlay, drop, scripts = [], dropScripts = [] }) {
|
|
88
|
+
const fixed = new Set(base.filter((f) => f.class === "fixed").map((f) => f.path));
|
|
89
|
+
const dropped = new Set(drop);
|
|
90
|
+
for (const path of overlay) {
|
|
91
|
+
if (dropped.has(path)) throw new Error(`template ${template}: mode ${mode} both drops and overlays ${path} — a mode lays a file or leaves it out, not both`);
|
|
92
|
+
if (!fixed.has(path)) {
|
|
93
|
+
throw new Error(`template ${template}: mode ${mode} overlays ${path}, which is not a fixed file of files/ — an overlay replaces a fixed file and adds none`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const path of drop) {
|
|
97
|
+
if (!fixed.has(path)) throw new Error(`template ${template}: mode ${mode} drops ${path}, which is not a fixed file of files/ — it would drop nothing`);
|
|
98
|
+
}
|
|
99
|
+
for (const name of dropScripts) {
|
|
100
|
+
if (!scripts.includes(name)) throw new Error(`template ${template}: mode ${mode} drops the script ${name}, which files/package.json has not — it would drop nothing`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A mode's overlay folder: beside files/, a plain name. */
|
|
105
|
+
const OVERLAY = /^[a-z]+$/;
|
|
106
|
+
|
|
107
|
+
function loadModes(name, dir, meta, base) {
|
|
108
|
+
const modes = {};
|
|
109
|
+
for (const [mode, spec] of Object.entries(meta.modes ?? {})) {
|
|
110
|
+
if (!NAME.test(mode) || mode === BASE_MODE) throw new Error(`template ${name}: mode "${mode}" — ${BASE_MODE} is files/ itself, and a mode is named lowercase and dashed`);
|
|
111
|
+
const folder = spec?.overlay;
|
|
112
|
+
if (typeof folder !== "string" || !OVERLAY.test(folder) || folder === "files") {
|
|
113
|
+
throw new Error(`template ${name}: mode ${mode} names its overlay ${JSON.stringify(folder)} — a plain folder name beside files/`);
|
|
114
|
+
}
|
|
115
|
+
const root = join(dir, folder);
|
|
116
|
+
// A folder that is not there is an empty overlay: the mode only leaves
|
|
117
|
+
// files out. Browser mode's own files come after its drop list does.
|
|
118
|
+
const overlay = existsSync(root) ? walk(root) : [];
|
|
119
|
+
const drop = [...(spec.drop ?? [])];
|
|
120
|
+
const dropScripts = [...(spec.dropScripts ?? [])];
|
|
121
|
+
const merged = base.find((f) => f.class === "merged");
|
|
122
|
+
const scripts = Object.keys((merged && readJson(merged.from)?.scripts) ?? {});
|
|
123
|
+
checkMode(name, mode, { base, overlay, drop, scripts, dropScripts });
|
|
124
|
+
modes[mode] = { root, drop, dropScripts, files: overlay.map((path) => ({ path, class: "fixed", from: join(root, path) })) };
|
|
125
|
+
}
|
|
126
|
+
return modes;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The files a template lays in a mode: the base's, less the mode's drops,
|
|
131
|
+
* with the mode's copy in place of the base's where its overlay has one.
|
|
132
|
+
* The base mode is files/ as it is. null for a mode the template has not.
|
|
133
|
+
*/
|
|
134
|
+
export function modeFiles(template, mode = BASE_MODE) {
|
|
135
|
+
if (mode === BASE_MODE) return template.files;
|
|
136
|
+
const m = template.modes?.[mode];
|
|
137
|
+
if (!m) return null;
|
|
138
|
+
const over = new Map(m.files.map((f) => [f.path, f]));
|
|
139
|
+
const drop = new Set(m.drop);
|
|
140
|
+
return template.files.filter((f) => !drop.has(f.path)).map((f) => over.get(f.path) ?? f);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The scripts an app in this mode has none of (template.json
|
|
145
|
+
* `modes.<mode>.dropScripts`). Browser mode drops `start` (0.8.0): a
|
|
146
|
+
* browser app is built once and served as files, nothing ever starts it,
|
|
147
|
+
* and the store refuses a browser app's package.json that names one (its
|
|
148
|
+
* own-rules.mjs, browserScriptRefusals). Until 0.8.0 init laid the base's
|
|
149
|
+
* `start` into every app.
|
|
150
|
+
*/
|
|
151
|
+
export function modeDropScripts(template, mode = BASE_MODE) {
|
|
152
|
+
return mode === BASE_MODE ? [] : [...(template.modes?.[mode]?.dropScripts ?? [])];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The template's package.json as a mode lays it: the base's, less the
|
|
156
|
+
* scripts the mode drops. null when the template has no merged file. */
|
|
157
|
+
export function modePackage(template, mode = BASE_MODE) {
|
|
158
|
+
const merged = template.files.find((f) => f.class === "merged");
|
|
159
|
+
if (!merged) return null;
|
|
160
|
+
const pkg = readJson(merged.from);
|
|
161
|
+
const drop = modeDropScripts(template, mode);
|
|
162
|
+
if (drop.length && pkg.scripts) pkg.scripts = Object.fromEntries(Object.entries(pkg.scripts).filter(([name]) => !drop.includes(name)));
|
|
163
|
+
return pkg;
|
|
164
|
+
}
|
|
165
|
+
|
|
59
166
|
export function loadTemplate(name) {
|
|
60
167
|
const dir = join(TEMPLATES, name);
|
|
61
168
|
const meta = readJson(join(dir, "template.json"));
|
|
@@ -94,7 +201,8 @@ export function loadTemplate(name) {
|
|
|
94
201
|
// The release lock (0.6.0 on): beside files/, never laid as it is. Each
|
|
95
202
|
// app's pnpm-lock.yaml is cut from it (src/lockfile.mjs).
|
|
96
203
|
const lock = existsSync(join(dir, RELEASE_LOCK)) ? join(dir, RELEASE_LOCK) : null;
|
|
97
|
-
|
|
204
|
+
const modes = loadModes(name, dir, meta, files);
|
|
205
|
+
return { name, title: meta.title ?? name, description: meta.description ?? "", root, files, components, retired, lock, modes };
|
|
98
206
|
}
|
|
99
207
|
|
|
100
208
|
/** The release lock's file, in templates/<name>/: made by `npm run lock`
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { claw } from "../lib/claw";
|
|
5
|
+
import { dataChanged, whenLanded } from "../lib/door";
|
|
6
|
+
import { changeFeed, refreshLoop } from "./frame";
|
|
7
|
+
|
|
8
|
+
// A browser app's auto-refresh (makersclaw.json "mode": "browser"). Rows
|
|
9
|
+
// change under this page from outside it: the agent writes them while
|
|
10
|
+
// the founder watches, and so do jobs and other members. The page is
|
|
11
|
+
// files, so there is nothing to re-render from a server: the hooks that
|
|
12
|
+
// read the rows (lib/data's useRows and the rest, lib/brain.js's) read
|
|
13
|
+
// again, and only when the data door says something changed.
|
|
14
|
+
//
|
|
15
|
+
// The rules are app/frame.js's (refreshLoop, and the reasons with each),
|
|
16
|
+
// the server app's every one:
|
|
17
|
+
//
|
|
18
|
+
// - only while the tab is visible AND the App tab is on screen: no timer
|
|
19
|
+
// at all otherwise, and coming back asks at once;
|
|
20
|
+
// - only while someone is there: after 30 minutes with no key, pointer,
|
|
21
|
+
// touch, wheel or focus in the app it stops, and the next sign asks
|
|
22
|
+
// at once;
|
|
23
|
+
// - only while the founder is not typing and no dialog is open
|
|
24
|
+
// (isEditing), and never while a surface has opted out with
|
|
25
|
+
// data-claw-refresh="off" on its root;
|
|
26
|
+
// - one tick at a time, the next timed once the last has landed: the
|
|
27
|
+
// interval plus what the last one took.
|
|
28
|
+
//
|
|
29
|
+
// What a tick does is this mode's own (changeFeed):
|
|
30
|
+
//
|
|
31
|
+
// - it asks GET _claw/data/changes?since=<cursor>. The first answer
|
|
32
|
+
// only sets the cursor and the stamp of the app's own tables; after
|
|
33
|
+
// it, a brain change or a different stamp is a change;
|
|
34
|
+
// - a change is dataChanged(), every hook on the page reading again,
|
|
35
|
+
// and the tick lands when whenLanded() says they all have their new
|
|
36
|
+
// rows. A read that fails keeps the rows it had;
|
|
37
|
+
// - never router.refresh(): under an export it fetches the same
|
|
38
|
+
// prebuilt payload. Never /claw-alive: nothing in the machine answers
|
|
39
|
+
// for a browser app, and the door is the platform's.
|
|
40
|
+
//
|
|
41
|
+
// The feed's first answer is asked as the page mounts, not at the first
|
|
42
|
+
// tick. That answer only sets where the feed is, so a change between it
|
|
43
|
+
// and the tick would never count: asked five seconds late, it would hide
|
|
44
|
+
// every change made while the page opened. This sits ahead of the Shell
|
|
45
|
+
// in the layout and React runs effects in that order, so the ask goes
|
|
46
|
+
// out before the surfaces' first reads: a change made in between is read
|
|
47
|
+
// twice, not missed.
|
|
48
|
+
export default function AutoRefresh() {
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const feed = changeFeed((path, signal) => claw(path, { signal }));
|
|
51
|
+
feed.probe();
|
|
52
|
+
const loop = refreshLoop({
|
|
53
|
+
doc: document,
|
|
54
|
+
probe: feed.probe,
|
|
55
|
+
refresh: () => {
|
|
56
|
+
feed.covered();
|
|
57
|
+
dataChanged();
|
|
58
|
+
return whenLanded();
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
return () => {
|
|
62
|
+
loop.stop();
|
|
63
|
+
feed.stop();
|
|
64
|
+
};
|
|
65
|
+
}, []);
|
|
66
|
+
return null;
|
|
67
|
+
}
|