create-caspian-app 0.4.0-rc.8 → 0.5.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -95,6 +95,7 @@ This is the top architectural requirement for this workspace. Treat it as a hard
95
95
  ### `main.py`
96
96
 
97
97
  - Treat `main.py` as the repo source of truth for FastAPI setup, auth bootstrap, middleware wiring, route registration, cache defaults, and error handlers.
98
+ - `main.py` finalizes every rendered page through `finalize_html(...)` = `transform_scripts(...)` then `defer_component_roots(...)`. `defer_component_roots(...)` wraps each outermost `pp-component` root in an inert `<template pp-component>` so the browser never parses raw `{...}` placeholders as live DOM; the PulsePoint `mount()` bootstrap materializes them back before scanning. Because of this deferral, `{...}` is safe in any attribute or position (SVG `d`/`viewBox`/`points`, `src`/`href`, form `value`/date/number/color, table/select text). Do not add per-tag workarounds to dodge browser first-paint validation (static-path `hidden` toggles, `data-*` URL holders, `hidden`-gated `<img src>`, or SSR-resolved initial values). Keep `pp-style` (source-file tooling) and the controlled form-field `value`/`checked`/`defaultvalue`/`<textarea>` rewrites (attribute-vs-property correctness); those exist for reasons deferral does not replace.
98
99
  - Treat `main.py` as the source of truth for app-owned WebSocket endpoints, origin validation, idle timeouts, maximum socket message size, JSON message handling, close codes, and broadcast-channel wiring.
99
100
  - When the app factors response-header hardening or safe static-file behavior into app-owned helpers, treat `main.py` plus those imported helpers as the runtime source of truth together.
100
101
  - Preserve the effective middleware execution order unless the task explicitly changes request semantics: `SecurityHeadersMiddleware -> SessionMiddleware -> CSRFMiddleware -> AuthMiddleware -> RPCMiddleware`.
package/dist/AGENTS.md CHANGED
@@ -82,6 +82,7 @@ Use `.github/copilot-instructions.md` for the repo-wide implementation rules. Th
82
82
  - Before updating docs, verify runtime-specific claims such as middleware order, route param injection, `layout()` behavior, `StateManager` persistence, safe public-file serving, response header, or session-secret behavior against the current `main.py` and installed `casp` package, especially `.venv/Lib/site-packages/casp/runtime_security.py`, rather than copying older notes.
83
83
  - When generating or reviewing `src/app/**/index.html`, `src/app/**/layout.html`, or component HTML templates, treat the single-root rule as a hard requirement: exactly one authored top-level parent element or one imported `x-*` root, with any owned `<script>` kept inside that same root. Do not allow sibling top-level tags, sibling scripts, or stray top-level text, because Caspian injects `pp-component` on that final root and errors if it cannot.
84
84
  - When generating or reviewing sign-in flows, do not ask the sign-in page to decide redirect targets by re-implementing `next` support or post-login routing. In this stack, redirect behavior is already owned by the Caspian auth runtime plus `src/lib/auth/auth_config.py`; protected-route guest redirects, auth-route redirects, and the default destination are centralized there, with `default_signin_redirect` defaulting to `/dashboard`.
85
+ - Component markup is server-deferred in an inert `<template>`. `main.py` finalizes every page through `finalize_html(...)` = `transform_scripts(...)` then `defer_component_roots(...)`, which wraps each outermost `pp-component` root in `<template pp-component="…">`. The browser never parses/validates/fetches `<template>` contents, so raw `{...}` placeholders never reach live DOM at first paint; the PulsePoint `mount()` bootstrap materializes `template[pp-component]` back into live DOM (via `OwnedTemplateManager.materializeTemplateComponentBoundaries(document.body)`, which also runs on every SPA navigation) before it scans for roots. Because of this, `{...}` is safe in ANY attribute or position — SVG geometry (`d`, `viewBox`, `points`, `transform`), URL attributes (`src`, `srcset`, `href`, `poster`), form `value`/date/number/color, and text placed directly inside `<table>`/`<select>`. Do NOT add per-tag workarounds to dodge browser first-paint validation: no static-path `hidden` toggles just to avoid binding `d`, no `data-*` URL holders, no gating `<img src>` behind `hidden`, and no SSR-resolving an initial value only to prevent a validation flash. Two compiler transforms still apply for different reasons and stay: `pp-style` (so `.html` source-file HTML/CSS tooling does not choke on `style="{...}"`) and the `<input>`/`<select>`/`checked`/`defaultvalue`/`<textarea>` value rewrites (attribute-vs-property correctness for controlled form fields), not first-paint validation.
85
86
 
86
87
  ## Task Routing
87
88
 
@@ -132,4 +133,4 @@ Before merging doc or runtime changes:
132
133
  1. Compare the claim or behavior against `main.py`, `src/lib/**`, and `.venv/Lib/site-packages/casp/**`.
133
134
  2. Update the matching packaged doc in `node_modules/caspian-utils/dist/docs/` if the running behavior changed.
134
135
  3. Update `.github/copilot-instructions.md` if the repo-wide implementation rules changed.
