makersclaw 0.7.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.
Files changed (85) hide show
  1. package/README.md +8 -1
  2. package/package.json +1 -1
  3. package/r/annotate.json +4 -4
  4. package/r/approval-request.json +2 -2
  5. package/r/audio-player.json +2 -2
  6. package/r/auth-form.json +2 -2
  7. package/r/board.json +2 -2
  8. package/r/calendar-view.json +3 -3
  9. package/r/cart.json +2 -2
  10. package/r/chart.json +5 -5
  11. package/r/chat.json +3 -3
  12. package/r/code-editor.json +2 -2
  13. package/r/comment-box.json +3 -3
  14. package/r/contact-section.json +2 -2
  15. package/r/core.json +7 -1
  16. package/r/csv-import.json +3 -3
  17. package/r/data-table.json +2 -2
  18. package/r/diagram.json +2 -2
  19. package/r/doc-editor.json +2 -2
  20. package/r/doc-page.json +3 -3
  21. package/r/email-render.json +2 -2
  22. package/r/faq.json +2 -2
  23. package/r/file-upload.json +2 -2
  24. package/r/flow-canvas.json +3 -3
  25. package/r/footer.json +3 -3
  26. package/r/gantt.json +3 -3
  27. package/r/job-panel.json +2 -2
  28. package/r/json-editor.json +3 -3
  29. package/r/map.json +2 -2
  30. package/r/menubar.json +2 -2
  31. package/r/mind-map.json +4 -4
  32. package/r/navigation-menu.json +2 -2
  33. package/r/order-detail.json +2 -2
  34. package/r/org-chart.json +4 -4
  35. package/r/page-builder.json +3 -3
  36. package/r/payment-methods.json +2 -2
  37. package/r/pdf-document.json +2 -2
  38. package/r/pdf-viewer.json +3 -3
  39. package/r/product-detail.json +2 -2
  40. package/r/product-grid.json +3 -3
  41. package/r/record-form.json +2 -2
  42. package/r/record-notes.json +4 -4
  43. package/r/registry.json +126 -121
  44. package/r/search-dialog.json +2 -2
  45. package/r/settings-list.json +2 -2
  46. package/r/setup-checklist.json +2 -2
  47. package/r/share-dialog.json +2 -2
  48. package/r/sidebar.json +3 -3
  49. package/r/signature-pad.json +2 -2
  50. package/r/sketch-canvas.json +3 -3
  51. package/r/spreadsheet.json +2 -2
  52. package/r/stat-cards.json +3 -3
  53. package/r/task-list.json +3 -3
  54. package/r/task-model.json +2 -2
  55. package/r/testimonials.json +2 -2
  56. package/r/transaction-list.json +2 -2
  57. package/r/whiteboard.json +4 -4
  58. package/registry/blocks/annotate/README.md +14 -11
  59. package/registry/blocks/chart/cartesian.jsx +1 -1
  60. package/registry/blocks/chart/core.jsx +33 -23
  61. package/registry/blocks/chart/pie.jsx +1 -1
  62. package/registry/blocks/chat/README.md +4 -1
  63. package/registry/blocks/chat/demo.jsx +2 -2
  64. package/registry/blocks/flow-canvas/README.md +15 -8
  65. package/registry/blocks/footer/README.md +3 -2
  66. package/registry/blocks/mind-map/README.md +4 -2
  67. package/registry/blocks/org-chart/README.md +4 -2
  68. package/registry/blocks/page-builder/README.md +1 -1
  69. package/registry/blocks/page-builder/demo.jsx +3 -3
  70. package/registry/blocks/pdf-viewer/README.md +4 -1
  71. package/registry/blocks/sidebar/README.md +23 -13
  72. package/registry/blocks/sketch-canvas/README.md +15 -9
  73. package/registry/blocks/whiteboard/README.md +20 -14
  74. package/src/cli.mjs +2 -2
  75. package/src/install.mjs +23 -6
  76. package/src/registry.mjs +34 -3
  77. package/templates/next/files/README.md +291 -11
  78. package/templates/next/files/app/shell.jsx +54 -37
  79. package/templates/next/files/check/run.mjs +76 -4
  80. package/templates/next/files/components/ui/index.js +1 -0
  81. package/templates/next/files/components/ui/layout.jsx +307 -0
  82. package/templates/next/files/lib/claw.js +4 -1
  83. package/templates/next/files/lib/hooks.js +71 -2
  84. package/templates/next/files/lib/platform.js +93 -4
  85. package/templates/next/template.json +10 -1
