@plitzi/sdk-server 0.38.2 → 0.38.4

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 (54) hide show
  1. package/CHANGELOG.md +325 -0
  2. package/README.md +17 -7
  3. package/dist/core/createServer.devReload.test.d.ts +1 -0
  4. package/dist/core/http/stages/devReload.d.ts +18 -0
  5. package/dist/core/http/stages/devReload.js +40 -0
  6. package/dist/core/http/stages/images.js +1 -1
  7. package/dist/core/server/baseServer.d.ts +2 -0
  8. package/dist/core/server/baseServer.js +1 -0
  9. package/dist/core/server/drain.d.ts +2 -2
  10. package/dist/core/server/drain.js +17 -2
  11. package/dist/core/server/pageServer.js +8 -2
  12. package/dist/modules/images/imageProxy.js +19 -1
  13. package/dist/modules/rsc/connectorRscData.js +1 -1
  14. package/dist/modules/rsc/publicFileResolver.d.ts +3 -0
  15. package/dist/modules/rsc/publicFileResolver.js +10 -4
  16. package/dist/modules/ssr/prepareRender.js +1 -0
  17. package/dist/modules/ssr/views/template.ejs +6 -0
  18. package/dist/plugins/manager.js +1 -1
  19. package/package.json +3 -3
  20. package/skills/plitzi-authoring/CHEATSHEET.md +9 -5
  21. package/skills/plitzi-authoring/SKILL.md +6 -2
  22. package/skills/plitzi-authoring/recipes/accordion.ts +58 -0
  23. package/skills/plitzi-authoring/recipes/carousel.ts +14 -9
  24. package/skills/plitzi-authoring/recipes/detail-page.ts +10 -6
  25. package/skills/plitzi-authoring/recipes/every-few-seconds.ts +13 -8
  26. package/skills/plitzi-authoring/recipes/filter-a-list.ts +2 -2
  27. package/skills/plitzi-authoring/recipes/forms-and-modals.ts +1 -2
  28. package/skills/plitzi-authoring/recipes/from-tailwind.ts +1 -1
  29. package/skills/plitzi-authoring/recipes/link-to-a-section.ts +3 -3
  30. package/skills/plitzi-authoring/recipes/link-with-data.ts +12 -7
  31. package/skills/plitzi-authoring/recipes/marquee.ts +2 -2
  32. package/skills/plitzi-authoring/recipes/scroll-a-row.ts +5 -6
  33. package/skills/plitzi-authoring/recipes/server-data.ts +65 -0
  34. package/skills/plitzi-authoring/recipes/show-data.ts +12 -7
  35. package/skills/plitzi-authoring/recipes/style-a-page.ts +1 -1
  36. package/skills/plitzi-authoring/recipes/typed-data.ts +9 -6
  37. package/skills/plitzi-authoring/recipes/usable-without-sight.ts +2 -2
  38. package/skills/plitzi-authoring/reference/accessibility.md +8 -4
  39. package/skills/plitzi-authoring/reference/authoring-errors.md +13 -3
  40. package/skills/plitzi-authoring/reference/colours-and-motion.md +7 -0
  41. package/skills/plitzi-authoring/reference/components.md +2 -1
  42. package/skills/plitzi-authoring/reference/data-and-visibility.md +26 -16
  43. package/skills/plitzi-authoring/reference/drawing.md +22 -0
  44. package/skills/plitzi-authoring/reference/efficiency.md +10 -1
  45. package/skills/plitzi-authoring/reference/elements-and-styles.md +7 -11
  46. package/skills/plitzi-authoring/reference/lists.md +12 -7
  47. package/skills/plitzi-authoring/reference/plugins.md +5 -0
  48. package/skills/plitzi-authoring/reference/review-checklist.md +14 -2
  49. package/skills/plitzi-authoring/reference/snippets-and-export.md +2 -2
  50. package/skills/plitzi-authoring/reference/structure.md +1 -1
  51. package/skills/plitzi-authoring/reference/templates.md +1 -1
  52. package/skills/plitzi-authoring/reference/testing.md +10 -5
  53. package/skills/plitzi-authoring/reference/typed-sources.md +1 -1
  54. package/skills/plitzi-authoring/reference/validation.md +3 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,330 @@
1
1
  # @plitzi/sdk-server
2
2
 
