@webjsdev/core 0.7.50 → 0.7.51

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 (57) hide show
  1. package/dist/webjs-core-browser.js +5 -5
  2. package/dist/webjs-core-browser.js.map +3 -3
  3. package/dist/webjs-core-testing.js +20 -20
  4. package/dist/webjs-core-testing.js.map +3 -3
  5. package/dist/webjs-core.js +7 -7
  6. package/dist/webjs-core.js.map +3 -3
  7. package/index-browser.js +1 -1
  8. package/index.d.ts +1 -1
  9. package/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/component/lifecycle.js +1481 -0
  12. package/src/component/reactive.js +171 -0
  13. package/src/component/server-element.js +248 -0
  14. package/src/component.js +2 -1896
  15. package/src/render-client/parts.js +2007 -0
  16. package/src/render-client/reconciler.js +388 -0
  17. package/src/render-client/template-compiler.js +525 -0
  18. package/src/render-client.js +1 -2938
  19. package/src/render-server/attrs.js +160 -0
  20. package/src/render-server/dsd.js +445 -0
  21. package/src/render-server/html-scan.js +402 -0
  22. package/src/render-server/slots.js +213 -0
  23. package/src/render-server/stream.js +163 -0
  24. package/src/render-server/template-renderer.js +923 -0
  25. package/src/render-server/text.js +163 -0
  26. package/src/render-server.js +6 -2405
  27. package/src/router-client/anchors.js +44 -0
  28. package/src/router-client/boundaries.js +221 -0
  29. package/src/router-client/constants.js +187 -0
  30. package/src/router-client/diagnostics.js +377 -0
  31. package/src/router-client/dom-differ.js +709 -0
  32. package/src/router-client/dom-parse.js +150 -0
  33. package/src/router-client/events.js +139 -0
  34. package/src/router-client/fetch-apply.js +387 -0
  35. package/src/router-client/form-encoder.js +135 -0
  36. package/src/router-client/frames.js +329 -0
  37. package/src/router-client/head-merge.js +344 -0
  38. package/src/router-client/nav-error.js +105 -0
  39. package/src/router-client/navigator.js +781 -0
  40. package/src/router-client/prefetch.js +824 -0
  41. package/src/router-client/scroll.js +259 -0
  42. package/src/router-client/snapshot-cache.js +97 -0
  43. package/src/router-client/state.js +92 -0
  44. package/src/router-client/stream.js +206 -0
  45. package/src/router-client/swap.js +451 -0
  46. package/src/router-client/upgrade.js +261 -0
  47. package/src/router-client/view-transition.js +223 -0
  48. package/src/router-client.d.ts +10 -1
  49. package/src/router-client.js +75 -5345
  50. package/src/slot/assignment.js +304 -0
  51. package/src/slot/interception.js +558 -0
  52. package/src/slot/polyfills.js +365 -0
  53. package/src/slot/project.js +432 -0
  54. package/src/slot/sensors.js +303 -0
  55. package/src/slot/state.js +245 -0
  56. package/src/slot/symbols.js +85 -0
  57. package/src/slot.js +7 -2199