135
- 4. Update this file if the decision order, task routing, workspace clarifications, or packaged-doc maintenance rules changed.
136
+ 4. Update this file if the decision order, task routing, workspace clarifications, or packaged-doc maintenance rules changed.
package/dist/main.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from casp.components_compiler import transform_components
2
2
  from casp.scripts_type import transform_scripts
3
+ from casp.html_native import parse_fragment, serialize_fragment
3
4
  import asyncio
4
5
  import inspect
5
6
  import os
@@ -214,6 +215,7 @@ async def combined_lifespan(app: FastAPI):
214
215
  async with AsyncExitStack() as stack:
215
216
  for lifespan in get_app_lifespans():
216
217
  await stack.enter_async_context(lifespan(app))
218
+
217
219
  yield
218
220
 
219
221
  app = FastAPI(
@@ -950,7 +952,7 @@ def register_single_route(url_pattern: str, file_path: str):
950
952
  component_compiler=transform_components
951
953
  )
952
954
 
953
- html_output = transform_scripts(html_output)
955
+ html_output = finalize_html(html_output)
954
956
  response = HTMLResponse(content=html_output)
955
957
  response.headers['X-PP-Root-Layout'] = root_layout_id
956
958
 
@@ -989,6 +991,63 @@ def register_single_route(url_pattern: str, file_path: str):
989
991
  methods=route_methods, name=endpoint)
990
992
 
991
993
 
994
+ def defer_component_roots(html_output: str) -> str:
995
+ """Wrap top-level ``[pp-component]`` roots in an inert ``<template>``.
996
+
997
+ The browser never parses/validates/fetches the contents of a ``<template>``
998
+ element, so raw ``{...}`` placeholders inside SVG geometry attributes, form
999
+ ``value``/date inputs, ``src``/``href`` URLs, or table/select structure no
1000
+ longer trigger console errors, bogus ``404`` requests, value coercion, or
1001
+ HTML foster-parenting before hydration. PulsePoint's ``mount()`` bootstrap
1002
+ materializes ``template[pp-component]`` back into live DOM (reusing the
1003
+ existing ``materializeTemplateComponentBoundaries`` path) before it scans
1004
+ for component roots, so post-hydration behavior is identical to today.
1005
+
1006
+ Only the outermost (non-nested) roots are wrapped; nested component
1007
+ boundaries ride along inside the inert content and become live when the
1008
+ outer template is materialized, so morphing and RPC re-render still operate
1009
+ on live ``[pp-component]`` DOM.
1010
+ """
1011
+ if 'pp-component' not in html_output:
1012
+ return html_output
1013
+
1014
+ soup = parse_fragment(html_output)
1015
+ body = soup.body
1016
+ if body is None:
1017
+ return html_output
1018
+
1019
+ roots = [
1020
+ el for el in body.select('[pp-component]')
1021
+ if el.name != 'template'
1022
+ and not any(
1023
+ parent.has_attr('pp-component') for parent in el.parents
1024
+ )
1025
+ ]
1026
+ if not roots:
1027
+ return html_output
1028
+
1029
+ for root in roots:
1030
+ key = root.get('pp-component')
1031
+ if key is None:
1032
+ continue
1033
+ template = soup.new_tag('template')
1034
+ template['pp-component'] = key
1035
+ root.insert_before(template)
1036
+ template.append(root.extract())
1037
+
1038
+ return serialize_fragment(soup)
1039
+
1040
+
1041
+ def finalize_html(html_output: str) -> str:
1042
+ """Final full-document transforms applied just before the response.
1043
+
1044
+ Runs ``transform_scripts`` (author ``<script>`` -> ``type="text/pp"``) then
1045
+ ``defer_component_roots`` so scripts are tagged before they are moved into
1046
+ the inert component ``<template>``.
1047
+ """
1048
+ return defer_component_roots(transform_scripts(html_output))
1049
+
1050
+
992
1051
  register_routes()
993
1052
  register_rpc_routes(app)
994
1053
 
@@ -1019,7 +1078,7 @@ async def custom_404_handler(request: Request, exc: StarletteHTTPException):
1019
1078
  context_data={'request': request},
1020
1079
  page_component_source=not_found_path,
1021
1080
  control_mode=True,
1022
- transform_fn=transform_scripts
1081
+ transform_fn=finalize_html
1023
1082
  )
1024
1083
  resp = HTMLResponse(content=html_output, status_code=404)
1025
1084
  resp.headers['X-PP-Root-Layout'] = root_layout_id
@@ -1054,7 +1113,7 @@ async def custom_general_exception_handler(request: Request, exc: Exception):
1054
1113
  context_data=context_data,
1055
1114
  page_component_source=error_page_path,
1056
1115
  control_mode=True,
1057
- transform_fn=transform_scripts
1116
+ transform_fn=finalize_html
1058
1117
  )
1059
1118
  resp = HTMLResponse(content=html_output, status_code=500)
1060
1119
  resp.headers['X-PP-Root-Layout'] = root_layout_id