3
+ ## 0.38.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 6ae4161: ## `plitzi push`: a self-hosted project back on its space
8
+
9
+ `plitzi create --from` and `plitzi pull` took a space out as a project and kept it in step; nothing put the project's
10
+ own changes back but a command per part, and no command at all for its pages. `plitzi push` is the way back
11
+ (`docs/en/projects-from-spaces.md`):
12
+
13
+ - **What changed, or what is named.** `plitzi push` sends what changed since the project last had the space — at a
14
+ terminal, offered as a ticked list to choose from (`↑/↓`, space, Enter); `plitzi push space functions` sends only
15
+ those parts: `space`, `functions`, `runtime`, `plugins`.
16
+ - **In order:** each changed plugin packed and uploaded (`pack plugin` + `upload plugin`, `--cdn`/`--bucket`), the
17
+ functions, the runtime, then the space — `src/space.ts` authored, the actions `src/actions.ts` serves and the manifests
18
+ in `src/connectors/` — as the space's draft. Never a published environment.
19
+ - **Never over the builder's work unseen.** The export now carries which state the draft is in (`SpaceExport.draft`),
20
+ recorded in `.plitzi/space.json` by `create --from` and `pull`; a push names it, and a draft edited since is refused
21
+ until `--force`. A project that never had the space may take one nobody has worked on; one holding work takes
22
+ `--force`.
23
+ - **Then `pull` follows it.** `.plitzi/space.json` records what was sent — only that, so a builder's change to a part
24
+ not pushed is still the next pull's — and a project that started on its own works with `pull` from then on.
25
+ - `@plitzi/sdk-shared/source`: `SpaceImport` / `SpaceImportResult` (`SPACE_IMPORT_FORMAT`), the push's one shape for
26
+ both ends, beside `SpaceExport`, which gains `draft`.
27
+ - `functions push`, `runtime push` and `upload plugin` keep their behaviour; their cores are what `push` runs.
28
+
29
+ ## A project's folders: `tmp/` for what it writes, `.plitzi/` committed, `public/` said to be public
30
+ - **`tmp/` is what a project writes for itself.** `@plitzi/sdk-server` keeps the plugins it builds in `tmp/.sdk-plugins`
31
+ and resized pictures in `tmp/images` by default (were `.sdk-plugins` and `.plitzi/images`). A project the CLI writes
32
+ puts the port it took (`tmp/dev-server.json`), the space as last authored while developing (`tmp/space.json`) and
33
+ Playwright's output (`tmp/visual`) there too, and its `.gitignore` ignores `tmp` — one line for all of it.
34
+ - **`.plitzi/` is committed.** The scaffold ignored it whole, so a clone lost `space.json`, `functions.json` and
35
+ `scaffold.json` — what `pull`, `push`, `functions push` and `upgrade` stand on. Now it holds only what the CLI records.
36
+ `npx plitzi upgrade --write` brings an existing project's `.gitignore` up.
37
+ - **No `space/offline-data.json`.** `npm run author` checks the space and writes nothing; the server asks it for the
38
+ documents with `--out tmp/space.json` while developing. The space is `src/space.ts`, and nothing beside it says
39
+ otherwise.
40
+ - **`public/` is on the internet.** The generated `README.md` and `AGENTS.md`, the CLI and authoring skills, the MCP
41
+ guide, `@plitzi/sdk-server`'s README and the docs now say so where data goes: never a secret, a key or data only
42
+ some visitors may read — in `public/`, or in a space's documents, which reach every visitor too.
43
+
44
+ ## An agent leaves the project clean
45
+
46
+ `AGENTS.md` gains **Keep the project clean** — nothing unused left behind, scratch work in `tmp/`, one of everything,
47
+ files a reader can find, and `author`, `typecheck`, `lint`, `format` and `check` passing — and the authoring skill's
48
+ review checklist a **Nothing left behind** section.
49
+
50
+ ## The dev tools' X-ray is the outlines too
51
+
52
+ The QA tab had **Outlines** (every element's box, its type and id when pointed at) and **X-ray** (the same boxes,
53
+ fainter, with what is wired to each). They are one tool now: the X-ray draws every box with its type and id, and marks
54
+ the wiring picked — all of it, one kind, or **Boxes only**.
55
+
56
+ ## A new space that says where to go
57
+
58
+ The space a new account and `plitzi create` start from keeps its welcome, and gains the three ways to change it — the
59
+ builder, code (with the command to run) and an agent — before the guides, which are shorter. Header, navigation, main
60
+ and footer are landmarks; the grid behind the top fades out instead of ending at an edge; the footer's line spans the
61
+ content; the links say their words and icon themselves, with no suggestion left.
62
+
63
+ ## Server data reads the same everywhere
64
+ - **One shape for a provider with a `query`.** A `runtime: 'server'` provider that only reads a JSON file the server
65
+ serves answered the body itself, while the same provider in the browser publishes `{ status, data }` — so moving it
66
+ to the server broke every binding. `publicFileResolver` and the builder's mock now answer `{ status, data }` too:
67
+ `<source>.data.<field>` whichever runtime.
68
+ - **Server data is on unless a space turns it off.** The page render read a missing `rsc` as on and the connector
69
+ resolver as off, so a server provider in a space the builder or the MCP made (neither writes `rsc`) resolved to
70
+ nothing, silently. Both read it as on now; only `rsc: { enabled: false }` turns it off (and is warned with a server
71
+ element). `authorSpace` writes `enabled: true` for a space that has one.
72
+ - **Never inside a component.** The page server resolves a page's tree and its layouts only, so a server provider in a
73
+ component rendered nothing, silently. Refused now (`server-provider-in-component`), with the fix: the provider on
74
+ the page, the rows handed in as a prop.
75
+ - **Bindings held to the data.** `authorSpace` takes `data` — what a provider's `query` answers — and
76
+ `publicData(folder)` (`@plitzi/sdk-authoring/node`) reads it from `public/`. A binding onto a path the file does not
77
+ have is warned (`path-not-in-data`) with the keys it does have. Generated projects pass it.
78
+ - **`plitzi check` reads the data too.** A binding that reads a path its provider's answer lacks, a provider that
79
+ failed (once, not as every element it left empty) and the rows each list rendered (`dataIssues`). `--ssr` names what
80
+ the server's HTML lacks that the hydrated page has. The dev tools' badge and panel (`data-plitzi-devtools`) are hidden
81
+ while `check` and `shot` look.
82
+
83
+ ## Authoring, clearer where agents tripped
84
+ - **`controls` on a button** (`aria-controls`): named by the element's id; authoring gives that element the anchor and
85
+ refuses an id the space lacks (`controls-unknown`); a saved document naming no anchor is warned
86
+ (`controls-no-anchor`). The accordion recipe uses it.
87
+ - **`live` on a container** (`aria-live`, `polite` or `assertive`) for words that change while a visitor reads them.
88
+ - **`subType: 'p'`** for a paragraph made of parts; `span-holds-block` covers it.
89
+ - **`formControl` types `search`, `url` and `tel`.**
90
+ - **Images:** no `src` draws the SDK's own placeholder (the CDN one answered 404); `resize: false` keeps a picture as
91
+ written; an SVG is never resized — `/_plitzi/img` redirects to it (`307`) instead of refusing it.
92
+ - **Messages:** an id taken by another page or layout names it and the id to write; a hyphenated source in a template
93
+ (`list_study-plans`) is explained as valid; an unknown CSS property says to write it in `customCss`; an unknown
94
+ variant on `custom` says it is never a prop of the component it hosts.
95
+ - **`plitzi explain` knows the helpers** — `bindTemplate`, `visibleWhen`, `variantFrom`, `activeOn`, `activeWhen`,
96
+ `when`, `named`, `scope`, `source`, `twig` — and `motion`, from the presets the SDK plays (`MOTION_ENTERS`,
97
+ `MOTION_TRIGGERS`, `MOTION_LOOPS`, now exported). `--list helpers` lists them.
98
+ - **`plitzi shot`** writes to `tmp/shots/` by default, and a full page shows the arrivals tied to the scroll as they
99
+ end. `inspectPage` no longer reports an image a hidden ancestor hides.
100
+ - **Style inspector:** `container-type` in Size.
101
+ - **New recipes:** `server-data.ts`, `accordion.ts`.
102
+
103
+ ## Matching a page to another, measured
104
+ - **A whole page is the whole page.** The SDK scrolls a pane of its own, so `shot`'s full-page picture was one
105
+ screen; the pane is unrolled now (`unrollPage`) — the viewport untouched, so a `100vh` hero stays one screen.
106
+ - **`plitzi shot --compare` says why, not only how much.** Every lazy picture on both pages is loaded before they are
107
+ taken (`loadImages`), so a section no longer swings between 23 % and 40 % on how many covers had arrived. Each section
108
+ is compared where it is on the other page (`alignPictures` over row profiles): a page 400 px longer is said once, with
109
+ the section the drift starts at, instead of a footer with the same styles reading 100 %. And the texts both pages
110
+ have are paired (`pageTexts`, `compareTexts`), each with what it does differently there —
111
+ `h1 "Learn CSS" — font-size 68px → 60px · y +19px` — position less the drift of its section. `comparePictures` takes
112
+ its options as an object now (`{ regions, tolerance, align }`).
113
+ - **`--scheme` is the space's theme.** A space whose default is dark painted dark under `--scheme light`, and the file
114
+ said light. `check` and `shot` now set the `theme` cookie a visitor's toggle writes; left out, the space's default is
115
+ pictured and the file is named by the theme actually painted.
116
+
117
+ ## `motion: { on: 'view' }` arrives once
118
+
119
+ `view` was tied to the scroll both ways: cards faded out again as the reader scrolled back up. Now it plays once, the
120
+ first time the element comes into view, with its `duration`, and stays — `revealOnView` (`@plitzi/sdk-shared/schema/
121
+ motionReveal`), one observer under the SDK's root, marks it `data-motion-seen`. The scroll-driven arrival is
122
+ `on: 'scroll'`. A page read without scripts shows them as they end (`@media (scripting: none)`); the builder's previews
123
+ and the MCP's local screenshots hold motion at its end. The builder's Motion tab has the three.
124
+
125
+ The builder's preview plays motion as the published page does. It held everything at its end, and nothing marked
126
+ the canvas `data-hydrated`, so neither the loops nor a space's own `[data-hydrated]` animations ever started there.
127
+ Now an arrival plays as it comes into view, and the loops and keyframes run; **▶** on the canvas being edited runs the
128
+ space's own `[data-hydrated]` loops too. The motion guide adds the rule this needs from a space: `[data-hydrated]` is for
129
+ loops; an entrance of its own that plays once (`transform`/`opacity` only) plays from the first paint with
130
+ `backwards`, as the presets do — gated on hydration it jumps when the page goes live, and a canvas that never goes
131
+ live holds it on its first frame.
132
+
133
+ ## Thirteen ways in, nine loops
134
+
135
+ `motion` grows from six arrivals and four loops to thirteen and nine, all still `opacity` and the transforms:
136
+
137
+ - **Bold arrivals:** `slide-up` (from well below), `zoom-in` (from 60%), `zoom-out` (settling from 125%), `tilt`
138
+ (rising as it turns straight), `spin-in` (half a turn as it grows).
139
+ - **Springy arrivals:** `pop` overshoots and settles; `bounce-in` falls into place and bounces.
140
+ - **Loops:** `bounce`, `wobble`, `orbit` (a slow circle round where it is), `heartbeat`, `drift`.
141
+
142
+ An arrival is now a list of frames (`MOTION_ENTER_FRAMES` in `@plitzi/sdk-shared/schema/motion`; `MOTION_ENTER_FROM`
143
+ is its first), and so is a loop (`MOTION_LOOP_FRAMES[name].frames`, replacing `transform`/`at`). Arrivals move the
144
+ individual `translate`, `scale` and `rotate`, so they still compose with an element's own `transform` and with a loop.
145
+ The builder's Motion tab previews every frame; authoring, the MCP, `explain motion` and GraphQL read the same lists.
146
+
147
+ An arrival from the side no longer scrolls the page sideways. `slide-left` starts 32px to the right of where it lands,
148
+ past a phone's 16px gutter, and until it had played the page could be dragged sideways. The SDK's root clips that way
149
+ now (`overflow-x: clip`, which keeps the window the scroller: sticky bars and scroll timelines are untouched), and the
150
+ builder's preview does the same. Content wider than the page is cut rather than scrolling the page — give it a box of
151
+ its own with `overflow-x: auto`.
152
+
153
+ **3D and the transform parts are style properties.** `transform-style`, `perspective-origin`, `backface-visibility`,
154
+ `translate`, `rotate` and `scale` join the vocabulary (they were refused as `css-property-unknown` and had to go in
155
+ `customCss`); `scale: 0.94` is a factor, not pixels.
156
+
157
+ **The builder's placeholder for an empty element is 24px** (was 50px), one size for every element that has one. It
158
+ is a minimum, so it also grew empty shapes with a size of their own — a dot, a planet — while editing; 24px is still
159
+ a box to see, pick and drop into.
160
+
161
+ The desktop app's window was blank: authoring, run in the renderer to build the rail, asked Vite's `process` shim for
162
+ a working directory it does not have. It reads one only where there is one now — and only when a class conflict is
163
+ reported, rather than for every element of every space it writes.
164
+
165
+ ## Authoring advice that knows where and when
166
+ - **`class-conflict` says where both declarations are:** `styles('md-menu-button') at src/components/dropdown.ts:12,
167
+ used by button at …, and styles('md-menu-button') at src/site/layout.ts:8, used by …` — `styles()` records its line as
168
+ an element factory does.
169
+ - **`repeated-shape` leaves controls alone.** Siblings that read different sources or write different state keys — a
170
+ menu for the language, one for the level — are not offered as a list.
171
+ - **`quiet: ['repeated-shape']`** on an element says a suggestion was left on purpose, and it is not offered again.
172
+ Only suggestions' codes (`quiet-unknown`). It is saved on the element (`definition.quiet`), so the builder's problems
173
+ panel, the MCP's `upsertElement`/`patchElement` and a project pulled back with `plitzi pull` all leave it alone.
174
+
175
+ ## What the docs say is what the code does
176
+
177
+ A pass across authoring's own messages, the skills, the MCP guide, `docs/en`, the CLI's README and the website, so an
178
+ agent reading two of them is never told two things:
179
+
180
+ - **A binding's `source` is the bound value, and only that.** With no value yet it fell back to the attribute's
181
+ current one — `sourceTo` — so `{{ source|default('…') }}` never took its default and `{{ source ? … }}` read an
182
+ element's placeholder ("Text") as an answer. It is `undefined` until the source has a value now; `sourceTo` is still
183
+ the value before.
184
+ - **`explain` writes a step the way its builder is called:** `delay(ms)`, `openModal('modal-id', data?)`,
185
+ `scrollBy('row-id', { x: '80%' })` — not `delay(…)` above the document's param names, which read as `delay({ time })`.
186
+ - **A page field nobody takes is told what it was probably for** — `seo` → `seoTitle` and `seoDescription` — instead of a
187
+ hint about keeping state appended to every one.
188
+ - Docs set straight: an element's id is `data-plitzi-el` in the page (`data-id` is the builder's, with debugging on);
189
+ a visibility condition writes nothing while its source has no value and is a yes or a no once it has one; `mobile`
190
+ is up to and including 48rem; `loadStrategy` defaults by type (`lazy` for a modal); the accessibility codes listed
191
+ in full everywhere (`embed-without-title`, `dropdown-without-control`, `controls-no-anchor`); the generated
192
+ `AGENTS.md` says a repeated block is a component and rows of data a list, as the skill does; `plitzi runtime` is
193
+ documented in the CLI's README, its skill and the website, which no longer says state across requests needs a
194
+ server of your own.
195
+
196
+ - Updated dependencies [6ae4161]
197
+ - @plitzi/sdk-shared@0.38.4
198
+ - @plitzi/plitzi-sdk@0.38.4
199
+
200
+ ## 0.38.3
201
+
202
+ ### Patch Changes
203
+
204
+ - e047e1d: ## `plitzi upgrade`: a project brought up to its CLI
205
+
206
+ A project written by an older CLI had no way up but `plitzi create` in a scratch folder and a file-by-file comparison:
207
+ `skills update` brought the skills and nothing else, so the `check` and `shot` scripts they teach did not exist.
208
+ `plitzi upgrade` (also `update`) shows what this CLI writes today for what is the CLI's in the project, and `--write`
209
+ makes it. One part or several: `plitzi upgrade skills`, `plitzi upgrade files packages`.
210
+
211
+ - **`files`:** the CLI's machinery — `author.ts`, `main.ts`, the Playwright, TypeScript and lint configs, AGENTS.md. One
212
+ nobody changed since the CLI wrote it is replaced, a missing one added; one the project made its own is shown as a
213
+ diff and left, until `--take <file>` (or `all`). `plitzi create` now records what it wrote, by digest, in
214
+ `.plitzi/scaffold.json`; a project made before that sees every file that differs as its own.
215
+ - **`packages`:** `package.json` merged, never replaced — the scripts and dependencies it lacks added, `@plitzi/*`
216
+ raised to this version, then the install (`--no-install` to leave it). A script of the project's own stays.
217
+ - **`skills`:** `.claude/skills/plitzi-*` from the packages installed, each replaced whole, and the ones a newer CLI
218
+ writes added. `plitzi skills update` is `plitzi upgrade skills --write`.
219
+ - **`renames`:** a name a version renamed with no alias, at its file and line — `canTemplate` → `canSnippet`,
220
+ `authorTemplate` → `authorSnippet` and its kin (renamed only where imported from `@plitzi/sdk-authoring`) — renamed
221
+ with `--write`.
222
+
223
+ `npm run author` says when the skills or the CLI's files are older than the SDK installed, and `plitzi --version`
224
+ prints the CLI's.
225
+
226
+ ## Page checks that can be the definition of done
227
+
228
+ `inspectPage`, `plitzi check` and the scaffold's visual test failed pages that were whole, so an agent learnt to ignore
229
+ them:
230
+
231
+ - **A breakpoint hiding an element on purpose** — the desktop navigation on a phone, a bottom bar only a phone shows —
232
+ is no longer "not visible": an element whose `display` or `visibility` a breakpoint rule sets is checked at the width
233
+ it shows at.
234
+ - **A lazy image out of sight** waits whichever side it is out of: below the fold, as before, or beside the screen in a
235
+ carousel's track — cut off by any ancestor that clips, as the browser decides when to fetch it.
236
+ - **A plugin that draws nothing** says so: `drawsNothing: true` in its declaration (a clock that fires a flow), and its
237
+ handle is `boxless`, so no check waits for it on screen.
238
+
239
+ `plitzi check --json` and `PageReport` carry `issues` beside `problems`: each with its `code` (`element-hidden`,
240
+ `image-not-loaded`, `sideways-scroll`…), the `elementId` it is about and, in `check`, the `width`.
241
+
242
+ ## Authoring
243
+ - **A link's `content` names it.** `control-without-name` read only a link's `label`, so applying the
244
+ `content-attribute` suggestion as written turned every link into a warning.
245
+ - **`plitzi fix` writes `content-attribute`** where it has one reading: a button's or link's children that are only
246
+ `text(…)` and `fontAwesome({ icon })` become its own `content` and `icon` — the words kept as written, an import left
247
+ unused taken out. A child with an id, options or a class of the space's own stays, and is said. `npm run author`'s
248
+ `[fix]` line counts these too.
249
+ - **The recipes, the skill's examples and the catalog template** hold to the suggestions as well as the warnings, and
250
+ their tests say so: six recipes and the template wrote a link's or a button's words as a child.
251
+
252
+ ## Declared motion — in code, in the builder and over MCP
253
+
254
+ An element says how it arrives and whether it keeps moving with `motion`, and the SDK's stylesheet plays it — no
255
+ keyframes to write: `{ enter: 'fade-up', on: 'view' }` arrives as it scrolls into view, `{ enter: 'scale', stagger:
256
+ 60 }` brings its children one by one (a grid, a list's rows), `{ loop: 'float' }` keeps it moving. Enters: `fade`,
257
+ `fade-up`, `fade-down`, `slide-left`, `slide-right`, `scale`; loops: `float`, `pulse`, `spin`, `sway`; `duration`,
258
+ `delay`, `stagger` in ms. Opacity and the transforms only — an arrival on `translate`/`scale`, so it composes with an
259
+ element's own `transform` — a loop held until the page is live (`data-hydrated`), and none of it for a visitor who asked
260
+ for less motion. `view` follows the scroll where the browser has scroll timelines and plays on load where not.
261
+
262
+ - **Authoring:** `motion` on any factory; refused where it cannot play (`motion-invalid`, `motion-no-tag`), and carried
263
+ by `specFromSpace` and `compareSpaces`.
264
+ - **Builder:** a **Motion** tab in an element's tools: the presets as chips, a stage that plays the one pointed at (a
265
+ loop shown stronger than the page plays it, and saying so), when and how long, children one by one, and the choices
266
+ read back as a sentence — laid out side by side once the sidebar is wide enough. The canvas holds motion still while
267
+ editing; **▶** in the header (or **Play on the canvas** in the tab) plays it from the start, restarting what already
268
+ played, and an arrival tied to the scroll plays by the clock there, where it is usually in view already.
269
+ - **MCP:** `motion` on `upsertElement` and `patchElement`, checked the same way; the guide names the presets.
270
+ - **Schema:** `definition.motion` (`ElementMotion`, `@plitzi/sdk-shared/schema/motion` — the presets, `motionProblems`,
271
+ `motionAttributes`, `isMotionAnimation`, and each preset's frames, which the stylesheet is tested against), in both
272
+ init queries. **The platform's GraphQL schema has to declare `SpaceElementMotion`
273
+ before this version's builder or SDK queries it.**
274
+
275
+ ## Dev tools QA: x-ray and motion
276
+
277
+ The **QA** tab gains an **X-ray**: every element the document wires something to — bound to data, shown on a condition,
278
+ running a flow, moving, behind a flag — outlined in its colour and named on the page, read from the space's document by
279
+ the element's name; its legend counts each kind on the page and picks one to show alone. Beside **Pause**, **Slow**
280
+ plays every animation at a quarter of its speed and **Replay** plays the declared motion again from the start, without
281
+ reloading the page.
282
+
283
+ ## Plugins that draw
284
+
285
+ `useCanvas2d`, `useWebGL`, `useWebGL2` and `useAnimationFrame` (`@plitzi/plitzi-sdk`): a canvas sized to the device (at
286
+ most 2×), followed as it resizes, animating only while somebody can see it move — a live page, no reduced motion, the
287
+ tab in front, the canvas on screen — and one still frame otherwise (the builder included). `createShaderProgram`
288
+ compiles and links, and a shader that fails throws a `ShaderError` with the driver's log, printed as
289
+ `[plugin <type> "<id>"] fragment shader failed: …` instead of an empty canvas; `error` and `ready` say where it is.
290
+ `useReducedMotion` for the rest.
291
+
292
+ ## A space re-authored without a restart
293
+
294
+ A project's `npm run start:dev` restarts for its server code and its plugins only: a save to the space is re-authored
295
+ in a process of its own and every open page loads again (`server.reloadPages()` over an SSE endpoint, on with
296
+ `createServer({ devReload: true })` — never by `devMode` alone, since every open page holds a connection for it); an
297
+ edit authoring refuses is printed and the page keeps the last space that authored. A shutdown also ends WebSockets at
298
+ once (`1001`, going away) rather than waiting out the grace — the ten seconds a restart used to wait on an open page.
299
+
300
+ ## Checks and authoring, from the stripe.com experiment
301
+ - **A component's instance** is found by its own name: its root carries `data-plitzi-instance`, and the instance's
302
+ handle selects it — `inspectPage` no longer reports every named instance as missing.
303
+ - **Hidden at this width on purpose** — the burger on a desktop, the desktop menu on a phone — is listed apart
304
+ (`hiddenAtWidth`, and a dim line under `plitzi check`'s ✓) rather than as a problem.
305
+ - **An element whose every child is conditional** — four flyouts in one list, each opening on the state that names
306
+ it — is conditional too: at rest it shows nothing, and that is it working.
307
+ - **A carousel that scrolls by itself** (`overflow-x: auto`) is not the page scrolling sideways; the page's own pane
308
+ still is.
309
+ - **Templates:** `{{ state.faq ?? -1 }}` — a sign on the right of `??` — reads.
310
+ - **A plugin attribute named as an element field** (`variant`, `class`, `id`…) is warned about
311
+ (`plugin-attribute-reserved`): a factory never hands it to the plugin. A `variant` no class or type style declares is
312
+ `unknown-variant`.
313
+ - **Lists:** a list with `items` renders as the `<ul>` (or `<ol>`) its `subType` says — it was a `<div>` — with no
314
+ markers and the same spacing, so a page looks as it did; the builder offers the list type for it too. Its rows are
315
+ `<li>`s: `list-row-not-li` warns of one that is not — a plain container is made one by `fixSpace`, a link or a button
316
+ is wrapped — and the recipes, the catalog template and the docs write rows as `listItem`.
317
+ - **CSS:** `mask-size`, `mask-position`, `mask-repeat`, `mask-composite`, `-webkit-mask-image`,
318
+ `-webkit-background-clip`, `box-decoration-break` and `-webkit-box-decoration-break`.
319
+ - **`plitzi shot`:** `--clip <element>`, `--scroll-to <element>` and `--viewport`; `--frames` takes the same framing.
320
+ - **SVG from files:** `svgFile` and `svgFiles` (`@plitzi/sdk-authoring/node`) read a logo or a folder of them,
321
+ compacted (`compactSvg`, also on the main entry), instead of strings in the space's source.
322
+ - The plugin scaffolds say that an inline `style` on `RootElement` outranks the element's classes.
323
+
324
+ - Updated dependencies [e047e1d]
325
+ - @plitzi/sdk-shared@0.38.3
326
+ - @plitzi/plitzi-sdk@0.38.3
327
+
3
328
  ## 0.38.2
4
329
 
5
330
  ### Patch Changes
package/README.md CHANGED
@@ -158,7 +158,8 @@ serve it — worth running over anything that arrives as a file.
158
158
  A space on Plitzi comes out whole, as a project wired to this server: `npx @plitzi/cli create my-site --from <space>`
159
159
  writes its pages as authoring code, its actions as `defineAction` code, its functions, the source of its plugins and
160
160
  runtime, and its files into `public/` — a draft, or any published snapshot (`--environment`, `--revision`) — and
161
- `plitzi pull` keeps it in step. See `docs/en/projects-from-spaces.md` in the workspace.
161
+ `plitzi pull` keeps it in step, and `plitzi push` puts what changed in it back on the space. See
162
+ `docs/en/projects-from-spaces.md` in the workspace.
162
163
 
163
164
  ## Configuration
164
165
 
@@ -167,6 +168,7 @@ runtime, and its files into `public/` — a draft, or any published snapshot (`-
167
168
  | `httpVersion` | `1 \| 2 \| 3` | `2` with `tls`, else `1` | HTTP protocol version. Falls back to the nearest available lower version. |
168
169
  | `tls` | `{ key, cert, minVersion? }` | — | TLS key and certificate. Required for versions 2 and 3; optional for version 1. |
169
170
  | `devMode` | `boolean` | `false` | Enables development mode: appends `?dev` to esm.sh CDN URLs for React, and activates per-request timing metrics (see [Dev metrics](#dev-metrics)). Off, the process must run with `NODE_ENV=production` or React renders with its development build; the server says so once, at `error`. |
171
+ | `devReload` | `boolean` | `false` | Every page this server renders listens for `server.reloadPages()` and loads again — for a project that re-authors its space on save (what `plitzi create` writes into `main.ts`). Not turned on by `devMode`: each open page holds a connection for it. |
170
172
  | `assetVersion` | `string` | — | Cache-buster appended as `?v=<assetVersion>` to all default SDK asset URLs. Compute from file mtime or package version at startup. |
171
173
  | `cacheTtlMs` | `number` | `300000` | TTL in milliseconds for the SSR render cache. Set to `0` to disable. |
172
174
  | `flags` | `Record<string, boolean> \| ({ spaceId, environment }) => Record<string, boolean>` | — | This deployment's say over the spaces' feature flags: above what a space declares, below the SDK's `flags` prop and a tester's dev tools, and only for flags the space declares. A function answers per space. Server actions and RSC resolve with it too. See `docs/en/feature-flags.md` in the workspace. |
@@ -175,11 +177,11 @@ runtime, and its files into `public/` — a draft, or any published snapshot (`-
175
177
  | `signIn` | `SSRSignInConfig` | — | Sign visitors in by redirect through an OAuth 2.1 authorization server: `GET /auth/sign-in?return=/x` registers this host as a client, sends the browser out with PKCE (state in a `__Host-` cookie), and `/auth/sign-in/callback` redeems the code server to server and hands the token to `exchangeCredential`, which makes the session here. Needs the `exchangeCredential` adapter. |
176
178
  | `templateFn` | `SSRTemplateFn` | built-in EJS template | Custom render function. Receives all template params and returns an HTML string. |
177
179
  | `plugins` | `Record<string, PluginSource>` | — | Named plugin definitions. Compiled or copied on first use and cached for `pluginsTtlMs`. |
178
- | `pluginsCacheDir` | `string` | `.sdk-plugins` | Directory where compiled plugin files are stored. |
180
+ | `pluginsCacheDir` | `string` | `tmp/.sdk-plugins` | Directory where compiled plugin files are stored — under `tmp/`, with everything else the server writes for itself. |
179
181
  | `pluginsTtlMs` | `number` | `604800000` | TTL in milliseconds for compiled plugins (default: 1 week). |
180
182
  | `autoLoadSchemaPlugins` | `boolean` | `true` | Auto-download and cache plugins declared in the schema's `offlineData.plugins` list. A type the deployment registers itself (`plugins`, named in `pluginNames`) is never looked for there: its own build is the one rendered. Set to `false` to manage plugin loading manually. |
181
183
  | `allowPrivatePluginHosts` | `boolean` | `false` | Read a schema plugin from a private address (`localhost`, this server's own network). A plugin's address is typed by whoever edits a space, so leave it off anywhere but a development machine whose bucket is local. |
182
- | `publicDir` | `string` | — | Absolute path to a directory served at the root URL level (e.g. `robots.txt`, `favicon.png`). Files are checked before `static` prefix routes. |
184
+ | `publicDir` | `string` | — | Absolute path to a directory served at the root URL level (e.g. `robots.txt`, `favicon.png`). Files are checked before `static` prefix routes. **Everything in it is public:** served to anyone who asks, with no check. |
183
185
  | `static` | `Record<string, string>` | — | URL prefix → filesystem path mappings for static file serving. |
184
186
  | `images` | `{ domains, cacheDir? }` | — | Pictures from other sites resized at `/_plitzi/img`, for an `image` to offer as a `srcset` (see [Images](#images)). |
185
187
  | `ssrOnly` | `boolean` | `false` | Omit client-side JS from the rendered page. Useful for verifying SSR HTML without hydration. |
@@ -396,11 +398,13 @@ createServer({ images: { domains: ['images.example.com', '*.cdn.example.com'] },
396
398
 
397
399
  An `image` whose `src` is another site's then offers the browser a `srcset` of widths this server makes (320 to 1920
398
400
  px) at `/_plitzi/img?url=…&w=…`, in AVIF or WebP when the browser takes them. Everything is kept on disk (`cacheDir`,
399
- default `.plitzi/images`): each original is downloaded once, and each size is made once from it. A week on, the kept
401
+ default `tmp/images`): each original is downloaded once, and each size is made once from it. A week on, the kept
400
402
  files keep answering while the original is asked for with its `ETag` / `Last-Modified` — unchanged, nothing is resized
401
403
  again; changed, its sizes are made anew. A picture that could not be fetched is not asked for again for five minutes.
402
404
  Only the listed hosts are fetched, every redirect is held to the same
403
- list and to the rule every outbound request follows (no private network), and SVG is refused. Resizing needs `sharp`
405
+ list and to the rule every outbound request follows (no private network). An SVG is never resized nor served from
406
+ this origin: the proxy answers it with a redirect to its own address (`307`), so the picture still shows. An `image`
407
+ with `resize: false` keeps its `src` as written. Resizing needs `sharp`
404
408
  (an optional peer): without it pictures are passed through and kept, and the server says so once.
405
409
 
406
410
  ### Public directory
@@ -416,6 +420,10 @@ createServer({
416
420
 
417
421
  The lookup order for a request is: `publicDir` → `static` prefix routes → SSR renderer.
418
422
 
423
+ **Everything in `publicDir` is on the internet.** A file there is served to anyone who asks for it, as it is — no
424
+ session, no permission, no check. Never put a secret, a key, an `.env`, a private document, a database dump or data only
425
+ some visitors may read in it: that is served by a server action whose `access` checks who is asking.
426
+
419
427
  `/.well-known/` paths follow the same lookup order: served from `publicDir` if a matching file exists, otherwise `404 Not Found`. They are never handled by the SSR renderer.
420
428
 
421
429
  ## Compression
@@ -517,7 +525,7 @@ const server = createServer({
517
525
  // From a source file — compiled to ESM with esbuild
518
526
  'my-chart': {
519
527
  js: '/abs/path/to/MyChart.tsx',
520
- css: '/abs/path/to/MyChart.css', // filesystem path — copied to .sdk-plugins
528
+ css: '/abs/path/to/MyChart.css', // filesystem path — copied to tmp/.sdk-plugins
521
529
  version: '1.2.0'
522
530
  },
523
531
  // Pre-compiled local file — copied as-is (version defaults to '1.0.0')
@@ -670,7 +678,9 @@ The SSR server includes a lightweight RSC endpoint that delivers server-side dat
670
678
 
671
679
  ### Schema setup
672
680
 
673
- Enable RSC at the top level of your schema:
681
+ Server data is on unless the schema turns it off with `"rsc": { "enabled": false }` — the page render and the
682
+ default `getRscData` (`connectorRscData`) read a missing `rsc` alike. `authorSpace` writes it out for a space with a
683
+ server provider:
674
684
 
675
685
  ```json
676
686
  {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import { Stage } from '../types';
2
+ /** Where an open page listens for "the space changed: load again", in development. */
3
+ export declare const DEV_RELOAD_PATH = "/__plitzi/reload";
4
+ /**
5
+ * The page reloading itself when the space it shows changed — the edit loop of a project that authors its space in
6
+ * code, without restarting the server: `main.ts` re-authors in a process of its own and calls `reloadPages()`, and
7
+ * every page open on this server loads again.
8
+ *
9
+ * Only with `devReload` on: every open page keeps a connection for it, which a server that never calls `reloadPages`
10
+ * — a published one, or a platform's — would pay for and get nothing from. The template listens only then too.
11
+ */
12
+ export declare const createDevReload: () => {
13
+ stage: Stage;
14
+ /** Every page open on this server loads again. */
15
+ reload: () => void;
16
+ /** The streams ended — the server is going away, and each page reconnects to whichever comes up next. */
17
+ close: () => void;
18
+ };
@@ -0,0 +1,40 @@
1
+ import { openEventStream } from "../sse.js";
2
+ //#region src/core/http/stages/devReload.ts
3
+ /** Where an open page listens for "the space changed: load again", in development. */
4
+ var DEV_RELOAD_PATH = "/__plitzi/reload";
5
+ /**
6
+ * The page reloading itself when the space it shows changed — the edit loop of a project that authors its space in
7
+ * code, without restarting the server: `main.ts` re-authors in a process of its own and calls `reloadPages()`, and
8
+ * every page open on this server loads again.
9
+ *
10
+ * Only with `devReload` on: every open page keeps a connection for it, which a server that never calls `reloadPages`
11
+ * — a published one, or a platform's — would pay for and get nothing from. The template listens only then too.
12
+ */
13
+ var createDevReload = () => {
14
+ const listening = /* @__PURE__ */ new Set();
15
+ const stage = (ctx) => {
16
+ if (!ctx.config.devReload || ctx.req.path !== "/__plitzi/reload") return Promise.resolve(false);
17
+ const stream = openEventStream(ctx.rawRes, {
18
+ onAbort: () => listening.delete(stream),
19
+ retryMs: 1e3
20
+ });
21
+ listening.add(stream);
22
+ ctx.raw.once("close", () => {
23
+ listening.delete(stream);
24
+ stream.close();
25
+ });
26
+ return Promise.resolve(true);
27
+ };
28
+ return {
29
+ stage,
30
+ /** Every page open on this server loads again. */
31
+ reload: () => listening.forEach((stream) => stream.send("reload", {})),
32
+ /** The streams ended — the server is going away, and each page reconnects to whichever comes up next. */
33
+ close: () => {
34
+ listening.forEach((stream) => stream.close());
35
+ listening.clear();
36
+ }
37
+ };
38
+ };
39
+ //#endregion
40
+ export { DEV_RELOAD_PATH, createDevReload };
@@ -15,7 +15,7 @@ var proxyFor = (images) => {
15
15
  if (!transform) serverLog.warn("IMAGES", "sharp is not installed: pictures are passed through and kept, not resized (npm i sharp).");
16
16
  return createImageProxy({
17
17
  domains: images.domains,
18
- cacheDir: path.resolve(images.cacheDir ?? ".plitzi/images"),
18
+ cacheDir: path.resolve(images.cacheDir ?? "tmp/images"),
19
19
  fetch: globalThis.fetch,
20
20
  transform
21
21
  });
@@ -18,5 +18,7 @@ export interface HttpServerParts {
18
18
  /** What the primary does once, before it starts the workers — work each of them would otherwise repeat, or race
19
19
  * the others to do. */
20
20
  beforeFork?: () => Promise<void>;
21
+ /** The open pages told to load again (`SSRServer.reloadPages`) — a page server in `devMode`; nothing elsewhere. */
22
+ reloadPages?: () => void;
21
23
  }
22
24
  export declare const createHttpServer: (config: SSRServerConfig, makeHandlerForPort: (port: number) => Handler, parts: HttpServerParts) => SSRServer;
@@ -34,6 +34,7 @@ var createHttpServer = (config, makeHandlerForPort, parts) => {
34
34
  return {
35
35
  cache: channel && parts.cache ? relayCache(parts.cache, `${channel}.cache`) : parts.cache,
36
36
  plugins: channel && parts.forgetPlugins ? relayPlugins(parts.plugins, parts.forgetPlugins, `${channel}.plugins`, label) : parts.plugins,
37
+ reloadPages: parts.reloadPages ?? (() => void 0),
37
38
  listen(port, host = "0.0.0.0") {
38
39
  if (plan.requested !== void 0 && role !== "worker") serverLog.warn(label, `${plan.requested} workers asked for, ${plan.count} cores to run them on: starting ${plan.count}`);
39
40
  if (role === "primary") {
@@ -10,8 +10,8 @@ export declare const SHUTDOWN_GRACE_MS = 10000;
10
10
  * Node's `server.close()` stops taking connections and then waits for every open one to end. A request ends; a
11
11
  * WebSocket or an event stream does not — a board open in a tab, an agent's app listening — so a server with one open
12
12
  * waited for ever, and a Ctrl+C or a SIGTERM hung until something killed it. `drain` stops taking connections, closes
13
- * the idle ones, ends every event stream at once (a stream has nothing left to finish), gives what is still being
14
- * answered `graceMs`, and then cuts what remains.
13
+ * the idle ones, ends every event stream and WebSocket at once (neither has anything left to finish), gives what is
14
+ * still being answered `graceMs`, and then cuts what remains.
15
15
  */
16
16
  export declare const watchConnections: (server: PrimaryServer) => {
17
17
  drain: (label: string, graceMs?: number) => Promise<void>;
@@ -5,6 +5,13 @@ import { serverLog } from "../../helpers/serverLog.js";
5
5
  * called a moment ago. Well under the 30 seconds a cluster gives a pod between SIGTERM and SIGKILL.
6
6
  */
7
7
  var SHUTDOWN_GRACE_MS = 1e4;
8
+ /** A WebSocket close frame, as a server sends it (unmasked): status 1001, "going away". */
9
+ var GOING_AWAY = Buffer.from([
10
+ 136,
11
+ 2,
12
+ 3,
13
+ 233
14
+ ]);
8
15
  /**
9
16
  * Whether a response is an event stream: said by its content type — or, since headers written with `writeHead(status,
10
17
  * headers)` are not kept where `getHeader` looks, by what it answers: a GET that asked for one, as an EventSource and
@@ -21,8 +28,8 @@ var isEventStream = (asking, answering) => {
21
28
  * Node's `server.close()` stops taking connections and then waits for every open one to end. A request ends; a
22
29
  * WebSocket or an event stream does not — a board open in a tab, an agent's app listening — so a server with one open
23
30
  * waited for ever, and a Ctrl+C or a SIGTERM hung until something killed it. `drain` stops taking connections, closes
24
- * the idle ones, ends every event stream at once (a stream has nothing left to finish), gives what is still being
25
- * answered `graceMs`, and then cuts what remains.
31
+ * the idle ones, ends every event stream and WebSocket at once (neither has anything left to finish), gives what is
32
+ * still being answered `graceMs`, and then cuts what remains.
26
33
  */
27
34
  var watchConnections = (server) => {
28
35
  const sockets = /* @__PURE__ */ new Set();
@@ -35,6 +42,13 @@ var watchConnections = (server) => {
35
42
  sockets.add(socket);
36
43
  socket.once("close", () => sockets.delete(socket));
37
44
  });
45
+ const websockets = /* @__PURE__ */ new Set();
46
+ server.on("upgrade", (request, socket) => {
47
+ if ([request.headers.upgrade].flat().some((value) => value?.toLowerCase() === "websocket")) {
48
+ websockets.add(socket);
49
+ socket.once("close", () => websockets.delete(socket));
50
+ }
51
+ });
38
52
  server.on("request", (request, response) => {
39
53
  answering.set(response, request);
40
54
  response.once("close", () => {
@@ -61,6 +75,7 @@ var watchConnections = (server) => {
61
75
  answering.forEach((request, response) => {
62
76
  if (isEventStream(request, response)) response.end(closeIdle);
63
77
  });
78
+ websockets.forEach((socket) => socket.end(GOING_AWAY));
64
79
  }) };
65
80
  };
66
81
  //#endregion
@@ -14,6 +14,7 @@ import { DRAFT_STORE_METHODS, createMemoryDraftStore } from "../../modules/ssr/p
14
14
  import { compileTemplate } from "../../modules/ssr/template.js";
15
15
  import { PluginManager } from "../../plugins/manager.js";
16
16
  import { makeHandler } from "../http/dispatcher.js";
17
+ import { createDevReload } from "../http/stages/devReload.js";
17
18
  import { buildPagePipeline } from "../services/registry.js";
18
19
  //#region src/core/server/pageServer.ts
19
20
  /** The page-serving machinery: html/rsc caches, the render template and the plugin manager, driving the page
@@ -49,7 +50,8 @@ var createPageServer = (config, services, extensions) => {
49
50
  };
50
51
  const actions = actionsModuleFor(config);
51
52
  const realtime = realtimeModuleFor(config);
52
- const stages = buildPagePipeline(services, extensions);
53
+ const devReload = config.devReload ? createDevReload() : void 0;
54
+ const stages = [...devReload ? [devReload.stage] : [], ...buildPagePipeline(services, extensions)];
53
55
  const makeHandlerForPort = (port) => {
54
56
  const buildContext = (raw, rawRes, req, res) => ({
55
57
  raw,
@@ -81,7 +83,11 @@ var createPageServer = (config, services, extensions) => {
81
83
  pluginManager.forget(name, version);
82
84
  invalidatePluginComponentCache();
83
85
  },
84
- onClosing: () => realtime?.close(),
86
+ onClosing: () => {
87
+ realtime?.close();
88
+ devReload?.close();
89
+ },
90
+ ...devReload ? { reloadPages: devReload.reload } : {},
85
91
  onDestroy: async () => {
86
92
  await actions?.jobs?.stop();
87
93
  destroyServerCaches(caches);