@@ -127,7 +127,10 @@ not run.
127
127
  has no `author`.
128
128
  - `emptyTitle` · `emptyBody` · `emptyState` — what an empty thread says;
129
129
  `emptyState` replaces the whole thing.
130
- - `height` — default `"min(40rem, 80dvh)"`; the list scrolls inside it.
130
+ - `height` — default `"min(40rem, 80dvh)"` (the block's own); the list
131
+ scrolls inside it. Yours is a length (`"40rem"`) or `"100%"` in a sized
132
+ box, such as the kit's `AppLayout contentMode="fill"`: a `vh` length in
133
+ your code is refused by `npm run check`.
131
134
  - `label` — what the thread is called to a screen reader.
132
135
  - `className` — layout around it (width, margin). Never colour.
133
136
 
@@ -233,7 +233,7 @@ function SupportDemo() {
233
233
  emptyTitle="No messages yet"
234
234
  emptyBody="Ask about billing, your workspace or an invoice — Ada answers in a few seconds."
235
235
  label="Support chat"
236
- height="min(44rem, 85dvh)"
236
+ height="44rem"
237
237
  />
238
238
  </div>
239
239
  );
@@ -412,7 +412,7 @@ function AssistantDemo() {
412
412
  emptyTitle="What should we look at?"
413
413
  emptyBody="Pip reads your orders, stock and returns. Pick a question or ask your own."
414
414
  label="Planning assistant"
415
- height="min(40rem, 85dvh)"
415
+ height="40rem"
416
416
  />
417
417
  </div>
418
418
  );
@@ -111,7 +111,10 @@ re-route as cards move; handle ids are not stored.
111
111
  screen). `onExitPresentation()` — Esc and the bar's Exit call it; turn
112
112
  `presentation` off there.
113
113
  - `height` — the canvas's height, default `560` (px; any CSS length works:
114
- `"70vh"`, `"100%"` inside a sized box).
114
+ `"100%"` inside a sized box, such as the kit's `AppLayout
115
+ contentMode="fill"` for a board that is the whole surface, the
116
+ Example). Never a `vh` length: `npm run check` refuses a size taken
117
+ from the window.
115
118
  - `bare` — `false` (default: rounded, bordered). `true`: no border or
116
119
  radius, for a full-page board.
117
120
  - `label` — the board's accessible name, default `"Board"`; also the list
@@ -370,6 +373,7 @@ import { useState } from "react";
370
373
  import { useSearchParams } from "next/navigation";
371
374
  import { useRow } from "../lib/data";
372
375
  import { updateRow } from "../lib/actions";
376
+ import { AppLayout } from "../components/ui";
373
377
  import { FlowCanvas } from "../components/blocks/flow-canvas";
374
378
 
375
379
  export default function LaunchSurface() {
@@ -382,13 +386,16 @@ export default function LaunchSurface() {
382
386
  function LaunchBoard({ project }) {
383
387
  const [board, setBoard] = useState(project.board); // jsonb, may be null; read once per project
384
388
  return (
385
- <FlowCanvas
386
- value={board ?? undefined}
387
- onChange={setBoard}
388
- onSave={(b) => updateRow({ source: "projects", id: project.id, patch: { board: b } })}
389
- label={`${project.name} board`}
390
- height="calc(100dvh - 8rem)"
391
- />
389
+ <AppLayout contentMode="fill">
390
+ <FlowCanvas
391
+ value={board ?? undefined}
392
+ onChange={setBoard}
393
+ onSave={(b) => updateRow({ source: "projects", id: project.id, patch: { board: b } })}
394
+ label={`${project.name} board`}
395
+ height="100%"
396
+ bare
397
+ />
398
+ </AppLayout>
392
399
  );
393
400
  }
394
401
  ```
@@ -117,8 +117,9 @@ In a server-mode app the same calls are `loadRows` in a server surface and the S
117
117
 
118
118
  - Change the language or currency by itself: `locales[].onChange` is
119
119
  yours.
120
- - Stick to the bottom of a short page: that is your layout's
121
- `min-h-dvh` flex column.
120
+ - Stick to the bottom of a short page: that is your layout's. The kit's
121
+ `AppLayout` content is a flex column, so a `flex-1` above the footer
122
+ pushes it down on a short page and lets it follow a long one.
122
123
  - Paint itself in the primary colour: the look is the workspace
