Merge branch 'worktree-iso-phase-4' into macros
This commit is contained in:
@@ -864,22 +864,31 @@ if(window.Sx&&Sx.resolveSuspense){Sx.resolveSuspense(i,s)}\
|
|||||||
else{window.__sxPending.push({id:i,sx:s})}}</script>"""
|
else{window.__sxPending.push({id:i,sx:s})}}</script>"""
|
||||||
|
|
||||||
|
|
||||||
def sx_page_streaming_parts(ctx: dict, page_sx: str, *,
|
def sx_page_streaming_parts(ctx: dict, page_html: str, *,
|
||||||
|
page_sx: str = "",
|
||||||
meta_html: str = "") -> tuple[str, str]:
|
meta_html: str = "") -> tuple[str, str]:
|
||||||
"""Split the page into shell (before scripts) and tail (scripts).
|
"""Split the page into shell (before scripts) and tail (scripts).
|
||||||
|
|
||||||
Returns (shell, tail) where:
|
For streaming, the initial page is rendered to **HTML** server-side so
|
||||||
shell = everything up to and including the page SX mount script
|
``[data-suspense]`` elements are in the DOM immediately — no client-side
|
||||||
tail = the suspense bootstrap + sx-browser.js + body.js scripts
|
SX rendering needed for the shell. Resolution scripts can find and
|
||||||
|
replace suspense placeholders without waiting for sx-browser.js to boot.
|
||||||
|
|
||||||
For streaming, the caller yields shell first, then resolution chunks,
|
Args:
|
||||||
then tail to close the document.
|
page_html: Server-rendered HTML for the page body (with suspense
|
||||||
|
placeholders already as real HTML elements).
|
||||||
|
page_sx: SX source scanned for component deps (may differ from
|
||||||
|
page_html when components were expanded server-side).
|
||||||
"""
|
"""
|
||||||
from .jinja_bridge import components_for_page, css_classes_for_page
|
from .jinja_bridge import components_for_page, css_classes_for_page
|
||||||
from .css_registry import lookup_rules, get_preamble, registry_loaded, store_css_hash
|
from .css_registry import lookup_rules, get_preamble, registry_loaded, store_css_hash
|
||||||
|
|
||||||
from quart import current_app as _ca
|
from quart import current_app as _ca
|
||||||
component_defs, component_hash = components_for_page(page_sx, service=_ca.name)
|
|
||||||
|
# Scan the SX source for component deps (needed for resolution scripts
|
||||||
|
# that may contain component calls the client must render)
|
||||||
|
scan_source = page_sx or page_html
|
||||||
|
component_defs, component_hash = components_for_page(scan_source, service=_ca.name)
|
||||||
|
|
||||||
client_hash = _get_sx_comp_cookie()
|
client_hash = _get_sx_comp_cookie()
|
||||||
if not _is_dev_mode() and client_hash and client_hash == component_hash:
|
if not _is_dev_mode() and client_hash and client_hash == component_hash:
|
||||||
@@ -888,7 +897,7 @@ def sx_page_streaming_parts(ctx: dict, page_sx: str, *,
|
|||||||
sx_css = ""
|
sx_css = ""
|
||||||
sx_css_classes = ""
|
sx_css_classes = ""
|
||||||
if registry_loaded():
|
if registry_loaded():
|
||||||
classes = css_classes_for_page(page_sx, service=_ca.name)
|
classes = css_classes_for_page(scan_source, service=_ca.name)
|
||||||
classes.update(["bg-stone-50", "text-stone-900"])
|
classes.update(["bg-stone-50", "text-stone-900"])
|
||||||
rules = lookup_rules(classes)
|
rules = lookup_rules(classes)
|
||||||
sx_css = get_preamble() + rules
|
sx_css = get_preamble() + rules
|
||||||
@@ -898,13 +907,6 @@ def sx_page_streaming_parts(ctx: dict, page_sx: str, *,
|
|||||||
title = ctx.get("base_title", "Rose Ash")
|
title = ctx.get("base_title", "Rose Ash")
|
||||||
csrf = _get_csrf_token()
|
csrf = _get_csrf_token()
|
||||||
|
|
||||||
if _is_dev_mode() and page_sx and page_sx.startswith("("):
|
|
||||||
from .parser import parse as _parse, serialize as _serialize
|
|
||||||
try:
|
|
||||||
page_sx = _serialize(_parse(page_sx), pretty=True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
styles_hash = _get_style_dict_hash()
|
styles_hash = _get_style_dict_hash()
|
||||||
client_styles_hash = _get_sx_styles_cookie()
|
client_styles_hash = _get_sx_styles_cookie()
|
||||||
styles_json = "" if (not _is_dev_mode() and client_styles_hash == styles_hash) else _build_style_dict_json()
|
styles_json = "" if (not _is_dev_mode() and client_styles_hash == styles_hash) else _build_style_dict_json()
|
||||||
@@ -916,7 +918,7 @@ def sx_page_streaming_parts(ctx: dict, page_sx: str, *,
|
|||||||
sx_js_hash = _script_hash("sx-browser.js")
|
sx_js_hash = _script_hash("sx-browser.js")
|
||||||
body_js_hash = _script_hash("body.js")
|
body_js_hash = _script_hash("body.js")
|
||||||
|
|
||||||
# Shell: everything up to and including the page SX
|
# Shell: head + body with server-rendered HTML (not SX mount script)
|
||||||
shell = (
|
shell = (
|
||||||
'<!doctype html>\n<html lang="en">\n<head>\n'
|
'<!doctype html>\n<html lang="en">\n<head>\n'
|
||||||
'<meta charset="utf-8">\n'
|
'<meta charset="utf-8">\n'
|
||||||
@@ -954,7 +956,8 @@ def sx_page_streaming_parts(ctx: dict, page_sx: str, *,
|
|||||||
f'<script type="text/sx-styles" data-hash="{styles_hash}">{styles_json}</script>\n'
|
f'<script type="text/sx-styles" data-hash="{styles_hash}">{styles_json}</script>\n'
|
||||||
f'<script type="text/sx" data-components data-hash="{component_hash}">{component_defs}</script>\n'
|
f'<script type="text/sx" data-components data-hash="{component_hash}">{component_defs}</script>\n'
|
||||||
f'<script type="text/sx-pages">{pages_sx}</script>\n'
|
f'<script type="text/sx-pages">{pages_sx}</script>\n'
|
||||||
f'<script type="text/sx" data-mount="body">{page_sx}</script>\n'
|
# Server-rendered HTML — suspense placeholders are real DOM elements
|
||||||
|
f'{page_html}\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Tail: bootstrap suspense resolver + scripts + close
|
# Tail: bootstrap suspense resolver + scripts + close
|
||||||
|
|||||||
@@ -330,7 +330,8 @@ async def execute_page_streaming(
|
|||||||
from .async_eval import async_eval
|
from .async_eval import async_eval
|
||||||
from .page import get_template_context
|
from .page import get_template_context
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
_render_to_sx, sx_page_streaming_parts,
|
render_to_html as _helpers_render_to_html,
|
||||||
|
sx_page_streaming_parts,
|
||||||
sx_streaming_resolve_script,
|
sx_streaming_resolve_script,
|
||||||
)
|
)
|
||||||
from .parser import SxExpr, serialize as sx_serialize
|
from .parser import SxExpr, serialize as sx_serialize
|
||||||
@@ -413,18 +414,24 @@ async def execute_page_streaming(
|
|||||||
data_task = asyncio.create_task(_eval_data_and_content())
|
data_task = asyncio.create_task(_eval_data_and_content())
|
||||||
header_task = asyncio.create_task(_eval_headers())
|
header_task = asyncio.create_task(_eval_headers())
|
||||||
|
|
||||||
# --- Build initial shell (still in request context) ---
|
# --- Build initial shell as HTML (still in request context) ---
|
||||||
|
# Render to HTML so [data-suspense] elements are real DOM immediately.
|
||||||
|
# No dependency on sx-browser.js boot timing for the initial shell.
|
||||||
|
|
||||||
initial_page_sx = await _render_to_sx("app-body",
|
suspense_header_sx = f'(~suspense :id "stream-headers" :fallback {header_fallback})'
|
||||||
header_rows=SxExpr(
|
suspense_content_sx = f'(~suspense :id "stream-content" :fallback {fallback_sx})'
|
||||||
f'(~suspense :id "stream-headers" :fallback {header_fallback})'
|
|
||||||
),
|
initial_page_html = await _helpers_render_to_html("app-body",
|
||||||
content=SxExpr(
|
header_rows=SxExpr(suspense_header_sx),
|
||||||
f'(~suspense :id "stream-content" :fallback {fallback_sx})'
|
content=SxExpr(suspense_content_sx),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
shell, tail = sx_page_streaming_parts(tctx, initial_page_sx)
|
# Pass the SX source for component scanning (resolution scripts may
|
||||||
|
# contain component calls that the client needs to render)
|
||||||
|
page_sx_for_scan = f'(~app-body :header-rows {suspense_header_sx} :content {suspense_content_sx})'
|
||||||
|
shell, tail = sx_page_streaming_parts(
|
||||||
|
tctx, initial_page_html, page_sx=page_sx_for_scan,
|
||||||
|
)
|
||||||
|
|
||||||
# --- Return async generator that yields chunks ---
|
# --- Return async generator that yields chunks ---
|
||||||
# No context access needed below — just awaiting tasks and yielding strings.
|
# No context access needed below — just awaiting tasks and yielding strings.
|
||||||
|
|||||||
Reference in New Issue
Block a user