@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.
- package/dist/webjs-core-browser.js +5 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +20 -20
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +7 -7
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +1 -1
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/component/lifecycle.js +1481 -0
- package/src/component/reactive.js +171 -0
- package/src/component/server-element.js +248 -0
- package/src/component.js +2 -1896
- package/src/render-client/parts.js +2007 -0
- package/src/render-client/reconciler.js +388 -0
- package/src/render-client/template-compiler.js +525 -0
- package/src/render-client.js +1 -2938
- package/src/render-server/attrs.js +160 -0
- package/src/render-server/dsd.js +445 -0
- package/src/render-server/html-scan.js +402 -0
- package/src/render-server/slots.js +213 -0
- package/src/render-server/stream.js +163 -0
- package/src/render-server/template-renderer.js +923 -0
- package/src/render-server/text.js +163 -0
- package/src/render-server.js +6 -2405
- package/src/router-client/anchors.js +44 -0
- package/src/router-client/boundaries.js +221 -0
- package/src/router-client/constants.js +187 -0
- package/src/router-client/diagnostics.js +377 -0
- package/src/router-client/dom-differ.js +709 -0
- package/src/router-client/dom-parse.js +150 -0
- package/src/router-client/events.js +139 -0
- package/src/router-client/fetch-apply.js +387 -0
- package/src/router-client/form-encoder.js +135 -0
- package/src/router-client/frames.js +329 -0
- package/src/router-client/head-merge.js +344 -0
- package/src/router-client/nav-error.js +105 -0
- package/src/router-client/navigator.js +781 -0
- package/src/router-client/prefetch.js +824 -0
- package/src/router-client/scroll.js +259 -0
- package/src/router-client/snapshot-cache.js +97 -0
- package/src/router-client/state.js +92 -0
- package/src/router-client/stream.js +206 -0
- package/src/router-client/swap.js +451 -0
- package/src/router-client/upgrade.js +261 -0
- package/src/router-client/view-transition.js +223 -0
- package/src/router-client.d.ts +10 -1
- package/src/router-client.js +75 -5345
- package/src/slot/assignment.js +304 -0
- package/src/slot/interception.js +558 -0
- package/src/slot/polyfills.js +365 -0
- package/src/slot/project.js +432 -0
- package/src/slot/sensors.js +303 -0
- package/src/slot/state.js +245 -0
- package/src/slot/symbols.js +85 -0
- 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
|
+
}
|