123
124
  theme's.
124
125
  - Draw store badges: "Get the app" is two buttons with the Apple and
@@ -96,7 +96,9 @@ string, or nothing (a fresh map). The component runs it on `value`.
96
96
  - `placeholder` — what an empty topic shows while it is typed (default
97
97
  "New topic").
98
98
  - `height` — px or any CSS length, default `600`. A height class in
99
- `className` (`h-dvh`, `h-full`, `flex-1`) replaces it.
99
+ `className` (`h-full`, `min-h-0 flex-1`) replaces it: `min-h-0 flex-1`
100
+ fills what the surface has left (the Example). Never `h-dvh` or a `vh`
101
+ height, which `npm run check` refuses.
100
102
  - `label` — the map's accessible name (default "Mind map").
101
103
  - `onSelectionChange(topic | null)` — the selected topic changed (a side
102
104
  panel with its notes).
@@ -186,7 +188,7 @@ function PlanMap({ plan }) {
186
188
  onChange={setMap}
187
189
  onSave={(m) => updateRow({ source: "plans", id: plan.id, patch: { map: m, map_text: mindMapToText(m) } })}
188
190
  label={`${plan.name} map`}
189
- className="h-[calc(100dvh-8rem)]"
191
+ className="min-h-0 flex-1"
190
192
  />
191
193
  );
192
194
  }
@@ -73,7 +73,9 @@ One row per person, as a table holds them:
73
73
  reports: kit parts (their open deals, a "Message" button).
74
74
  - `onSelectionChange(person | null)` — the selected card changed.
75
75
  - `height` — px or any CSS length, default `640`. A height class in
76
- `className` (`h-dvh`, `h-full`, `flex-1`) replaces it.
76
+ `className` (`h-full`, `min-h-0 flex-1`) replaces it: `min-h-0 flex-1`
77
+ fills what the surface has left (the Example). Never `h-dvh` or a `vh`
78
+ height, which `npm run check` refuses.
77
79
  - `label` — the chart's accessible name (default "Org chart").
78
80
  - `className` — layout around it (margin, width, height). Never colour.
79
81
 
@@ -124,7 +126,7 @@ export default function TeamChart() {
124
126
  if (res.ok) await whenLanded(); // the people are read again: the chart draws the new manager
125
127
  return res;
126
128
  }}
127
- className="h-[calc(100dvh-8rem)]"
129
+ className="min-h-0 flex-1"
128
130
  />
129
131
  );
130
132
  }
@@ -105,7 +105,7 @@ variables the app's Tailwind utilities read. The built-in blocks' CSS
105
105
  | `actions` | — | Extra top-bar controls before Save, such as landing-builder's Publish. Use kit `Button`s at `size="sm"`. |
106
106
  | `saveLabel` | `"Save"` | The Save button's words. |
107
107
  | `builderRef` | — | A ref from `usePageBuilder()`. It receives the handle. |
108
- | `height` | `720` | The builder's height: a number (px) or any CSS length (`"min(760px, 88dvh)"`). It fills its width. |
108
+ | `height` | `720` | The builder's height: a number (px) or any CSS length (`"100%"` in a sized box, such as the kit's `AppLayout contentMode="fill"`; never a `vh` length, which `npm run check` refuses). It fills its width. |
109
109
  | `className` | — | Added to the frame. |
110
110
 
111
111
  ### The handle