@@ -0,0 +1,163 @@
1
+ import { cspNonce } from '../csp-nonce.js';
2
+ import { escapeAttr } from '../escape.js';
3
+ import { render, defaultSSRErrorTemplate } from './template-renderer.js';
4
+ import { injectDSD } from './dsd.js';
5
+
6
+ /**
7
+ * Render a TemplateResult (or any renderable value) to an HTML string.
8
+ *
9
+ * Async by design: template holes may be Promises, components' `render()`
10
+ * methods may be async, and data-fetching inside nested components is
11
+ * awaited before the final string is emitted.
12
+ *
13
+ * If `opts.suspenseCtx` is provided, Suspense boundaries encountered during
14
+ * the render will push `{ id, promise }` into `opts.suspenseCtx.pending`
15
+ * and their fallback HTML is emitted immediately. The caller is responsible
16
+ * for streaming each resolved promise afterwards. Without a suspenseCtx,
17
+ * Suspense still works but we fall back to emitting only the fallback
18
+ * (the promise is dropped: appropriate for static pre-render).
19
+ *
20
+ * @typedef {{ pending: {id: string, promise: Promise<unknown>}[], nextId: number }} SuspenseCtx
21
+ *
22
+ * A boundary's enclosing form scope used to be recorded here and threaded back
23
+ * in (#1207), because the page pipeline in `@webjsdev/server` drains
24
+ * `ctx.pending` and re-renders each resolved child through a FRESH scan with no
25
+ * view of the shell it belongs to, so a `<button formaction=${fn}>` inside a
26
+ * bound form's Suspense boundary read as form-less and was refused. Gone with
27
+ * #1307: a bound submitter carries its own `formmethod` and enctype, so no
28
+ * renderer needs to know what encloses it and there is nothing left to thread.
29
+ *
30
+ * @param {unknown} value
31
+ * @param {{ ssr?: boolean, suspenseCtx?: SuspenseCtx, dev?: boolean }} [opts]
32
+ * @returns {Promise<string>}
33
+ */
34
+ export async function renderToString(value, opts = { ssr: true }) {
35
+ const ctx = opts && opts.suspenseCtx;
36
+ // The server `dev` flag drives prod-silence of SSR error states (#483).
37
+ // `opts.dev` wins; else inherit a `dev` already stamped on the ctx (so a
38
+ // streamed sub-context inherits it); back-fill the ctx so downstream renders
39
+ // sharing it see the same flag. Undefined stays undefined (NODE_ENV fallback).
40
+ const dev = opts && opts.dev !== undefined ? opts.dev : ctx && ctx.dev;
41
+ if (ctx && ctx.dev === undefined && dev !== undefined) ctx.dev = dev;
42
+ const html = await render(value, ctx);
43
+ return opts && opts.ssr === false ? html : await injectDSD(html, ctx, [], dev);
44
+ }
45
+
46
+ /**
47
+ * Render a TemplateResult (or any renderable value) to a `ReadableStream`
48
+ * that yields HTML chunks as strings.
49
+ *
50
+ * Works identically to {@link renderToString} but streams partial HTML as
51
+ * it is rendered: avoiding buffering the entire page in memory. For
52
+ * Suspense boundaries, the fallback is yielded immediately and resolved
53
+ * content is streamed afterwards at the end of the response.
54
+ *
55
+ * **AI hint:** Use `renderToStream` when you want to pipe SSR output
56
+ * directly into a `Response` for streaming delivery (e.g. HTTP chunked
57
+ * transfer). It accepts the same arguments as `renderToString`.
58
+ *
59
+ * @param {unknown} value A TemplateResult, string, array, or any renderable.
60
+ * @param {{ ssr?: boolean, suspenseCtx?: SuspenseCtx }} [opts]
61
+ * @returns {ReadableStream<string>}
62
+ */
63
+ export function renderToStream(value, opts = { ssr: true }) {
64
+ const ctx = opts && opts.suspenseCtx;
65
+ // Server dev flag for prod-silence of SSR error states (#483), same sourcing
66
+ // as renderToString: opts.dev wins, else inherit from the ctx, else undefined
67
+ // (NODE_ENV fallback). Back-fill the ctx so the streamed sub-renders share it.
68
+ const dev = opts && opts.dev !== undefined ? opts.dev : ctx && ctx.dev;
69
+ if (ctx && ctx.dev === undefined && dev !== undefined) ctx.dev = dev;
70
+ return new ReadableStream({
71
+ async start(controller) {
72
+ try {
73
+ if (opts && opts.ssr === false) {
74
+ // No DSD injection: just stream the raw rendered chunks.
75
+ const { streamRender } = await import('./template-renderer.js');
76
+ await streamRender(value, ctx, controller);
77
+ } else {
78
+ // Render to string first to run DSD injection (which operates on
79
+ // the full HTML), then enqueue the result. This matches the
80
+ // semantics of renderToString but still gives us a stream.
81
+ const html = await render(value, ctx);
82
+ const full = await injectDSD(html, ctx, [], dev);
83
+ controller.enqueue(full);
84
+ }
85
+
86
+ // Stream resolved Suspense boundaries after the main content.
87
+ if (ctx && ctx.pending.length) {
88
+ await streamSuspenseBoundaries(ctx, controller, dev);
89
+ }
90
+ controller.close();
91
+ } catch (err) {
92
+ controller.error(err);
93
+ }
94
+ },
95
+ });
96
+ }
97
+
98
+ /**
99
+ * After the main HTML has been streamed, resolve pending Suspense promises
100
+ * and stream their replacement content as out-of-order `<template>` tags
101
+ * with tiny inline scripts that swap the fallback for the resolved HTML.
102
+ *
103
+ * @param {SuspenseCtx} ctx
104
+ * @param {ReadableStreamDefaultController<string>} controller
105
+ * @param {boolean} [dev] server dev flag for prod-silence of a rejected
106
+ * streamed boundary (#483); undefined falls back to NODE_ENV
107
+ */
108
+ async function streamSuspenseBoundaries(ctx, controller, dev) {
109
+ // Resolve the per-request nonce once per call. The provider in
110
+ // @webjsdev/server sources it from AsyncLocalStorage; outside a
111
+ // request scope (or in the browser) the helper returns '' and we
112
+ // emit the script unnonced, which is fine on documents not under
113
+ // strict CSP and matches the no-nonce case for the rest of the
114
+ // SSR pipeline.
115
+ const nonce = cspNonce();
116
+ const nonceAttr = nonce ? ` nonce="${escapeAttr(nonce)}"` : '';
117
+ while (ctx.pending.length) {
118
+ const batch = ctx.pending.splice(0);
119
+ await Promise.all(
120
+ batch.map(async ({ id, promise }) => {
121
+ try {
122
+ const resolved = await promise;
123
+ const html = await render(resolved, ctx);
124
+ const full = await injectDSD(html, ctx, [], dev);
125
+ controller.enqueue(
126
+ `<template data-webjs-resolve="${id}">${full}</template>` +
127
+ `<script${nonceAttr}>` +
128
+ `(function(){` +
129
+ `var t=document.currentScript.previousElementSibling;` +
130
+ `var b=document.getElementById("${id}");` +
131
+ `if(b&&t){b.replaceWith(t.content.cloneNode(true));t.remove()}` +
132
+ `document.currentScript.remove()` +
133
+ `})()` +
134
+ `</script>`
135
+ );
136
+ } catch (err) {
137
+ console.error(`[webjs] Suspense boundary "${id}" rejected:`, err);
138
+ // Render a boundary-scoped error state rather than leaving the
139
+ // fallback stuck forever (#471). Dev surfaces the message; prod
140
+ // renders a silent empty element (no leak). A failure HERE (the
141
+ // error render itself throwing) leaves the fallback in place.
142
+ try {
143
+ const e = err instanceof Error ? err : new Error(String(err));
144
+ const errHtml = await injectDSD(await render(defaultSSRErrorTemplate('webjs-suspense', e, dev), ctx), ctx, [], dev);
145
+ controller.enqueue(
146
+ `<template data-webjs-resolve="${id}">${errHtml}</template>` +
147
+ `<script${nonceAttr}>` +
148
+ `(function(){` +
149
+ `var t=document.currentScript.previousElementSibling;` +
150
+ `var b=document.getElementById("${id}");` +
151
+ `if(b&&t){b.replaceWith(t.content.cloneNode(true));t.remove()}` +
152
+ `document.currentScript.remove()` +
153
+ `})()` +
154
+ `</script>`
155
+ );
156
+ } catch (errorRenderThrew) {
157
+ console.error(`[webjs] Suspense boundary "${id}" error render also threw:`, errorRenderThrew);
158
+ }
159
+ }
160
+ })
161
+ );
162
+ }
163
+ }