@nifrajs/web 3.2.0 → 3.5.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.
- package/dist/build-plan.d.ts +17 -0
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js +7 -1
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts +14 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +29 -4
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +15 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +57 -15
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +18 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +115 -0
- package/dist/client.js.map +1 -1
- package/dist/css-contract.d.ts +21 -0
- package/dist/css-contract.d.ts.map +1 -0
- package/dist/css-contract.js +28 -0
- package/dist/css-contract.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/codegen.d.ts +3 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +11 -6
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/page-execution.d.ts +57 -0
- package/dist/internal/page-execution.d.ts.map +1 -0
- package/dist/internal/page-execution.js +555 -0
- package/dist/internal/page-execution.js.map +1 -0
- package/dist/internal/render-document.d.ts +12 -2
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +28 -5
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/request-url.d.ts +3 -0
- package/dist/internal/request-url.d.ts.map +1 -0
- package/dist/internal/request-url.js +14 -0
- package/dist/internal/request-url.js.map +1 -0
- package/dist/internal/web-app.d.ts +20 -1
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +37 -734
- package/dist/internal/web-app.js.map +1 -1
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +39 -4
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts +3 -0
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +6 -3
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +69 -44
- package/dist/public-dir.js.map +1 -1
- package/dist/trusted-html.d.ts +34 -0
- package/dist/trusted-html.d.ts.map +1 -0
- package/dist/trusted-html.js +35 -0
- package/dist/trusted-html.js.map +1 -0
- package/package.json +3 -3
- package/src/build-plan.ts +23 -1
- package/src/build-vite.ts +47 -4
- package/src/build.ts +69 -6
- package/src/client.ts +140 -0
- package/src/css-contract.ts +44 -0
- package/src/index.ts +15 -0
- package/src/internal/codegen.ts +20 -6
- package/src/internal/page-execution.ts +761 -0
- package/src/internal/render-document.ts +39 -6
- package/src/internal/request-url.ts +14 -0
- package/src/internal/web-app.ts +68 -835
- package/src/isr.ts +42 -3
- package/src/llms-txt.ts +16 -4
- package/src/manifest.ts +1 -1
- package/src/public-dir.ts +70 -45
- package/src/trusted-html.ts +44 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isSameOriginPath, type ResponseResult, status as statusResult } from "@nifrajs/core/server"
|
|
2
2
|
import type { BoundaryRegistration, BoundaryStates } from "../boundary.ts"
|
|
3
|
+
import { type CssLoadingMode, DEFAULT_CSS_LOADING, normalizeCssLoading } from "../css-contract.ts"
|
|
3
4
|
import { DEFERRED_ERROR_CODE, DEFERRED_RUNTIME, prepareDeferred } from "../deferred.ts"
|
|
4
5
|
import { ISR_REVALIDATE_HEADER, ISR_REVALIDATE_TAGS_HEADER, serializeISRTags } from "../isr.ts"
|
|
5
6
|
import type {
|
|
@@ -97,6 +98,13 @@ export interface RenderPageOptions {
|
|
|
97
98
|
* `BuildManifest.css`) - injected as `<link rel="stylesheet">` in `<head>` so styles arrive with the
|
|
98
99
|
* first paint (no FOUC). Rendered even on non-hydrated pages. Empty/omitted ⇒ none (unchanged). */
|
|
99
100
|
readonly styles?: readonly string[]
|
|
101
|
+
/**
|
|
102
|
+
* Activation policy for these framework-owned stylesheet links (default `"blocking"`). In
|
|
103
|
+
* `"deferred"` mode, hydrating pages fetch with `media="print"` and the generated client promotes
|
|
104
|
+
* them to `media="all"` after load/error/timeout. Non-hydrated pages remain blocking so JS-off pages
|
|
105
|
+
* and terminal documents are styled.
|
|
106
|
+
*/
|
|
107
|
+
readonly cssLoading?: CssLoadingMode
|
|
100
108
|
/** SSG: the prerendered-path set, serialized to `window.__NIFRA_PRERENDERED__` so the client fetches
|
|
101
109
|
* a static `_data.json` on soft-nav into a prerendered route. Empty/omitted ⇒ not injected. */
|
|
102
110
|
readonly prerenderedPaths?: readonly string[]
|
|
@@ -157,7 +165,8 @@ export interface RenderPageOptions {
|
|
|
157
165
|
* tail - emitted **regardless of `hydrate`**, so a static (`hydrate: false`) page can still mount
|
|
158
166
|
* no-framework islands. URLs are attribute-escaped. Empty/omitted ⇒ none (unchanged output). */
|
|
159
167
|
readonly islandScripts?: readonly string[]
|
|
160
|
-
/** CSP nonce applied to every framework-owned executable script in this document.
|
|
168
|
+
/** CSP nonce applied to every framework-owned executable script in this document. Nonce-bearing
|
|
169
|
+
* documents are marked `private, no-store` so a request-specific nonce is never replayed. */
|
|
161
170
|
readonly nonce?: string
|
|
162
171
|
/**
|
|
163
172
|
* Advanced: a per-route slot the renderer fills with the request-invariant document pieces (shell
|
|
@@ -165,7 +174,8 @@ export interface RenderPageOptions {
|
|
|
165
174
|
*
|
|
166
175
|
* Only pass a cache when every shell-shaping input is IDENTICAL across the requests sharing it:
|
|
167
176
|
* same `head` content (a static meta chain - never a `meta(data)` function), `title`, `styles`,
|
|
168
|
-
* `preload`, `islandScripts`, `clientEntry`, `rootId`, `hydrate`, and `prerenderedPaths`.
|
|
177
|
+
* `cssLoading`, `preload`, `islandScripts`, `clientEntry`, `rootId`, `hydrate`, and `prerenderedPaths`.
|
|
178
|
+
* Per-request
|
|
169
179
|
* values (`data`, `params`, `search`, `actionData`, `layoutData`, deferred state) are always assembled
|
|
170
180
|
* fresh and safe. A per-request `nonce` disables the cache automatically. `createWebApp` wires this
|
|
171
181
|
* per route, gated on the route + layout metas being static.
|
|
@@ -219,6 +229,7 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
219
229
|
clientEntry,
|
|
220
230
|
preload = [],
|
|
221
231
|
styles = [],
|
|
232
|
+
cssLoading: requestedCssLoading,
|
|
222
233
|
prerenderedPaths = [],
|
|
223
234
|
revalidate,
|
|
224
235
|
revalidateTags = [],
|
|
@@ -233,6 +244,7 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
233
244
|
boundaries,
|
|
234
245
|
headers: extraHeaders,
|
|
235
246
|
} = options
|
|
247
|
+
const cssLoading = normalizeCssLoading(requestedCssLoading ?? DEFAULT_CSS_LOADING)
|
|
236
248
|
if (nonce !== undefined && nonce.trim() === "") {
|
|
237
249
|
throw new TypeError("[nifra/web] renderPage nonce must be non-empty when provided")
|
|
238
250
|
}
|
|
@@ -317,10 +329,25 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
317
329
|
preloadLinks += `<link rel="modulepreload" href="${escapeAttr(url)}">`
|
|
318
330
|
}
|
|
319
331
|
// The matched route's stylesheets - `<link rel="stylesheet">` in `<head>` so CSS arrives with the
|
|
320
|
-
// first paint (no FOUC).
|
|
321
|
-
//
|
|
332
|
+
// first paint (no FOUC). Deferred mode is deliberately opt-in and only applies to hydrating pages:
|
|
333
|
+
// a JS-off page must retain a normal stylesheet link, and a terminal/static page has no generated
|
|
334
|
+
// client to promote `media="print"` to `media="all"`.
|
|
322
335
|
let styleLinks = ""
|
|
323
|
-
|
|
336
|
+
const deferStyles = hydrate && cssLoading === "deferred"
|
|
337
|
+
const styleUrls = [...new Set(styles)]
|
|
338
|
+
for (const url of styleUrls) {
|
|
339
|
+
const href = escapeAttr(url)
|
|
340
|
+
if (!deferStyles) {
|
|
341
|
+
styleLinks += `<link rel="stylesheet" href="${href}">`
|
|
342
|
+
continue
|
|
343
|
+
}
|
|
344
|
+
// `media="print"` starts the fetch without making the CSS render-blocking. The marker is scoped
|
|
345
|
+
// to framework-owned links so the generated client never rewrites an author's head link. The
|
|
346
|
+
// noscript copy preserves progressive enhancement when JavaScript is disabled.
|
|
347
|
+
styleLinks +=
|
|
348
|
+
`<link rel="stylesheet" href="${href}" media="print" data-nifra-css="deferred">` +
|
|
349
|
+
`<noscript><link rel="stylesheet" href="${href}"></noscript>`
|
|
350
|
+
}
|
|
324
351
|
// Island bundles are referenced only by a `<script type="module">` at the END of `<body>`, so the
|
|
325
352
|
// browser doesn't discover them until the whole page is parsed. `modulepreload` them in `<head>`
|
|
326
353
|
// so the fetch starts immediately, in parallel with parsing - regardless of `hydrate`.
|
|
@@ -395,6 +422,10 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
395
422
|
if (revalidate !== undefined) headers[ISR_REVALIDATE_HEADER] = String(revalidate)
|
|
396
423
|
const serializedTags = serializeISRTags(revalidateTags)
|
|
397
424
|
if (serializedTags !== undefined) headers[ISR_REVALIDATE_TAGS_HEADER] = serializedTags
|
|
425
|
+
// A per-request nonce must not be replayed from a browser, CDN, or ISR cache. The same policy
|
|
426
|
+
// also keeps `withISR` from storing a document whose executable scripts carry request-specific
|
|
427
|
+
// authorization to run under the caller's CSP.
|
|
428
|
+
if (nonce !== undefined) headers["cache-control"] = "private, no-store"
|
|
398
429
|
const renderProps: RenderProps = {
|
|
399
430
|
data: forComponent,
|
|
400
431
|
actionData: actionSplit?.forComponent,
|
|
@@ -554,8 +585,10 @@ function streamDocument(
|
|
|
554
585
|
deferred.map(async (d) => {
|
|
555
586
|
try {
|
|
556
587
|
const value = serializeData(await d.promise)
|
|
588
|
+
// `serializeData` JSON-serializes the value and escapes `<`, `>`, and Unicode line
|
|
589
|
+
// separators before this framework-owned script is emitted.
|
|
557
590
|
controller.enqueue(
|
|
558
|
-
enc.encode(`<script${nonceAttr}>window.__nifraResolve(${d.id},${value})</script>`),
|
|
591
|
+
enc.encode(`<script${nonceAttr}>window.__nifraResolve(${d.id},${value})</script>`), // nosemgrep: javascript.lang.security.audit.unknown-value-with-script-tag.unknown-value-with-script-tag
|
|
559
592
|
)
|
|
560
593
|
} catch (err) {
|
|
561
594
|
// A rejected deferred streams __nifraReject (the client `<Await>` surfaces it) - it must
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type UrlParts, urlPartsOf } from "@nifrajs/core/server"
|
|
2
|
+
|
|
3
|
+
// A page request reaches the same Request object through mounts, fallback checks, search parsing,
|
|
4
|
+
// and rendering. Cache the allocation-light URL split once for that request. WeakMap keeps the cache
|
|
5
|
+
// bounded to requests that are still alive and makes the helper safe for every runtime adapter.
|
|
6
|
+
const REQUEST_URL_PARTS = new WeakMap<Request, UrlParts>()
|
|
7
|
+
|
|
8
|
+
export function urlPartsFor(request: Request): UrlParts {
|
|
9
|
+
const cached = REQUEST_URL_PARTS.get(request)
|
|
10
|
+
if (cached !== undefined) return cached
|
|
11
|
+
const parts = urlPartsOf(request.url)
|
|
12
|
+
REQUEST_URL_PARTS.set(request, parts)
|
|
13
|
+
return parts
|
|
14
|
+
}
|