@@ -78,7 +78,7 @@ export default function PageBuilderDemo() {
78
78
  <Landing />
79
79
  </TabPanel>
80
80
  <TabPanel tab="fresh" value={tab}>
81
- <PageBuilder title="New page" height="min(720px, 88dvh)" />
81
+ <PageBuilder title="New page" height={720} />
82
82
  </TabPanel>
83
83
  <TabPanel tab="own" value={tab}>
84
84
  <PageBuilder
@@ -86,7 +86,7 @@ export default function PageBuilderDemo() {
86
86
  builtIns="basics"
87
87
  blocks={OWN_BLOCKS}
88
88
  templates={OWN_TEMPLATES}
89
- height="min(720px, 88dvh)"
89
+ height={720}
90
90
  onSave={async () => {
91
91
  await new Promise((r) => setTimeout(r, 400));
92
92
  toast.success("Page saved");
@@ -122,7 +122,7 @@ function Landing() {
122
122
  page={{ html: LAUNCH.html }}
123
123
  onSave={onSave}
124
124
  builderRef={builder}
125
- height="min(760px, 88dvh)"
125
+ height={760}
126
126
  onUploadImage={async (file) => {
127
127
  await new Promise((r) => setTimeout(r, 500));
128
128
  // In an app: upload to storage and return its public address.
@@ -108,7 +108,10 @@ Store a highlight as a row in its own table (`document_id`, `page`,
108
108
  - `notes` — open the Highlights list on a wide viewer at first. Default:
109
109
  open when there are highlights.
110
110
  - `height` — the viewer's height, any CSS length. Default
111
- `"min(80vh, 860px)"`; it never goes under 384px.
111
+ `"min(80vh, 860px)"` (the block's own); it never goes under 384px.
112
+ Yours is a length, or `"100%"` in a sized box such as the kit's
113
+ `AppLayout contentMode="fill"`: a `vh` length in your code is refused
114
+ by `npm run check`.
112
115
  - `loading` — `true` shows the skeleton (while you fetch or make the
113
116
  file) instead of the empty state.
114
117
  - `emptyTitle` · `emptyBody` — the words when `file` is empty. Default
@@ -57,8 +57,11 @@ true) is where it starts. The icon rail shows every group unfolded.
57
57
  - `shortcut` — Cmd/Ctrl + this key toggles it; default `"b"`, `null` for none.
58
58
  - `variant` — `"inset"` draws the page as a card on the frame's ground.
59
59
  Default: the Sidebar's own `variant`, so you rarely pass it here.
60
- - `className` — the frame's height and edges (`min-h-dvh` in a shell).
61
- Never colour.
60
+ - `className` — the frame's edges, and its height from its parent: in a
61
+ shell it sits in the kit's `AppLayout contentMode="fill"` with
62
+ `min-h-0 flex-1` (the Example). The kit's `AppLayout` owns the
63
+ window's height; `min-h-dvh` or `h-dvh` here is refused by `npm run
64
+ check`. Never colour.
62
65
 
63
66
  ### Sidebar
64
67
 
@@ -138,6 +141,7 @@ The header button: opens the sheet on a phone, collapses to icons from
138
141
  // app/shell.jsx — the app's shell, the surfaces as the sidebar's items
139
142
  "use client";
140
143
  import { usePathname } from "next/navigation";
144
+ import { AppLayout } from "../components/ui";
141
145
  import { Sidebar, SidebarLayout, SidebarTrigger } from "../components/blocks/sidebar";
142
146
 
143
147
  const ICONS = { inbox: "inbox", deals: "dollar", contacts: "users", reports: "chart" };
@@ -153,18 +157,24 @@ export default function Shell({ name, surfaces, children }) {
153
157
  active: s.key === activeKey,
154
158
  badge: s.built ? undefined : "planned",
155
159
  }));
160
+ // The outer AppLayout owns the window; the inner one (it fills its
161
+ // slot) is the content that scrolls, back to its top on a new route.
156
162
  return (
157
- <SidebarLayout
158
- storageKey={`${name}.sidebar`}
159
- className="min-h-dvh"
160
- sidebar={<Sidebar title={name} items={items} label={`${name} surfaces`} />}
161
- >
162
- <header className="flex min-h-14 items-center gap-2 border-b border-border px-2 md:hidden">
163
- <SidebarTrigger />
164
- <span className="font-semibold">{name}</span>
165
- </header>
166
- <main className="min-w-0 flex-1 p-4 sm:p-6">{children}</main>
167
- </SidebarLayout>
163
+ <AppLayout contentMode="fill">
164
+ <SidebarLayout
165
+ storageKey={`${name}.sidebar`}
166
+ className="min-h-0 flex-1"
167
+ sidebar={<Sidebar title={name} items={items} label={`${name} surfaces`} />}
168
+ >
169
+ <header className="flex min-h-14 items-center gap-2 border-b border-border px-2 md:hidden">
170
+ <SidebarTrigger />
171
+ <span className="font-semibold">{name}</span>
172
+ </header>
173
+ <AppLayout contentClassName="p-4 sm:p-6" scrollKey={pathname}>
174
+ {children}
175
+ </AppLayout>
176
+ </SidebarLayout>
177
+ </AppLayout>
168
178
  );
169
179
  }
170
180
  ```
@@ -130,8 +130,11 @@ A sketch is plain JSON. Store it as it comes out of `onChange` / `onSave`
130
130
  - `onReady(handle)` — once, when the canvas has mounted.
131
131
  - `label` — the accessible name of the canvas region (default "Sketch").
132
132
  - `className` — layout: width, margin, height. The default height is
133
- `h-[32rem]`; a height class of yours (`h-dvh`, `h-full`, `min-h-…`,
134
- `flex-1`) replaces it. Never colour.
133
+ `h-[32rem]`; a height class of yours (`h-full`, `min-h-…`, `min-h-0
134
+ flex-1`) replaces it. A canvas that is the whole surface sits in an
135
+ `AppLayout contentMode="fill"` from the kit with `min-h-0 flex-1` (the
136
+ Example): never `h-dvh` or a `vh` height, which `npm run check`
137
+ refuses. Never colour.
135
138
 
136
139
  ### The handle
137
140
 
@@ -220,6 +223,7 @@ A whiteboard that saves itself:
220
223
  import { useSearchParams } from "next/navigation";
221
224
  import { useRow } from "../lib/data";
222
225
  import { updateRow } from "../lib/actions";
226
+ import { AppLayout } from "../components/ui";
223
227
  import { SketchCanvas } from "../components/blocks/sketch-canvas";
224
228
 
225
229
  export default function Board() {
@@ -227,13 +231,15 @@ export default function Board() {
227
231
  const { row: board, loading } = useRow({ source: "boards", id });
228
232
  if (loading || !board) return null;
229
233
  return (
230
- <SketchCanvas
231
- key={board.id}
232
- label={board.title}
233
- defaultValue={board.sketch} // jsonb, may be null; read once per board
234
- onSave={(sketch) => updateRow({ source: "boards", id: board.id, patch: { sketch } })}
235
- className="h-dvh rounded-none border-0"
236
- />
234
+ <AppLayout contentMode="fill">
235
+ <SketchCanvas
236
+ key={board.id}
237
+ label={board.title}
238
+ defaultValue={board.sketch} // jsonb, may be null; read once per board
239
+ onSave={(sketch) => updateRow({ source: "boards", id: board.id, patch: { sketch } })}
240
+ className="min-h-0 flex-1 rounded-none border-0"
241
+ />
242
+ </AppLayout>
237
243
  );
238
244
  }
239
245
  ```
@@ -93,8 +93,11 @@ for agents.
93
93
  receives the canvas handle: `exportImage()`, `getScene()`,
94
94
  `insertElements()`… (sketch-canvas README, "The handle").
95
95
  - `className` — layout: width, margin, height. The default height is
96
- `h-[40rem]`; a height class of yours (`h-dvh` for a full page, `h-full`,
97
- `flex-1`) replaces it. Never colour.
96
+ `h-[40rem]`; a height class of yours (`h-full`, `min-h-0 flex-1`)
97
+ replaces it. A board that is the whole surface sits in an `AppLayout
98
+ contentMode="fill"` from the kit with `min-h-0 flex-1` (the Example):
99
+ never `h-dvh` or a `vh` height, which `npm run check` refuses. Never
100
+ colour.
98
101
 
99
102
  ### Templates
100
103
 
@@ -118,6 +121,7 @@ A board page that saves itself:
118
121
  import { useSearchParams } from "next/navigation";
119
122
  import { useRow, whenLanded } from "../lib/data";
120
123
  import { updateRow } from "../lib/actions";
124
+ import { AppLayout } from "../components/ui";
121
125
  import { Whiteboard } from "../components/blocks/whiteboard";
122
126
  import { sketchToText } from "../components/blocks/sketch-canvas";
123
127
 
@@ -126,18 +130,20 @@ export default function BoardPage() {
126
130
  const { row: board, loading } = useRow({ source: "boards", id });
127
131
  if (loading || !board) return null;
128
132
  return (
129
- <Whiteboard
130
- key={board.id}
131
- title={board.title}
132
- onRename={async (next) => {
133
- const res = await updateRow({ source: "boards", id: board.id, patch: { title: next } });
134
- if (res.ok) await whenLanded(); // the board is read again: the title is the new one
135
- return res;
136
- }}
137
- defaultValue={board.sketch} // jsonb, may be null; read once per board
138
- onSave={(sketch) => updateRow({ source: "boards", id: board.id, patch: { sketch, sketch_text: sketchToText(sketch) } })}
139
- className="h-dvh rounded-none border-0"
140
- />
133
+ <AppLayout contentMode="fill">
134
+ <Whiteboard
135
+ key={board.id}
136
+ title={board.title}
137
+ onRename={async (next) => {
138
+ const res = await updateRow({ source: "boards", id: board.id, patch: { title: next } });
139
+ if (res.ok) await whenLanded(); // the board is read again: the title is the new one
140
+ return res;
141
+ }}
142
+ defaultValue={board.sketch} // jsonb, may be null; read once per board
143
+ onSave={(sketch) => updateRow({ source: "boards", id: board.id, patch: { sketch, sketch_text: sketchToText(sketch) } })}
144
+ className="min-h-0 flex-1 rounded-none border-0"
145
+ />
146
+ </AppLayout>
141
147
  );
142
148
  }
143
149
  ```
package/src/cli.mjs CHANGED
@@ -107,7 +107,7 @@ const listLine = (paths, n = 6) => (paths.length > n ? `${paths.slice(0, n).join
107
107
 
108
108
  function packageLines(changes) {
109
109
  if (!changes.length) return [];
110
- return [` package.json: ${changes.map((c) => `${c.name} ${c.from ?? "(new)"} → ${c.to}`).join(" · ")}`];
110
+ return [` package.json: ${changes.map((c) => `${c.name} ${c.from ?? "(new)"} → ${c.to ?? "(removed)"}`).join(" · ")}`];
111
111
  }
112
112
 
113
113
  /** The lock's line, when it was written. What it left out is a note. */
@@ -230,7 +230,7 @@ function diffText(r) {
230
230
  return lines.join("\n");
231
231
  }
232
232
  for (const e of r.entries) lines.push(` ${e.item.padEnd(14)} ${e.status.padEnd(8)} ${e.path}${e.detail ? ` — ${e.detail}` : ""}`);
233
- for (const c of r.package) lines.push(` ${"package.json".padEnd(14)} ${c.key}.${c.name}: ${c.from ?? "(new)"} → ${c.to}`);
233
+ for (const c of r.package) lines.push(` ${"package.json".padEnd(14)} ${c.key}.${c.name}: ${c.from ?? "(new)"} → ${c.to ?? "(removed)"}`);
234
234
  lines.push(`next: npx makersclaw update${r.compatible ? "" : " --major"}`);
235
235
  return lines.join("\n");
236
236
  }
package/src/install.mjs CHANGED
@@ -28,7 +28,7 @@ import { join } from "node:path";
28
28
  import { hashFile, readJson, removeFile, sha256, walk, writeFile, writeJson } from "./fsutil.mjs";
29
29
  import { LOCKFILE, LockError, deriveLock, parseLock } from "./lockfile.mjs";
30
30
  import { NPM_LOCKFILE, PNPM_VERSION, installDeps } from "./pm.mjs";
31
- import { BASE_MODE, BLOCK_DIR, loadBlocks, loadItems, loadTemplate, modeFiles, 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";
32
32
 
33
33
  export const MANIFEST = "makersclaw.json";
34
34
  export const PINS = "check/pins.json";
@@ -231,8 +231,11 @@ function loadTemplateOrFail(name) {
231
231
  }
232
232
 
233
233
  /** package.json: the template's scripts and dependencies win for the keys
234
- * it names; every other key is the app's. */
235
- function mergePackage(app, tpl) {
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 = []) {
236
239
  const out = { ...app };
237
240
  const changes = [];
238
241
  for (const key of ["scripts", "dependencies", "devDependencies"]) {
@@ -244,6 +247,12 @@ function mergePackage(app, tpl) {
244
247
  out[key][name] = value;
245
248
  }
246
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
+ }
247
256
  return { out, changes };
248
257
  }
249
258
 
@@ -346,8 +355,10 @@ function lay(dir, tpl, before, { tables, mode } = {}) {
346
355
  kept.push(f.path);
347
356
  continue;
348
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.
349
360
  if (f.class === "merged" && existsSync(to)) {
350
- const { out, changes } = mergePackage(readJson(to), readJson(f.from));
361
+ const { out, changes } = mergePackage(readJson(to), modePackage(tpl, appMode), modeDropScripts(tpl, appMode));
351
362
  if (changes.length) {
352
363
  writeJson(to, out);
353
364
  written.push(f.path);
@@ -355,6 +366,11 @@ function lay(dir, tpl, before, { tables, mode } = {}) {
355
366
  pkg = changes;
356
367
  continue;
357
368
  }
369
+ if (f.class === "merged") {
370
+ writeJson(to, modePackage(tpl, appMode));
371
+ written.push(f.path);
372
+ continue;
373
+ }
358
374
  const bytes = readFileSync(f.from);
359
375
  const hash = sha256(bytes);
360
376
  if (hashFile(to) !== hash) {
@@ -768,13 +784,14 @@ export function diff(dir) {
768
784
 
769
785
  const pkgPath = join(dir, "package.json");
770
786
  const tplPkg = tpl.files.find((f) => f.class === "merged");
771
- const pkg = existsSync(pkgPath) && tplPkg ? mergePackage(readJson(pkgPath), readJson(tplPkg.from)).changes : [];
787
+ const drop = modeDropScripts(tpl, appMode);
788
+ const pkg = existsSync(pkgPath) && tplPkg ? mergePackage(readJson(pkgPath), modePackage(tpl, appMode), drop).changes : [];
772
789
  // …and the installed blocks' npm packages, which update lays with them.
773
790
  const blockDeps = Object.assign({}, ...Object.keys(manifest.blocks ?? {}).map((n) => known.find((b) => b.name === n)?.dependencies ?? {}));
774
791
  if (existsSync(pkgPath) && Object.keys(blockDeps).length) pkg.push(...mergePackage(readJson(pkgPath), { dependencies: blockDeps }).changes);
775
792
  // …and the lock update would cut for the package.json it would leave.
776
793
  if (existsSync(pkgPath) && tplPkg) {
777
- const after = mergePackage(mergePackage(readJson(pkgPath), readJson(tplPkg.from)).out, { dependencies: blockDeps }).out;
794
+ const after = mergePackage(mergePackage(readJson(pkgPath), modePackage(tpl, appMode), drop).out, { dependencies: blockDeps }).out;
778
795
  const cut = cutLock(dir, tpl, after);
779
796
  if (cut && cut.text !== cut.before) entries.push({ item: "template", path: LOCKFILE, status: cut.before === null ? "new" : "changed" });
780
797
  }
package/src/registry.mjs CHANGED
@@ -81,8 +81,10 @@ export const BASE_MODE = "server";
81
81
  * - Each dropped file is a fixed file of the base: a drop the base does
82
82
  * not ship drops nothing, without a word, and only a pinned file is
83
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.
84
86
  */
85
- export function checkMode(template, mode, { base, overlay, drop }) {
87
+ export function checkMode(template, mode, { base, overlay, drop, scripts = [], dropScripts = [] }) {
86
88
  const fixed = new Set(base.filter((f) => f.class === "fixed").map((f) => f.path));
87
89
  const dropped = new Set(drop);
88
90
  for (const path of overlay) {
@@ -94,6 +96,9 @@ export function checkMode(template, mode, { base, overlay, drop }) {
94
96
  for (const path of drop) {
95
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`);
96
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
+ }
97
102
  }
98
103
 
99
104
  /** A mode's overlay folder: beside files/, a plain name. */
@@ -112,8 +117,11 @@ function loadModes(name, dir, meta, base) {
112
117
  // files out. Browser mode's own files come after its drop list does.
113
118
  const overlay = existsSync(root) ? walk(root) : [];
114
119
  const drop = [...(spec.drop ?? [])];
115
- checkMode(name, mode, { base, overlay, drop });
116
- modes[mode] = { root, drop, files: overlay.map((path) => ({ path, class: "fixed", from: join(root, path) })) };
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) })) };
117
125
  }
118
126
  return modes;
119
127
  }
@@ -132,6 +140,29 @@ export function modeFiles(template, mode = BASE_MODE) {
132
140
  return template.files.filter((f) => !drop.has(f.path)).map((f) => over.get(f.path) ?? f);
133
141
  }
134
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
+
135
166
  export function loadTemplate(name) {
136
167
  const dir = join(TEMPLATES, name);
137
168
  const meta = readJson(join(dir, "template.json"));