@webjsdev/server 0.8.63 → 0.8.65
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/index.d.ts +7 -2
- package/package.json +2 -2
- package/src/browser-entries.js +64 -0
- package/src/check/helpers.js +365 -0
- package/src/check/rules-actions.js +174 -0
- package/src/check/rules-components.js +275 -0
- package/src/check/rules-imports.js +166 -0
- package/src/check/rules-registry.js +128 -0
- package/src/check/rules-routing.js +224 -0
- package/src/check/rules-typescript.js +177 -0
- package/src/check/rules.js +127 -0
- package/src/check/runner-support.js +405 -0
- package/src/check/runner.js +113 -0
- package/src/check.js +4 -1774
- package/src/component-elision.js +173 -26
- package/src/dev/config.js +338 -0
- package/src/dev/handler.js +1372 -0
- package/src/dev/helpers.js +542 -0
- package/src/dev/serve.js +840 -0
- package/src/dev/server.js +322 -0
- package/src/dev-classify.js +174 -0
- package/src/dev-reload-worker.js +133 -7
- package/src/dev-styles.js +97 -0
- package/src/dev.js +22 -3077
- package/src/form-dispatch.js +12 -3
- package/src/importmap.js +6 -3
- package/src/listener-core.js +16 -2
- package/src/module-graph.js +112 -7
- package/src/router.js +11 -1
- package/src/ssr/client-router-flag.js +42 -0
- package/src/ssr/document.js +361 -0
- package/src/ssr/env-shim.js +51 -0
- package/src/ssr/escape.js +29 -0
- package/src/ssr/head.js +857 -0
- package/src/ssr/preloads.js +195 -0
- package/src/ssr/render.js +1481 -0
- package/src/ssr/responses.js +201 -0
- package/src/ssr.js +28 -2445
- package/src/vendor/audit.js +400 -0
- package/src/vendor/integrity.js +245 -0
- package/src/vendor/jspm.js +348 -0
- package/src/vendor/manifest.js +96 -0
- package/src/vendor/pins.js +892 -0
- package/src/vendor/providers.js +35 -0
- package/src/vendor/resolver.js +225 -0
- package/src/vendor/scanner.js +132 -0
- package/src/vendor.js +29 -2371
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { cspNonce, renderToString } from '@webjsdev/core';
|
|
2
|
+
import { BUFFERED_MARKER, STREAM_MARKER } from '../conditional-get.js';
|
|
3
|
+
import { publishedBuildId, appSourceId } from '../importmap.js';
|
|
4
|
+
import { escapeAttr, escapeHtml } from './escape.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Turning rendered HTML into a Response: the per-request CSP nonce read and
|
|
8
|
+
* the three builders (cached, buffered and streaming).
|
|
9
|
+
*
|
|
10
|
+
* Split off render.js, which owns the RENDER. Nothing here renders anything;
|
|
11
|
+
* it decides status, headers and framing for bytes it is handed.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The CSP nonce for the in-flight request, or undefined if none is in
|
|
16
|
+
* scope. Delegates to `cspNonce()`, which returns the per-request nonce
|
|
17
|
+
* the handler MINTED when CSP is enabled (issue #233), or, as a fallback,
|
|
18
|
+
* the nonce parsed from an inbound `Content-Security-Policy` request
|
|
19
|
+
* header (the legacy consume-only path). Using the same source as the
|
|
20
|
+
* `Content-Security-Policy` response header is what guarantees the inline
|
|
21
|
+
* boot script, the importmap, the modulepreload hints, and the header all
|
|
22
|
+
* carry the EXACT same nonce: one minted value, no drift.
|
|
23
|
+
*
|
|
24
|
+
* `req` is accepted (and ignored) so existing call sites stay unchanged;
|
|
25
|
+
* the value comes from the request-scoped AsyncLocalStorage store, not
|
|
26
|
+
* the argument.
|
|
27
|
+
*
|
|
28
|
+
* @param {Request} [_req]
|
|
29
|
+
* @returns {string | undefined}
|
|
30
|
+
*/
|
|
31
|
+
export function getNonce(_req) {
|
|
32
|
+
return cspNonce() || undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Rebuild a Response from a cached HTML record (#241). The stored body is
|
|
37
|
+
* the stable per-page HTML; the per-response varying bits are re-minted
|
|
38
|
+
* here so a new visitor still gets them: the published build id is re-read so
|
|
39
|
+
* a post-deploy client sees the current id. No cookie is set (action CSRF is
|
|
40
|
+
* an Origin / Sec-Fetch-Site check), which is what keeps a cached page
|
|
41
|
+
* cookieless and shareable. The BUFFERED marker opts the cached body into
|
|
42
|
+
* the conditional-GET funnel exactly as a fresh render does, so a cached
|
|
43
|
+
* PUBLIC-cacheable page still 304s. Output is observably identical to the
|
|
44
|
+
* fresh render of the same route within the window.
|
|
45
|
+
*
|
|
46
|
+
* @param {{ body: string, contentType: string, cacheControl: string, status: number }} rec
|
|
47
|
+
* @param {Request | undefined} req
|
|
48
|
+
* @param {URL | undefined} url
|
|
49
|
+
*/
|
|
50
|
+
export function cachedHtmlResponse(rec, req, url) {
|
|
51
|
+
const headers = new Headers({ 'content-type': rec.contentType });
|
|
52
|
+
headers.set('cache-control', rec.cacheControl);
|
|
53
|
+
headers.set('x-webjs-build', publishedBuildId());
|
|
54
|
+
headers.set('x-webjs-src', appSourceId());
|
|
55
|
+
headers.set(BUFFERED_MARKER, '1');
|
|
56
|
+
return new Response(rec.body, { status: rec.status, headers });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build an HTML Response. Sets no cookie: action CSRF is an Origin /
|
|
61
|
+
* Sec-Fetch-Site check, so the page response is cookieless (CDN-cacheable).
|
|
62
|
+
* @param {string} html
|
|
63
|
+
* @param {number} status
|
|
64
|
+
* @param {Request | undefined} req
|
|
65
|
+
* @param {URL | undefined} url
|
|
66
|
+
* @param {Record<string, any>} [metadata]
|
|
67
|
+
*/
|
|
68
|
+
export function htmlResponse(html, status, req, url, metadata) {
|
|
69
|
+
const headers = new Headers({ 'content-type': 'text/html; charset=utf-8' });
|
|
70
|
+
// Default: no caching. Pages are dynamic by default: the developer opts in
|
|
71
|
+
// explicitly via metadata.cacheControl. No non-200 guard here, unlike
|
|
72
|
+
// streamingHtmlResponse: every caller of THIS builder passes no metadata, so
|
|
73
|
+
// the value is already the no-store default and a guard would be dead code.
|
|
74
|
+
headers.set('cache-control', metadata?.cacheControl || 'no-store');
|
|
75
|
+
// X-Webjs-Build carries the published build id so the client
|
|
76
|
+
// router can detect post-deploy importmap changes on EVERY
|
|
77
|
+
// response, including the X-Webjs-Have partial responses that
|
|
78
|
+
// omit the head entirely. Empty until the map is authoritatively
|
|
79
|
+
// final, so a warming response is reload-safe. See router-client.js
|
|
80
|
+
// applySwap and publishedBuildId() in importmap.js.
|
|
81
|
+
headers.set('x-webjs-build', publishedBuildId());
|
|
82
|
+
headers.set('x-webjs-src', appSourceId());
|
|
83
|
+
// Buffered (string) body: opt into the conditional-GET funnel.
|
|
84
|
+
// A cacheable page (metadata.cacheControl) gets a weak ETag + 304. The
|
|
85
|
+
// funnel excludes only the no-store default; a `private` page IS validated,
|
|
86
|
+
// which is what keeps the router's partial responses cheap (#1140).
|
|
87
|
+
// See conditional-get.js.
|
|
88
|
+
headers.set(BUFFERED_MARKER, '1');
|
|
89
|
+
return new Response(html, { status, headers });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build a streaming Response. Degrades to a single-flush response when
|
|
94
|
+
* there are no pending Suspense boundaries.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} prefix
|
|
97
|
+
* @param {string} bodyHtml
|
|
98
|
+
* @param {string} closer
|
|
99
|
+
* @param {{ pending: {id: string, promise: Promise<unknown>}[], nextId: number }} ctx
|
|
100
|
+
* @param {number} status
|
|
101
|
+
* @param {Request | undefined} req
|
|
102
|
+
* @param {URL | undefined} url
|
|
103
|
+
* @param {Record<string, any>} [metadata]
|
|
104
|
+
* @param {string} [nonce]
|
|
105
|
+
* @param {boolean} [dev] dev surfaces a streamed-boundary error message; prod stays silent
|
|
106
|
+
*/
|
|
107
|
+
export function streamingHtmlResponse(prefix, bodyHtml, closer, ctx, status, req, url, metadata, nonce, dev) {
|
|
108
|
+
const encoder = new TextEncoder();
|
|
109
|
+
const headers = new Headers({ 'content-type': 'text/html; charset=utf-8' });
|
|
110
|
+
// Default: no caching. Pages are dynamic by default: the developer
|
|
111
|
+
// opts in to caching explicitly via metadata.cacheControl. A non-200 does
|
|
112
|
+
// NOT inherit it (#1140): the form-action re-render is a 422 carrying the
|
|
113
|
+
// submitter's own field values and errors, which must never be handed to a
|
|
114
|
+
// shared cache just because the page opted into public caching.
|
|
115
|
+
headers.set('cache-control', status === 200 ? (metadata?.cacheControl || 'no-store') : 'no-store');
|
|
116
|
+
// See htmlResponse: published build id on every response for the
|
|
117
|
+
// client router's importmap-mismatch detection on partial swaps.
|
|
118
|
+
headers.set('x-webjs-build', publishedBuildId());
|
|
119
|
+
headers.set('x-webjs-src', appSourceId());
|
|
120
|
+
|
|
121
|
+
if (!ctx.pending.length) {
|
|
122
|
+
// No pending boundaries: this degrades to a single buffered (string)
|
|
123
|
+
// flush, so opt it into the conditional-GET funnel like htmlResponse.
|
|
124
|
+
headers.set(BUFFERED_MARKER, '1');
|
|
125
|
+
return new Response(prefix + bodyHtml + closer, { status, headers });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Flag a genuinely streamed body so the conditional-GET funnel skips it
|
|
129
|
+
// (an unflushed stream cannot be hashed without buffering, which would
|
|
130
|
+
// defeat streaming). The marker is internal and stripped at the funnel
|
|
131
|
+
// before the response reaches the client. See conditional-get.js.
|
|
132
|
+
headers.set(STREAM_MARKER, '1');
|
|
133
|
+
|
|
134
|
+
const stream = new ReadableStream({
|
|
135
|
+
async start(controller) {
|
|
136
|
+
// Flush the shell (prefix + body with fallbacks) immediately, followed by
|
|
137
|
+
// a shell-ready sentinel comment IN THE SAME chunk. The resolved boundary
|
|
138
|
+
// templates and the `</body></html>` closer are emitted LATER (after the
|
|
139
|
+
// slow data settles), so without this sentinel a streaming soft-nav client
|
|
140
|
+
// could not tell "shell complete, awaiting the slow boundary" from "shell
|
|
141
|
+
// still arriving" and would block its progressive swap until the slow
|
|
142
|
+
// boundary (#473). The comment is inert for the native initial-load parse.
|
|
143
|
+
controller.enqueue(encoder.encode(prefix + bodyHtml + '<!--wj-stream-shell-->'));
|
|
144
|
+
try {
|
|
145
|
+
// Loop: resolve all currently-pending promises in parallel; nested
|
|
146
|
+
// Suspense inside resolved content adds more pending entries.
|
|
147
|
+
while (ctx.pending.length) {
|
|
148
|
+
const batch = ctx.pending.slice();
|
|
149
|
+
ctx.pending.length = 0;
|
|
150
|
+
const settled = await Promise.all(
|
|
151
|
+
batch.map(async (p) => {
|
|
152
|
+
try {
|
|
153
|
+
const resolved = await p.promise;
|
|
154
|
+
const sub = { pending: [], nextId: ctx.nextId, dev: ctx.dev };
|
|
155
|
+
// A fresh scan that cannot see the shell the boundary sits in.
|
|
156
|
+
// That used to require carrying the boundary's form scope
|
|
157
|
+
// (#1207), or a `<button formaction=${fn}>` inside a bound
|
|
158
|
+
// form's boundary read as form-less, was refused, and the catch
|
|
159
|
+
// below turned it into an empty boundary on a 200 in
|
|
160
|
+
// production. #1307 made a bound submitter self-sufficient, so
|
|
161
|
+
// there is nothing left to carry.
|
|
162
|
+
const html = await renderToString(resolved, { ssr: true, suspenseCtx: sub });
|
|
163
|
+
ctx.nextId = sub.nextId;
|
|
164
|
+
for (const n of sub.pending) ctx.pending.push(n);
|
|
165
|
+
return { id: p.id, html };
|
|
166
|
+
} catch (e) {
|
|
167
|
+
// Match the SSR error-isolation policy (render-server.js's
|
|
168
|
+
// defaultSSRErrorTemplate): dev surfaces the message so the
|
|
169
|
+
// failure is obvious, prod stays SILENT so no internal detail
|
|
170
|
+
// (a DB error, a stack-derived path) leaks to the client (#478).
|
|
171
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
172
|
+
const html = dev ? `<p>error: ${escapeHtml(msg)}</p>` : '';
|
|
173
|
+
return { id: p.id, html };
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
for (const r of settled) {
|
|
178
|
+
// Emit just the <template>: the MutationObserver-based resolver
|
|
179
|
+
// in the boot script detects it and swaps it into the placeholder.
|
|
180
|
+
// Falls back to the __webjsResolve global for browsers without MO.
|
|
181
|
+
// The fallback <script> carries the request's CSP nonce so
|
|
182
|
+
// strict-CSP enforcement passes. Browsers that support
|
|
183
|
+
// MutationObserver (all evergreen) handle the swap via the
|
|
184
|
+
// boot script's observer and skip this fallback; the
|
|
185
|
+
// <script> is here for legacy / extremely-restrictive
|
|
186
|
+
// environments. Either way it must be nonce-signed.
|
|
187
|
+
const scriptNonce = nonce ? ` nonce="${escapeAttr(nonce)}"` : '';
|
|
188
|
+
const chunk =
|
|
189
|
+
`<template data-webjs-resolve="${r.id}">${r.html}</template>` +
|
|
190
|
+
`<script${scriptNonce}>window.__webjsResolve&&__webjsResolve("${r.id}")</script>`;
|
|
191
|
+
controller.enqueue(encoder.encode(chunk));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} finally {
|
|
195
|
+
controller.enqueue(encoder.encode(closer));
|
|
196
|
+
controller.close();
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
return new Response(stream, { status, headers });
|
|
201
|
+
}
|