caspian-utils 0.2.0 → 0.2.2
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/dist/docs/ai-validation-checklist.md +3 -1
- package/dist/docs/auth.md +9 -8
- package/dist/docs/components.md +2 -1
- package/dist/docs/core-runtime-map.md +1 -1
- package/dist/docs/index.md +2 -1
- package/dist/docs/project-structure.md +16 -4
- package/dist/docs/pulsepoint-runtime-map.md +5 -2
- package/dist/docs/pulsepoint.md +159 -4
- package/dist/docs/routing.md +2 -2
- package/dist/docs/static-export.md +1 -1
- package/package.json +1 -1
|
@@ -62,7 +62,8 @@ Use prompts like these to check whether AI lands on the correct docs and files.
|
|
|
62
62
|
| Trace the auth cookie and CSRF cookie names used in development. | [index.md](./index.md), [auth.md](./auth.md), [core-runtime-map.md](./core-runtime-map.md) | `main.py`, `.venv/Lib/site-packages/casp/auth.py` | development cookie scoping and CSRF or session naming ownership |
|
|
63
63
|
| Find how route params and query params reach `page()`. | [index.md](./index.md), [routing.md](./routing.md), [core-runtime-map.md](./core-runtime-map.md) | `main.py`, `.venv/Lib/site-packages/casp/layout.py` | path-dict delivery, query coercion, and `request` injection |
|
|
64
64
|
| Decide whether a dashboard child route should use page caching. | [index.md](./index.md), [cache.md](./cache.md), [routing.md](./routing.md), [fetch-data.md](./fetch-data.md) | `main.py`, `.venv/Lib/site-packages/casp/cache_handler.py`, the route's `index.py` | route-level cache ownership, cacheable HTML scope, and invalidation points |
|
|
65
|
-
| Add a PulsePoint context provider to a reusable component. | [index.md](./index.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md) | `
|
|
65
|
+
| Add a PulsePoint context provider to a reusable component. | [index.md](./index.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md) | `public/js/pp-reactive-v2.js`, component `.html`, `.venv/Lib/site-packages/casp/components_compiler.py` | lowercase HTML-first `*.provider` usage, logical ancestry, single-root template shape |
|
|
66
|
+
| Render a conditional panel, a modal, or a table of rows in a route template. | [index.md](./index.md), [pulsepoint.md](./pulsepoint.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md), [routing.md](./routing.md) | `src/app/**/index.html`, `public/js/pp-reactive-v2.js` | `hidden="{...}"` for conditionals, `<template pp-for="…">` with `key` for lists, quoted brace attributes, and **no** JSX (`{cond && <div/>}`, `{list.map(...)}`, `class={...}`) |
|
|
66
67
|
| Add upload progress to an existing file manager. | [index.md](./index.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md), [file-uploads.md](./file-uploads.md), [fetch-data.md](./fetch-data.md) | `src/app/**/index.py`, `src/app/**/index.html`, `src/lib/**`, `public/js/pp-reactive-v2.js` | `pp.rpc` upload options, state replacement, persisted metadata, BrowserSync ignore rules |
|
|
67
68
|
| Add or debug a live WebSocket channel. | [index.md](./index.md), [websockets.md](./websockets.md), [core-runtime-map.md](./core-runtime-map.md), [pulsepoint.md](./pulsepoint.md), [auth.md](./auth.md) | `caspian.config.json`, `main.py`, `src/lib/websocket/**`, `src/app/**`, `settings/bs-config.json` | `websocket` feature gate, endpoint registration, origin checks, auth/session handling, native `WebSocket` client lifecycle, and BrowserSync URL selection |
|
|
68
69
|
| Decide whether MCP files should be created. | [index.md](./index.md), [mcp.md](./mcp.md), [commands.md](./commands.md), [project-structure.md](./project-structure.md) | `caspian.config.json`, `settings/restart-mcp.ts`, `package.json`, `src/lib/mcp/**` when enabled | `mcp` feature gate, update workflow, nested FastMCP config ownership |
|
|
@@ -75,6 +76,7 @@ Treat the table as a prompt pack for spot checks, not as a full validation matri
|
|
|
75
76
|
- AI reads only the packaged feature doc and never checks `main.py` or the installed runtime.
|
|
76
77
|
- AI edits framework internals when the task only requires app-owned route or helper changes.
|
|
77
78
|
- AI treats runtime HTML examples as authored template examples.
|
|
79
|
+
- AI reads the React comparison in [pulsepoint.md](./pulsepoint.md), [components.md](./components.md), [index.md](./index.md), or [routing.md](./routing.md) as permission to write JSX, and emits `{cond && (<div/>)}`, `{list.map(item => (<tr/>))}`, `className`, `onClick`, or unquoted `class={...}`. The React comparison covers the hook API and component decomposition only. An unquoted brace attribute is invalid HTML: the parser shreds it, the component root never compiles, and the page renders blank with **no console error**. Confirm every brace expression in an attribute is quoted, and that the file would still be valid HTML with all `{}` deleted.
|
|
78
80
|
- AI generates a route or component template with a valid-looking root element but leaves a sibling top-level `<script>` or second top-level element after it. This includes single-file `html("""...""")` components where the `<script>` is placed after the root's closing tag instead of nested inside it; the string handed to `html(...)` must resolve to one top-level element, script included.
|
|
79
81
|
- AI starts first-party interactivity with ids, `data-*` attributes, `querySelector`, `addEventListener`, manual `fetch`, manual `innerHTML`, or per-input `pp-ref` payload collection instead of PulsePoint `on*` attributes, form submit events, state, directives, and `pp.rpc()`.
|
|
80
82
|
- AI bypasses the generated Prisma Python ORM in a Prisma-enabled project with direct database drivers, custom fetch helpers, JSON active stores, browser-side database fetches, or a second app-owned database abstraction.
|
package/dist/docs/auth.md
CHANGED
|
@@ -305,21 +305,22 @@ Because Starlette runs the last-added middleware first, the effective request or
|
|
|
305
305
|
5. `RPCMiddleware`
|
|
306
306
|
6. route handler or RPC endpoint
|
|
307
307
|
|
|
308
|
-
Current behavior by layer:
|
|
309
|
-
|
|
310
|
-
- `SecurityHeadersMiddleware` can attach baseline response headers from `casp.runtime_security`, such as `X-Content-Type-Options`, framing policy, referrer policy, permissions policy, and production HSTS, while preserving any headers already set by the response. Do not assume this layer emits CSP or owns third-party browser resource allowlists; verify the installed package helper first.
|
|
311
|
-
- `
|
|
308
|
+
Current behavior by layer:
|
|
309
|
+
|
|
310
|
+
- `SecurityHeadersMiddleware` can attach baseline response headers from `casp.runtime_security`, such as `X-Content-Type-Options`, framing policy, referrer policy, permissions policy, and production HSTS, while preserving any headers already set by the response. Do not assume this layer emits CSP or owns third-party browser resource allowlists; verify the installed package helper first.
|
|
311
|
+
- `PublicFilesMiddleware` serves any existing `GET`/`HEAD` file under `public/**` before rate limiting, sessions, CSRF, auth, RPC, or page routing. It falls through for missing files and preserves restricted inline-media handling for configured upload directories.
|
|
312
|
+
- `SessionMiddleware` provides `request.session` for the rest of the stack.
|
|
312
313
|
- `CSRFMiddleware` ensures `request.session["csrf_token"]` exists and emits a scoped CSRF cookie based on `pp_csrf`, for example `pp_csrf_5091` in development or plain `pp_csrf` when no dev scope is active.
|
|
313
|
-
- `AuthMiddleware` sets request context with `Auth.set_request(request)`, initializes `StateManager`, runs provider callbacks,
|
|
314
|
+
- `AuthMiddleware` sets request context with `Auth.set_request(request)`, initializes `StateManager`, runs provider callbacks, and enforces public, auth, private, and role-based route redirects. Existing public files never reach it because `PublicFilesMiddleware` serves them first.
|
|
314
315
|
- `RPCMiddleware` handles `POST` requests with `X-PP-RPC: true` and forwards them to Caspian's RPC handler after auth and session setup are already available.
|
|
315
316
|
|
|
316
317
|
Keep `SessionMiddleware` immediately inside any outer response-header wrapper such as `SecurityHeadersMiddleware`. If CSRF, auth, or RPC handling runs before the session layer, `request.session` will not be available.
|
|
317
318
|
|
|
318
319
|
## Current `AuthMiddleware` Flow
|
|
319
320
|
|
|
320
|
-
The pasted `main.py` auth middleware currently behaves like this:
|
|
321
|
-
|
|
322
|
-
-
|
|
321
|
+
The pasted `main.py` auth middleware currently behaves like this:
|
|
322
|
+
|
|
323
|
+
- receives no request for an existing public file because the outer `PublicFilesMiddleware` has already served it; missing paths continue through normal auth and routing behavior
|
|
323
324
|
- initializes request-bound auth state with `StateManager.init(request)` and `Auth.set_request(request)`
|
|
324
325
|
- runs OAuth provider signin and callback handling before public or private route checks
|
|
325
326
|
- lets public routes through immediately
|
package/dist/docs/components.md
CHANGED
|
@@ -31,6 +31,7 @@ As the app grows, treat `src/components/` as the default home for reusable appli
|
|
|
31
31
|
- Use `html(...)` to keep markup, server interpolation, and a PulsePoint `<script>` inline in one Python file (single-file component) for a small or medium UI responsibility. It renders through Caspian's Jinja env, so `{{ ... }}` is server-side and `{ ... }` stays for PulsePoint.
|
|
32
32
|
- Use `render_html(...)` with a same-name `.html` file when the component has large markup, a long script, or you want clearer separation between Python logic and UI.
|
|
33
33
|
- Split UI by responsibility the way you would split React components. Single-file component authoring is a file shape, not permission to put a full page, dashboard, or all tab panels into one Python file.
|
|
34
|
+
- **The React comparison in this document is about decomposition and single-root shape only — never about syntax.** Component markup is plain HTML compiled by PulsePoint. JSX in a component template (`{cond && <div/>}`, `{list.map(...)}`, unquoted `class={...}`, `className`, `onClick`) corrupts the markup before the compiler runs, and an unquoted brace attribute silently blanks the whole page. Read [pulsepoint.md](./pulsepoint.md) "PulsePoint Is Not JSX" first.
|
|
34
35
|
- When a component needs first-party interactivity, bind events in the component template with PulsePoint-handled `on*` attributes and keep state in `pp.state(...)`; do not build id-driven `querySelector(...)` or `addEventListener(...)` wiring for normal component behavior.
|
|
35
36
|
- Keep page-level workflows in `src/app/`, move reusable UI into `src/components/`, and keep helpers, services, validators, and adapters in `src/lib/`.
|
|
36
37
|
|
|
@@ -626,7 +627,7 @@ In source, that parent may be a native HTML element or a single imported `x-*` c
|
|
|
626
627
|
|
|
627
628
|
This is not just style guidance. The installed compiler injects `pp-component` onto the final root element, and it raises an error when the template has no root, multiple sibling roots, a sibling script after the root, or stray top-level text.
|
|
628
629
|
|
|
629
|
-
For AI-generated templates, treat this as a hard authoring rule: write the HTML the same way a React component returns one parent node.
|
|
630
|
+
For AI-generated templates, treat this as a hard authoring rule: write the HTML the same way a React component returns one parent node — one parent, script inside it. The analogy covers the *number of roots*, nothing about the syntax inside them: the body is plain HTML, not JSX.
|
|
630
631
|
|
|
631
632
|
Failure shape to avoid:
|
|
632
633
|
|
|
@@ -39,7 +39,7 @@ Important current `main.py` behaviors AI should keep in mind:
|
|
|
39
39
|
- `register_single_route(...)` passes path params to `page()` as one positional dict, injects matching query params by name, and injects `request` by keyword when declared.
|
|
40
40
|
- The render pipeline is `transform_components(...)`, then `render_with_nested_layouts(...)`, then `transform_scripts(...)`.
|
|
41
41
|
- Route-level generators returned from `page()` are wrapped in `SSE(...)` before the response is sent.
|
|
42
|
-
- Safe public-file helpers live in `casp.runtime_security`
|
|
42
|
+
- Safe public-file helpers live in `casp.runtime_security`. `PublicFilesMiddleware` maps every existing nested file under `public/**` to the same root-relative URL without per-directory routes, rejects traversal and symlink escape, handles only `GET`/`HEAD`, and falls through when no file exists so page and mounted-app routing still works. User-upload directories must retain their restricted inline-media policy.
|
|
43
43
|
- Session middleware secrets may be resolved through `casp.runtime_security` so production can fail fast when `AUTH_SECRET` is missing or still on a default placeholder.
|
|
44
44
|
- Baseline response headers are built by `casp.runtime_security` and attached through `SecurityHeadersMiddleware`. That set now includes a Content-Security-Policy from `build_content_security_policy()`, which the `CONTENT_SECURITY_POLICY` environment variable replaces wholesale. The policy must keep `'unsafe-eval'` and `'unsafe-inline'` in `script-src`, because the PulsePoint runtime compiles templates with `new Function`; read the current helper before tightening it. Outside production the helper also adds loopback origins to `connect-src` so the BrowserSync live-reload client, which polls a different port than the proxied page, is not blocked; an environment override replaces that too.
|
|
45
45
|
- Middleware is added in source order as `RPCMiddleware`, `AuthMiddleware`, `CSRFMiddleware`, `SessionMiddleware`, `BodySizeLimitMiddleware`, `RateLimitMiddleware`, `SecurityHeadersMiddleware`, plus `RequestDiagnosticsMiddleware` outside production. The effective request order is reversed at runtime, so security headers and the rate limit run first and `RPCMiddleware` runs last. Verify the current list in `main.py` rather than trusting this order.
|
package/dist/docs/index.md
CHANGED
|
@@ -33,7 +33,8 @@ When generating or editing a Caspian app, treat these as the default choices unl
|
|
|
33
33
|
|
|
34
34
|
- Use PulsePoint for reactive frontend behavior.
|
|
35
35
|
- For first-party HTML events and reactivity, use PulsePoint `on*` attributes, state, refs, effects, directives, and `pp.rpc()` instead of ordinary DOM wiring with ids, `data-*` state, `querySelector`, `addEventListener`, or manual `innerHTML`.
|
|
36
|
-
- Treat every authored route, layout, and component HTML file like a React component return value
|
|
36
|
+
- Treat every authored route, layout, and component HTML file like a React component return value **in shape only**: exactly one top-level parent HTML element or one imported `x-*` root, with any owned plain `<script>` kept inside that same root.
|
|
37
|
+
- That single-root analogy is the *only* place React applies to markup. Template files are plain HTML — no JSX, no `{cond && <div/>}`, no `{list.map(...)}`, no unquoted `class={...}`. Conditionals are `hidden="{...}"` and lists are `<template pp-for="…">`. See [pulsepoint.md](./pulsepoint.md) "PulsePoint Is Not JSX" before writing any template.
|
|
37
38
|
- When `caspian.config.json` has `tailwindcss: true`, use Python `merge_classes(...)` plus browser `twMerge(...)` as the only supported Tailwind class-merging path.
|
|
38
39
|
- Use `@rpc()` plus `pp.rpc()` for browser-triggered reads, writes, streaming, and uploads.
|
|
39
40
|
- When `caspian.config.json` has `websocket: true`, use app-owned FastAPI WebSocket endpoints only for long-lived bidirectional live channels; keep normal browser-triggered data work on RPC.
|
|
@@ -180,9 +180,14 @@ This folder contains your database model definitions in `schema.prisma` and any
|
|
|
180
180
|
|
|
181
181
|
### `public/`
|
|
182
182
|
|
|
183
|
-
Store static assets here, including images, fonts, and generated frontend assets that should be served directly.
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
Store static assets here, including images, fonts, and generated frontend assets that should be served directly.
|
|
184
|
+
|
|
185
|
+
The public root maps directly to the URL root. Any existing nested file is
|
|
186
|
+
available at the same root-relative URL—for example,
|
|
187
|
+
`public/icons/app.png` is served as `/icons/app.png`. Adding a public
|
|
188
|
+
subdirectory does not require a matching route or mount in `main.py`.
|
|
189
|
+
|
|
190
|
+
Runtime-uploaded public blobs can also live here. Confirm the actual upload path in the project code, commonly `public/uploads/`, and keep that directory aligned with any BrowserSync ignore rules.
|
|
186
191
|
|
|
187
192
|
If the upload directory is created only at runtime, create it on demand in the owning upload helper instead of assuming it is already committed.
|
|
188
193
|
|
|
@@ -218,7 +223,14 @@ Use this package module when the task is about:
|
|
|
218
223
|
- user-facing vs production-safe exception messaging helpers
|
|
219
224
|
- baseline response headers such as permissions, referrer, MIME sniffing, framing, or HSTS behavior
|
|
220
225
|
|
|
221
|
-
Because this file is framework-owned, edit it only for Caspian runtime work or when documentation must match the installed package. App-specific auth policy still belongs in `src/lib/auth/auth_config.py`, and app-specific upload or storage behavior should live in route-owned code or other `src/lib/**` helpers.
|
|
226
|
+
Because this file is framework-owned, edit it only for Caspian runtime work or when documentation must match the installed package. App-specific auth policy still belongs in `src/lib/auth/auth_config.py`, and app-specific upload or storage behavior should live in route-owned code or other `src/lib/**` helpers.
|
|
227
|
+
|
|
228
|
+
`PublicFilesMiddleware` serves only existing `GET`/`HEAD` files that resolve
|
|
229
|
+
inside the configured public root and falls through for every other request.
|
|
230
|
+
Install it inside the security-header layer and outside rate limiting, sessions,
|
|
231
|
+
CSRF, auth, RPC, and page routing. Preserve a restricted inline-media policy for
|
|
232
|
+
user-upload directories so executable uploads such as HTML and SVG download
|
|
233
|
+
instead of rendering with the application's origin.
|
|
222
234
|
|
|
223
235
|
### `caspian.config.json`
|
|
224
236
|
|
|
@@ -36,8 +36,8 @@ If an inspected browser DOM disagrees with authored template source, remember th
|
|
|
36
36
|
| Template expressions | text and attributes with `{...}` | `public/js/pp-reactive-v2.js` | top-level script bindings are exported, nested bindings are not; every destructuring shape is supported at the top level (array, object, nested, defaults, rest, holes), so any tuple-returning hook reaches template scope |
|
|
37
37
|
| State | `pp.state(initial)` | `public/js/pp-reactive-v2.js` | setters accept values or updater functions, state belongs to the component instance |
|
|
38
38
|
| Effects | `pp.effect(...)`, `pp.layoutEffect(...)` | `public/js/pp-reactive-v2.js` | callbacks may return cleanup functions, promises are not awaited |
|
|
39
|
-
| Refs | `pp.ref(...)`, `pp-ref` on native or `x-*` tags | `components_compiler.py`, `
|
|
40
|
-
| Context | `pp.createContext(...)`, lowercase `<themecontext.provider>`, `pp.context(token)` | `
|
|
39
|
+
| Refs | `pp.ref(...)`, `pp-ref` on native or `x-*` tags | `components_compiler.py`, `public/js/pp-reactive-v2.js` | component-tag refs are reserved by the Python compiler, stamped with runtime-owned `pp-ref-owner`, captured in the parent's scope, and bound to the child's concrete root; do not author `data-pp-ref`, `pp-ref-owner`, or `pp-ref-forward`; callback refs may return synchronous cleanup functions that run on replacement or detach |
|
|
40
|
+
| Context | `pp.createContext(...)`, lowercase `<themecontext.provider>`, `pp.context(token)` | `public/js/pp-reactive-v2.js` | authored provider tags are HTML-first and lowercase; the runtime's template compiler rewrites `*.provider` to the runtime-owned `pp-context-provider` tag; ancestry is logical component ancestry; do not invent `pp-context` or `pp.provideContext` |
|
|
41
41
|
| Portals | `pp.portal(ref, target?)` | `public/js/pp-reactive-v2.js` | context should preserve logical ancestry through the registry |
|
|
42
42
|
| Props into a component | `x-*` attributes, Python `get_attributes({...}, props)` + `{{ attributes }}` | `html_attrs.py`, `components_compiler.py`, `public/js/pp-reactive-v2.js` | `pp.props` is read from the **rendered root's attributes**, so a prop accepted in Python but not re-emitted by `get_attributes` is silently `undefined` — no server error, no console warning. Brace expressions (`volume="{vol}"`) keep their real type; literals arrive as strings (`volume="0"` makes `volume === 0` false); valueless attributes arrive as `true`; JS reserved words such as `class` are dropped. See [components.md](./components.md#every-prop-a-template-reads-must-be-forwarded-to-the-root) |
|
|
43
43
|
| Stable ids | `pp.id()` | `public/js/pp-reactive-v2.js` | derived from component id plus hook slot; stable across rerenders, unique per instance; use for `id`/`for` pairing and `aria-*` instead of counters |
|
|
@@ -47,6 +47,7 @@ If an inspected browser DOM disagrees with authored template source, remember th
|
|
|
47
47
|
| Deferred values | `pp.deferredValue(value, initialValue?)` | `public/js/pp-reactive-v2.js` | lags one commit behind the source |
|
|
48
48
|
| Optimistic updates | `pp.optimistic(passthrough, reducer?)` | `public/js/pp-reactive-v2.js` | pending actions are dropped when `passthrough` changes, so a confirmed server value never double-counts the guess; without a reducer each action replaces the value |
|
|
49
49
|
| Error boundaries | `pp.errorBoundary()` | `public/js/pp-reactive-v2.js` | returns `[error, reset]`; render **and** effect/cleanup throws walk logical component ancestry to the nearest boundary; a boundary also catches its own throws; latches until `reset()`; caps at 5 captures per arming; event-handler errors are not routed — use `try`/`catch` |
|
|
50
|
+
| Conditional UI | `hidden="{!cond}"`, a ternary inside `{...}`, or `pp-for` over a 0/1-length array | `public/js/pp-reactive-v2.js` | **there is no `pp-if`, `pp-show`, or `pp-else`, and no JSX `{cond && <div/>}`**; `hidden` is a bound boolean attribute (Tailwind preflight makes `[hidden]` `display:none !important`, so it beats a `flex`/`grid` utility; without preflight a `display` rule wins); a hidden subtree still renders, so guard with `?.`. See [pulsepoint.md](./pulsepoint.md) "Conditional rendering" |
|
|
50
51
|
| Lists | `<template pp-for="item in items">` | `public/js/pp-reactive-v2.js` | `pp-for` belongs on `<template>`, accepts arrays and synchronous iterables, captures rendered row values for events/props, and uses plain `key`, not `pp-key` |
|
|
51
52
|
| Events | native `onclick`, `oninput`, `onchange`, `onsubmit` | `public/js/pp-reactive-v2.js` | first-party events belong in `on*` attributes; event scope exposes `event`, `e`, `$event`, `target`, `currentTarget`, and `el`; normal form submits should use `Object.fromEntries(new FormData(event.currentTarget).entries())` instead of per-input refs; avoid id-driven `querySelector`/`addEventListener` for normal UI |
|
|
52
53
|
| RPC | `pp.rpc(...)` in scripts, `@rpc()` in Python | `public/js/pp-reactive-v2.js`, `casp/rpc.py` | use `pp.rpc`, not legacy `pp.fetchFunction`; protected actions use `@rpc(require_auth=True)` |
|
|
@@ -69,6 +70,7 @@ If an inspected browser DOM disagrees with authored template source, remember th
|
|
|
69
70
|
|
|
70
71
|
## Copy-Safe Authoring Rules
|
|
71
72
|
|
|
73
|
+
- **Templates are plain HTML, not JSX.** PulsePoint borrows React's *hook API* and *component decomposition*, never its markup syntax. Quote every brace attribute (`class="{…}"`, never `class={…}` — the unquoted form is invalid HTML and blanks the page with no console error), use `hidden="{…}"` instead of `{cond && <div/>}`, and `<template pp-for="…">` instead of `{list.map(...)}`. See [pulsepoint.md](./pulsepoint.md) "PulsePoint Is Not JSX".
|
|
72
74
|
- Author one root element or one imported `x-*` root per route, layout, or component template.
|
|
73
75
|
- Keep any owned plain `<script>` inside that same root.
|
|
74
76
|
- Do not handwrite `pp-component`, `type="text/pp"`, `data-pp-ref`, `pp-owner`, `pp-event-owner`, or other runtime-managed attributes.
|
|
@@ -178,3 +180,4 @@ Use these prompts after docs or runtime changes to confirm AI can route correctl
|
|
|
178
180
|
- "Debug a dashboard sidebar losing scroll during child-route navigation."
|
|
179
181
|
- "Add an upload widget with progress and a reactive file list."
|
|
180
182
|
- "Use context to share theme state between parent and child components."
|
|
183
|
+
- "Render a list of users with edit and delete buttons, plus a confirmation modal." (Correct output uses `<template pp-for>` with `key` and `hidden="{…}"`; JSX `.map()` or `&&` in the template is a failure.)
|
package/dist/docs/pulsepoint.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: PulsePoint Runtime Guide
|
|
3
|
-
description: Use this page when the task mentions PulsePoint, `pp.state`, `pp.effect`, `pp-ref`, `pp-style`, `pp-for`, portals, `pp-reset-scroll`, SPA navigation, or `public/js/pp-reactive-v2.js`.
|
|
3
|
+
description: Use this page when the task mentions PulsePoint, `pp.state`, `pp.effect`, `pp-ref`, `pp-style`, `pp-for`, portals, `pp-reset-scroll`, SPA navigation, or `public/js/pp-reactive-v2.js`. Read the "PulsePoint Is Not JSX" section before writing any template — PulsePoint borrows React's hook API but its markup is plain HTML, and JSX constructs silently corrupt the page.
|
|
4
4
|
related:
|
|
5
5
|
title: Related docs
|
|
6
6
|
description: Read the components, routing, data-fetching, and project-structure docs alongside the PulsePoint runtime contract.
|
|
@@ -20,9 +20,9 @@ This file documents the PulsePoint contract for the shipped Caspian browser runt
|
|
|
20
20
|
|
|
21
21
|
If a task involves `pp.state`, `pp.effect`, `pp.layoutEffect`, `pp-ref`, `pp-style`, `pp-spread`, `pp-for`, context, portals, `pp-reset-scroll`, SPA navigation, or component boundary behavior, read this page first and keep generated code aligned with the current Caspian runtime.
|
|
22
22
|
|
|
23
|
-
Use `components.md` for authoring Python `@component` files, same-name HTML templates, and HTML-first `x-*` component tags. Use this page for the browser-side PulsePoint contract
|
|
23
|
+
Use `components.md` for authoring Python `@component` files, same-name HTML templates, and HTML-first `x-*` component tags. Use this page for the browser-side PulsePoint contract and the authoring rules that feed it.
|
|
24
24
|
|
|
25
|
-
PulsePoint is the default reactive frontend layer for Caspian.
|
|
25
|
+
PulsePoint is the default reactive frontend layer for Caspian. Its **script API** deliberately mirrors React hooks. Its **markup is plain HTML**, compiled by a template compiler that has no JSX support of any kind. Those two facts are independent, and conflating them is the single most common way generated PulsePoint code fails. Read the next section before writing a template.
|
|
26
26
|
|
|
27
27
|
Do not assume React, Vue, Svelte, JSX, Alpine, HTMX, or older PulsePoint docs unless the task explicitly asks for a different frontend contract.
|
|
28
28
|
|
|
@@ -38,6 +38,93 @@ Apply these before generating any template, even without reading the rest of thi
|
|
|
38
38
|
6. Call the backend with `pp.rpc(...)` backed by Python `@rpc()` actions; do not invent fetch wrappers or `pp.fetchFunction()`.
|
|
39
39
|
7. `pp-for` goes only on `<template>` with plain `key`; context uses `pp.createContext(...)`, a lowercase `<token.provider>` tag, and `pp.context(token)`.
|
|
40
40
|
8. If an API is not in `public/js/pp-reactive-v2.js`, it does not exist; do not invent hooks, directives, or globals.
|
|
41
|
+
9. **Every brace expression in an attribute must be inside quotes**: `class="{expr}"`, never `class={expr}`. Unquoted braces are shredded by the HTML parser.
|
|
42
|
+
10. **A `{...}` expression produces text, never elements.** There is no JSX. Conditionals use `hidden="{...}"`, lists use `<template pp-for="…">`.
|
|
43
|
+
|
|
44
|
+
## PulsePoint Is Not JSX
|
|
45
|
+
|
|
46
|
+
This section exists because the React comparison in this doc, in `components.md`, and in `index.md` has repeatedly been over-read as permission to write JSX. It is not.
|
|
47
|
+
|
|
48
|
+
**The React comparison is scoped to exactly two things:**
|
|
49
|
+
|
|
50
|
+
1. The **hook API inside `<script>`** — `pp.state`, `pp.effect`, `pp.memo`, `pp.ref`, dependency arrays, cleanup functions. These behave like their React counterparts.
|
|
51
|
+
2. **Component decomposition** — split a page into small single-responsibility components with props, the way you would split a React tree. This is about *file and responsibility shape*, not syntax.
|
|
52
|
+
|
|
53
|
+
**The React comparison does not extend to markup — at all.** A Caspian template is an HTML file. It is parsed by an HTML parser first, then compiled. It is never parsed as JavaScript, so JSX expressions in element position are not "unsupported" — they are not even seen as code.
|
|
54
|
+
|
|
55
|
+
### The JSX constructs that break PulsePoint
|
|
56
|
+
|
|
57
|
+
Each row is a real failure, not a style preference.
|
|
58
|
+
|
|
59
|
+
| JSX construct | What actually happens | PulsePoint form |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `{cond && (<div>…</div>)}` | The `{cond && (` becomes literal text; the `<div>` becomes a real always-visible element; the `)}` becomes literal text. | `<div hidden="{!cond}">…</div>` |
|
|
62
|
+
| `{cond ? <A/> : <B/>}` | Same — both branches render, plus visible stray text. | Two elements with complementary `hidden` bindings. |
|
|
63
|
+
| `{items.map(i => (<li>{i.name}</li>))}` | The `<li>` renders exactly once with the literal text `{items.map(i => (` before it. | `<template pp-for="item in items"><li key="{item.id}">{item.name}</li></template>` |
|
|
64
|
+
| An unquoted template-literal class, `class={` … `}` | **Silent HTML corruption.** The parser splits the unquoted value on spaces, producing junk attributes like `${b}="" 'a'=""`. The component root fails to compile and the page renders blank. | Quote the whole thing: ``class="{`a ${b}`}"`` |
|
|
65
|
+
| `selected={x === 'y'}` | Same unquoted-attribute corruption, because of the spaces around `===`. | `selected="{x === 'y'}"`, or bind `value` on the `<select>`. |
|
|
66
|
+
| `className=` | Not an HTML attribute. Ignored. | `class=` |
|
|
67
|
+
| `onClick={fn}` | Not a DOM event attribute in HTML (attributes are case-insensitive, so this lands as `onclick` with a JSX value). | `onclick="{fn()}"` |
|
|
68
|
+
| `htmlFor=`, `key={x}` in JSX braces | `htmlFor` is not HTML; `key` is a plain attribute here. | `for=`, `key="{x}"` |
|
|
69
|
+
| `<>…</>` fragments | Parsed as an unknown tag. Also breaks the single-root rule. | One real root element. |
|
|
70
|
+
| `style={{color:'red'}}` | Double-brace object literal is not CSS text. | `pp-style="{'color:red'}"` — a **string**, not an object. |
|
|
71
|
+
| `dangerouslySetInnerHTML` | Does not exist. | Server-render trusted HTML, or use `pp-for` over real data. |
|
|
72
|
+
|
|
73
|
+
### Why the corruption is silent
|
|
74
|
+
|
|
75
|
+
`class={...}` and `selected={...}` fail in the parser, not in PulsePoint. The template compiler never gets valid markup, the component root never mounts, and the runtime's reveal step never clears `<body style="opacity: 0">`. The result is a **blank white page with no console error** — the hardest possible failure to diagnose. Quoting the attribute is not cosmetic.
|
|
76
|
+
|
|
77
|
+
### The one-line test
|
|
78
|
+
|
|
79
|
+
Before writing a template, ask: *"Would this file be valid HTML if I deleted every `{}` from it?"* If not, it is JSX and it will not work.
|
|
80
|
+
|
|
81
|
+
## Complete Directive And API Surface
|
|
82
|
+
|
|
83
|
+
**These lists are closed.** PulsePoint has no other directives, template syntax, or globals. If something is not listed here, it does not exist — do not infer it from React, Vue, Alpine, or another PulsePoint version. Verify against `public/js/pp-reactive-v2.js` when in doubt.
|
|
84
|
+
|
|
85
|
+
### Author-facing template syntax (the whole list)
|
|
86
|
+
|
|
87
|
+
| Syntax | Where it goes | Purpose |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `{expression}` | Text nodes and **quoted** attribute values | Interpolate a value as text/attribute content |
|
|
90
|
+
| `on*` (`onclick`, `oninput`, `onchange`, `onsubmit`, any native DOM event attribute) | Any element | Event binding |
|
|
91
|
+
| `pp-for="item in items"` / `"(item, index) in items"` | **`<template>` only** | Keyed list rendering |
|
|
92
|
+
| `key="{expr}"` | Repeated sibling inside a `pp-for` template | Keyed diffing identity |
|
|
93
|
+
| `pp-ref="name"` / `pp-ref="{expr}"` | Native elements and `x-*` component tags | Imperative element access |
|
|
94
|
+
| `pp-style="{cssText}"` | Any element | Dynamic inline style, as a **CSS string** |
|
|
95
|
+
| `pp-spread="{...obj}"` | Any element | Spread an object into attributes |
|
|
96
|
+
| `pp-ref-forward` | *Server-set* on component roots — see note below | Ref forwarding through layout-neutral hosts |
|
|
97
|
+
| `<token.provider value="{v}">` (lowercase) | Anywhere in markup | Context provider |
|
|
98
|
+
| `pp-spa="true"` / `pp-spa="false"` | `<body>` enables SPA navigation; `pp-spa="false"` on an `<a>` opts that link out | SPA navigation interception |
|
|
99
|
+
| `pp-reset-scroll="true"` | A scroll container, or `<body>` | Reset that container's scroll on navigation |
|
|
100
|
+
| `pp-scroll-key="stable-name"` | A scroll container | Stable scroll-restoration identity |
|
|
101
|
+
| `pp-loading-content="true"` | The region swapped during navigation | Marks the navigation content region |
|
|
102
|
+
| `pp-loading-url="/route"` | A loading-state element | Route-specific loading lookup |
|
|
103
|
+
| `pp-loading-transition='{"fadeIn":…,"fadeOut":…}'` | The loading region | Navigation fade timings |
|
|
104
|
+
|
|
105
|
+
There is **no** `pp-if`, `pp-show`, `pp-else`, `pp-model`, `pp-bind`, `pp-class`, `pp-text`, `pp-html`, `pp-on`, or `pp-key`. Conditionals are `hidden="{...}"`; two-way binding is `value="{state}"` plus an `oninput` handler.
|
|
106
|
+
|
|
107
|
+
### Runtime-managed — never author these
|
|
108
|
+
|
|
109
|
+
The render pipeline or the browser runtime writes these. Handwriting them corrupts instance tracking:
|
|
110
|
+
|
|
111
|
+
`pp-component`, `type="text/pp"`, `pp-owner`, `pp-event-owner`, `pp-ref-owner`, `pp-ref-forward`, `pp-context-provider` (the generated tag), `pp-context-token`, `pp-context-value`, `pp-root-layout`, `pp-fragment-root`, `data-pp-ref`, `data-pp-input-value`, `data-pp-default-value`, `data-pp-checked-value`, `data-pp-default-checked`, `data-pp-select-value`, `pp-dynamic-script`, `pp-dynamic-meta`, `pp-dynamic-link`.
|
|
112
|
+
|
|
113
|
+
`pp-ref-forward` appears in the author-facing table only because you will see it in rendered HTML and in composition components; the server component compiler sets it on a root whose own root is another `x-*` component. Do not write it yourself.
|
|
114
|
+
|
|
115
|
+
### Component-script hooks (the whole list)
|
|
116
|
+
|
|
117
|
+
`pp.state`, `pp.effect`, `pp.layoutEffect`, `pp.ref`, `pp.memo`, `pp.callback`, `pp.reducer`, `pp.context`, `pp.portal`, `pp.id`, `pp.errorBoundary`, `pp.syncExternalStore`, `pp.imperativeHandle`, `pp.transition`, `pp.deferredValue`, `pp.optimistic`, plus the `pp.props` bag.
|
|
118
|
+
|
|
119
|
+
### Runtime utilities (the whole list)
|
|
120
|
+
|
|
121
|
+
`pp.createContext`, `pp.mount`, `pp.redirect`, `pp.rpc`, `pp.enablePerf`, `pp.disablePerf`, `pp.getPerfStats`, `pp.resetPerfStats`.
|
|
122
|
+
|
|
123
|
+
React hooks with **no** PulsePoint equivalent: `useContext`-as-a-provider-call, `useInsertionEffect`, `useDebugValue`, `useActionState`, `useFormStatus`, `forwardRef`, `memo()` as a component wrapper, `lazy`, `Suspense`, `startTransition` as a free function. Do not generate them.
|
|
124
|
+
|
|
125
|
+
### Identifiers injected into event handlers
|
|
126
|
+
|
|
127
|
+
Inside an `on*` attribute the runtime provides `event`, plus the aliases `e`, `$event`, `target` (→ `event.target`), `currentTarget` and `el` (both → `event.currentTarget`). An alias is skipped when the component scope already declares that name.
|
|
41
128
|
|
|
42
129
|
## Source Of Truth
|
|
43
130
|
|
|
@@ -393,7 +480,7 @@ That is the authored form. In browser-inspected runtime HTML, Caspian will alrea
|
|
|
393
480
|
|
|
394
481
|
## Hooks and runtime API
|
|
395
482
|
|
|
396
|
-
PulsePoint uses a React-style mental model inside each component script
|
|
483
|
+
PulsePoint uses a React-style mental model **inside each component script**: stateful render scope, dependency-based effects, refs, reducer-style updates, context consumption, and portals. That similarity stops at the `<script>` boundary — the surrounding markup is plain HTML, never JSX. See [PulsePoint Is Not JSX](#pulsepoint-is-not-jsx).
|
|
397
484
|
|
|
398
485
|
Hooks exposed inside component scripts through `pp`:
|
|
399
486
|
|
|
@@ -762,6 +849,9 @@ Nested components:
|
|
|
762
849
|
## Template expressions and attributes
|
|
763
850
|
|
|
764
851
|
- Use `{expression}` in text nodes and attribute values.
|
|
852
|
+
- **Attribute brace expressions must be quoted.** Write `class="{expr}"`, `disabled="{isSaving}"`, `selected="{role === 'admin'}"`. The unquoted JSX form `class={expr}` is not a PulsePoint syntax variant — it is invalid HTML. An unquoted value ends at the first space, so everything after it is re-parsed as further attribute names, and the element (and usually the whole component root) is destroyed before the compiler ever runs. This is silent: no console error, just a blank page.
|
|
853
|
+
- **An interpolation evaluates to a value, never to markup.** The compiler coerces the result with `String(...)` and HTML-escapes it. Elements cannot come out of an expression, so there is no JSX-style element-returning branch or `.map()`. Use `hidden="{...}"` for conditionals and `<template pp-for="…">` for lists.
|
|
854
|
+
- Objects, functions, and symbols in text position log `[PP-WARN] Invalid template child` and render nothing — that warning usually means JSX-shaped code was attempted.
|
|
765
855
|
- Follow HTML-first attribute naming. Native event attributes are lowercase DOM event attributes (`onclick`, `oninput`, `onsubmit`). Every other attribute on a component boundary is a prop: kebab-case names become camel-cased only inside `pp.props` (`selected-value` becomes `selectedValue`, `open-change` becomes `openChange`, and `on-open-change` becomes `onOpenChange`). A function prop does not need an `on-` prefix; that prefix is only an API naming convention. Use `on-click` when a component intentionally exposes an `onClick` prop, because lowercase `onclick` is reserved for the native DOM event on the boundary element.
|
|
766
856
|
- Pure bindings like `value="{count}"` are evaluated as expressions.
|
|
767
857
|
- For dynamic inline styles in authored templates, prefer `pp-style="{styleText}"` over `style="{styleText}"` so HTML/CSS tooling does not parse the brace expression as raw CSS.
|
|
@@ -849,6 +939,57 @@ Component example:
|
|
|
849
939
|
</div>
|
|
850
940
|
```
|
|
851
941
|
|
|
942
|
+
## Conditional rendering
|
|
943
|
+
|
|
944
|
+
PulsePoint has **no conditional directive**. There is no `pp-if`, no `pp-show`, no `pp-else`, and no JSX-style `{cond && (<div/>)}`. Conditional UI is expressed three ways, in this order of preference.
|
|
945
|
+
|
|
946
|
+
**1. `hidden="{...}"` — the default.** Bind the native boolean attribute. The element stays in the DOM and keeps its state; only its visibility changes.
|
|
947
|
+
|
|
948
|
+
```html
|
|
949
|
+
<div hidden="{!message}" class="notice">{message}</div>
|
|
950
|
+
|
|
951
|
+
<div hidden="{!showForm}">
|
|
952
|
+
<form onsubmit="{submitForm(event)}"> … </form>
|
|
953
|
+
</div>
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
Notes:
|
|
957
|
+
|
|
958
|
+
- `hidden` is in the runtime's boolean-attribute set, so a truthy value emits the bare attribute and a falsy value removes it.
|
|
959
|
+
- Under Tailwind's preflight, `[hidden]:where(:not([hidden="until-found"]))` is `display: none !important`, so `hidden` reliably beats a `flex`/`grid`/`block` utility on the same element. **Without Tailwind preflight**, `hidden` is only a UA-stylesheet `display: none` and any `display` rule overrides it — in that case hide with a bound class instead.
|
|
960
|
+
- Because the subtree still exists, guard expressions inside it: use `{deleteTarget?.name}`, not `{deleteTarget.name}`. A hidden element is still rendered, so a throw there still breaks the component.
|
|
961
|
+
|
|
962
|
+
**2. A ternary inside an interpolation — for text, classes, and attributes.**
|
|
963
|
+
|
|
964
|
+
```html
|
|
965
|
+
<h3>{editingUser ? 'Edit User' : 'Create User'}</h3>
|
|
966
|
+
<button disabled="{isSaving}">{isSaving ? 'Saving…' : 'Save'}</button>
|
|
967
|
+
<span class="{'badge ' + (user.role === 'admin' ? 'badge-admin' : 'badge-user')}">{user.role}</span>
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
Template literals work too, as long as the attribute is quoted — the compiler tracks nested braces and quotes: ``class="{`badge ${isAdmin ? 'badge-admin' : ''}`}"``. String concatenation is easier to read and avoids nesting mistakes.
|
|
971
|
+
|
|
972
|
+
**3. `pp-for` over a 0-or-1 length collection — when the node must truly leave the DOM.** Use this when a hidden subtree would be wrong (an expensive child component, a duplicate form field name, a video that must stop).
|
|
973
|
+
|
|
974
|
+
```html
|
|
975
|
+
<template pp-for="item in selected ? [selected] : []">
|
|
976
|
+
<x-detail-panel key="{item.id}" item="{item}" />
|
|
977
|
+
</template>
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
For an empty-state row, bind `hidden` on the placeholder rather than branching:
|
|
981
|
+
|
|
982
|
+
```html
|
|
983
|
+
<tbody>
|
|
984
|
+
<tr hidden="{users.length !== 0}">
|
|
985
|
+
<td colspan="5">No users found.</td>
|
|
986
|
+
</tr>
|
|
987
|
+
<template pp-for="user in users">
|
|
988
|
+
<tr key="{user.id}">…</tr>
|
|
989
|
+
</template>
|
|
990
|
+
</tbody>
|
|
991
|
+
```
|
|
992
|
+
|
|
852
993
|
## Lists and keyed diffing
|
|
853
994
|
|
|
854
995
|
- Use `pp-for` only on `<template>`.
|
|
@@ -1001,6 +1142,7 @@ These are runtime details.
|
|
|
1001
1142
|
|
|
1002
1143
|
Use these rules when generating or editing PulsePoint runtime code:
|
|
1003
1144
|
|
|
1145
|
+
- **Write plain HTML, never JSX.** Before finishing any template, verify it would still be valid HTML with every `{}` deleted, that every attribute brace expression is quoted, that conditionals use `hidden="{...}"`, and that lists use `<template pp-for="…">` with `key`. This check catches the highest-frequency generation failure and costs one pass.
|
|
1004
1146
|
- Treat PulsePoint as the default reactive frontend for Caspian app code.
|
|
1005
1147
|
- For first-party HTML interactions, use PulsePoint `on*` event attributes, state, refs, effects, directives, and `pp.rpc()` before reaching for DOM APIs.
|
|
1006
1148
|
- For simple forms, bind `onsubmit` in the authored HTML and read named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`; do not generate per-input refs or effect-managed submit listeners just to gather values.
|
|
@@ -1032,6 +1174,19 @@ Use these rules when generating or editing PulsePoint runtime code:
|
|
|
1032
1174
|
|
|
1033
1175
|
Do not generate these unless the current source explicitly adds support:
|
|
1034
1176
|
|
|
1177
|
+
JSX constructs in markup — these are the highest-frequency generation failure, so check for them explicitly before finishing a template:
|
|
1178
|
+
|
|
1179
|
+
- `{cond && (<element/>)}` or `{cond ? <A/> : <B/>}` element-returning branches — use `hidden="{...}"`
|
|
1180
|
+
- `{list.map(item => (<element/>))}` — use `<template pp-for="item in list">`
|
|
1181
|
+
- unquoted brace attributes: `class={…}`, `selected={…}`, `value={…}`, `disabled={…}` — always quote: `class="{…}"`
|
|
1182
|
+
- `className`, `htmlFor`, `onClick`/`onChange`/`onSubmit` camelCase event props, `defaultValue` as a React prop, `dangerouslySetInnerHTML`
|
|
1183
|
+
- `style={{ color: "red" }}` object literals — `pp-style` takes a CSS **string**
|
|
1184
|
+
- `<>…</>` fragments, or any second top-level node
|
|
1185
|
+
- `key` written as a JSX brace prop outside a quoted attribute
|
|
1186
|
+
- imported/returned components as JavaScript values — components are server-side `x-*` tags
|
|
1187
|
+
|
|
1188
|
+
Also avoid:
|
|
1189
|
+
|
|
1035
1190
|
- React, Vue, Svelte, Alpine, HTMX, or JSX-first patterns as the default Caspian frontend approach
|
|
1036
1191
|
- standard DOM scripting as the default first-party interaction model, including id/data-attribute driven `querySelector(...)`, `addEventListener(...)`, or manual `innerHTML` rendering for normal buttons, forms, filters, toggles, uploads, and reactive lists
|
|
1037
1192
|
- `pp-context`
|
package/dist/docs/routing.md
CHANGED
|
@@ -36,7 +36,7 @@ Start with these rules:
|
|
|
36
36
|
- In grouped shells with separate shell and content scrolling, put `pp-reset-scroll="true"` on the content pane in `layout.html` when that pane should reset on child-route navigation while the shell sidebar or rail keeps its own scroll.
|
|
37
37
|
- Use `layout.py` when a layout needs shared props or metadata before rendering. The `layout()` function may be synchronous or async.
|
|
38
38
|
- Keep visible route and layout markup in `index.html` and `layout.html`. Treat `index.py` and `layout.py` as backend companions, not as places to author visible HTML.
|
|
39
|
-
- Treat every authored route and layout template like a React component body
|
|
39
|
+
- Treat every authored route and layout template like a React component body **for root counting only**: exactly one top-level parent HTML element or one imported `x-*` root, with any owned plain `<script>` inside that same root. The markup itself is plain HTML, never JSX — see [pulsepoint.md](./pulsepoint.md) "PulsePoint Is Not JSX".
|
|
40
40
|
- For route and layout interactivity, use PulsePoint in the authored HTML first: native `on*` event attributes, `pp.state(...)`, refs, effects, directives, and `pp.rpc()`. Do not create standard JavaScript event systems with ids, `data-*` state, `querySelector`, `addEventListener`, or manual `innerHTML` for normal first-party UI.
|
|
41
41
|
|
|
42
42
|
## Hard Template Invariant
|
|
@@ -189,7 +189,7 @@ Route templates follow the same authored-vs-runtime contract documented in [puls
|
|
|
189
189
|
|
|
190
190
|
When a route needs button clicks, form submits, input changes, filters, tabs, menus, uploads, polling, or reactive list updates, author those interactions as PulsePoint behavior in `index.html`. Use `onclick`, `oninput`, `onchange`, `onsubmit`, `pp.state(...)`, `pp-for`, refs, effects, and `pp.rpc(...)` instead of starting with `id` attributes plus `document.querySelector(...)` or `addEventListener(...)`. For simple form submits, prefer `onsubmit="{submitForm(event)}"` and `Object.fromEntries(new FormData(event.currentTarget).entries())` over per-input `pp-ref` payload collection.
|
|
191
191
|
|
|
192
|
-
For AI-generated route templates, treat `src/app/**/index.html` the same way you would a React component body
|
|
192
|
+
For AI-generated route templates, treat `src/app/**/index.html` the same way you would a React component body **in root count only**: one parent node containing the entire route markup, including any owned script. Inside that root, write plain HTML with PulsePoint directives — `hidden="{...}"` for conditionals, `<template pp-for="…">` for lists, and quoted brace attributes such as `class="{…}"`. JSX shapes (`{cond && <div/>}`, `{list.map(...)}`, `class={...}`) break the render.
|
|
193
193
|
|
|
194
194
|
Good:
|
|
195
195
|
|
|
@@ -62,7 +62,7 @@ The script is composed so the export always runs against a **fresh route/compone
|
|
|
62
62
|
|
|
63
63
|
- boots the real ASGI app in-process and requests every route through Starlette's `TestClient`, so exported HTML is byte-identical to what the dev server serves (layouts, components, PulsePoint deferral, security headers — the whole pipeline),
|
|
64
64
|
- writes each page to `static/<route>/index.html` (`/` → `static/index.html`),
|
|
65
|
-
- mirrors public
|
|
65
|
+
- mirrors the complete `public/**` tree into `static/`, including app-defined directories such as `icons` and `fonts`,
|
|
66
66
|
- runs in `APP_ENV=development` so the build does not require production secrets.
|
|
67
67
|
|
|
68
68
|
**Scope policy is "warn & skip"** — anything that cannot be fully static is reported and NOT written, so nothing broken ships silently:
|