@nifrajs/web 5.1.0 → 5.3.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/CHANGELOG.md +3042 -0
- package/dist/build-plan.d.ts +7 -0
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js +19 -0
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts +1 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +51 -3
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +23 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +224 -16
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +2 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -7
- package/dist/client.js.map +1 -1
- package/dist/csp.d.ts +2 -0
- package/dist/csp.d.ts.map +1 -1
- package/dist/csp.js +2 -0
- package/dist/csp.js.map +1 -1
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +23 -3
- package/dist/deferred.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/codegen.d.ts +6 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +23 -5
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/copy-split.d.ts +27 -0
- package/dist/internal/copy-split.d.ts.map +1 -0
- package/dist/internal/copy-split.js +173 -0
- package/dist/internal/copy-split.js.map +1 -0
- package/dist/internal/deferred-free.d.ts +3 -0
- package/dist/internal/deferred-free.d.ts.map +1 -0
- package/dist/internal/deferred-free.js +15 -0
- package/dist/internal/deferred-free.js.map +1 -0
- package/dist/internal/module-preparation.d.ts +7 -0
- package/dist/internal/module-preparation.d.ts.map +1 -0
- package/dist/internal/module-preparation.js +24 -0
- package/dist/internal/module-preparation.js.map +1 -0
- package/dist/internal/output-guard.d.ts +2 -0
- package/dist/internal/output-guard.d.ts.map +1 -1
- package/dist/internal/output-guard.js +51 -9
- package/dist/internal/output-guard.js.map +1 -1
- package/dist/internal/page-execution.d.ts +20 -1
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +115 -50
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +3 -1
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/prerendered-pages.d.ts +42 -0
- package/dist/internal/prerendered-pages.d.ts.map +1 -0
- package/dist/internal/prerendered-pages.js +92 -0
- package/dist/internal/prerendered-pages.js.map +1 -0
- package/dist/internal/public-url.d.ts +7 -0
- package/dist/internal/public-url.d.ts.map +1 -1
- package/dist/internal/public-url.js +13 -0
- package/dist/internal/public-url.js.map +1 -1
- package/dist/internal/render-document.d.ts +11 -2
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +131 -41
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/request-url.d.ts +4 -0
- package/dist/internal/request-url.d.ts.map +1 -1
- package/dist/internal/request-url.js +24 -0
- package/dist/internal/request-url.js.map +1 -1
- package/dist/internal/secret-scan.d.ts +4 -0
- package/dist/internal/secret-scan.d.ts.map +1 -1
- package/dist/internal/secret-scan.js +8 -1
- package/dist/internal/secret-scan.js.map +1 -1
- package/dist/internal/typescript-api.d.ts +4 -0
- package/dist/internal/typescript-api.d.ts.map +1 -0
- package/dist/internal/typescript-api.js +8 -0
- package/dist/internal/typescript-api.js.map +1 -0
- package/dist/internal/vite-plugin-compat.d.ts +28 -0
- package/dist/internal/vite-plugin-compat.d.ts.map +1 -0
- package/dist/internal/vite-plugin-compat.js +82 -0
- package/dist/internal/vite-plugin-compat.js.map +1 -0
- package/dist/internal/web-app.d.ts +45 -0
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +32 -1
- package/dist/internal/web-app.js.map +1 -1
- package/dist/isr.d.ts +1 -0
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +7 -4
- package/dist/isr.js.map +1 -1
- package/dist/plugins/import-glob.d.ts +20 -0
- package/dist/plugins/import-glob.d.ts.map +1 -0
- package/dist/plugins/import-glob.js +401 -0
- package/dist/plugins/import-glob.js.map +1 -0
- package/dist/plugins/kit.d.ts +11 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +32 -0
- package/dist/plugins/kit.js.map +1 -1
- package/dist/plugins/stylex.d.ts.map +1 -1
- package/dist/plugins/stylex.js +2 -2
- package/dist/plugins/stylex.js.map +1 -1
- package/dist/prerender.d.ts +4 -0
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +9 -1
- package/dist/prerender.js.map +1 -1
- package/dist/public-dir.d.ts +15 -0
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +91 -12
- package/dist/public-dir.js.map +1 -1
- package/dist/render-seam.d.ts +42 -2
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +33 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/router.d.ts +3 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +4 -4
- package/dist/router.js.map +1 -1
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +2 -0
- package/dist/search.js.map +1 -1
- package/dist/server-components/typescript.d.ts.map +1 -1
- package/dist/server-components/typescript.js +2 -7
- package/dist/server-components/typescript.js.map +1 -1
- package/dist/vite.d.ts +7 -28
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +130 -123
- package/dist/vite.js.map +1 -1
- package/package.json +5 -4
- package/src/build-plan.ts +21 -0
- package/src/build-vite.ts +60 -3
- package/src/build.ts +283 -39
- package/src/client.ts +15 -7
- package/src/csp.ts +4 -0
- package/src/deferred.ts +20 -3
- package/src/index.ts +9 -0
- package/src/internal/codegen.ts +32 -3
- package/src/internal/copy-split.ts +220 -0
- package/src/internal/deferred-free.ts +15 -0
- package/src/internal/module-preparation.ts +30 -0
- package/src/internal/output-guard.ts +51 -9
- package/src/internal/page-execution.ts +137 -53
- package/src/internal/parity.ts +2 -1
- package/src/internal/prerendered-pages.ts +124 -0
- package/src/internal/public-url.ts +13 -0
- package/src/internal/render-document.ts +179 -58
- package/src/internal/request-url.ts +24 -0
- package/src/internal/secret-scan.ts +9 -1
- package/src/internal/typescript-api.ts +11 -0
- package/src/internal/vite-plugin-compat.ts +81 -0
- package/src/internal/web-app.ts +82 -2
- package/src/isr.ts +7 -4
- package/src/plugins/import-glob.ts +392 -0
- package/src/plugins/kit.ts +36 -0
- package/src/plugins/stylex.ts +2 -1
- package/src/prerender.ts +13 -1
- package/src/public-dir.ts +107 -12
- package/src/render-seam.ts +65 -2
- package/src/router.ts +7 -6
- package/src/search.ts +1 -0
- package/src/server-components/typescript.ts +2 -10
- package/src/vite.ts +133 -135
package/src/public-dir.ts
CHANGED
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
* Distinct from `publicPath` in `build.ts`, which is the URL prefix for content-hashed bundle chunks.
|
|
13
13
|
* The names collide and the concepts do not: `publicPath` never covers user-authored files.
|
|
14
14
|
*/
|
|
15
|
-
import { realpath } from "node:fs/promises"
|
|
15
|
+
import { type FileHandle, realpath } from "node:fs/promises"
|
|
16
16
|
import { extname, normalize, resolve, sep } from "node:path"
|
|
17
17
|
import { Readable } from "node:stream"
|
|
18
18
|
import { parseByteRange } from "@nifrajs/core/range"
|
|
19
19
|
import { pathnameOf } from "@nifrajs/core/server"
|
|
20
|
+
import { isHiddenPublicPath } from "./internal/public-url.ts"
|
|
20
21
|
import { openResolved } from "./open-resolved.ts"
|
|
21
22
|
|
|
22
23
|
/** How long each subtree may be cached. Content-hashed bundle output can be immutable; a
|
|
@@ -96,15 +97,33 @@ export const CONTENT_TYPES: Readonly<Record<string, string>> = {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
/**
|
|
99
|
-
* Whether a confined path crosses a
|
|
100
|
-
*
|
|
101
|
-
*
|
|
100
|
+
* Whether a confined path crosses a hidden segment ({@link isHiddenPublicPath}). Checked on the
|
|
101
|
+
* resolved path, so `%2e` encodings are covered, and again on the real target, so a visible name
|
|
102
|
+
* linking to `.env` is refused too.
|
|
102
103
|
*/
|
|
103
104
|
function isHiddenPath(root: string, abs: string): boolean {
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
return abs !== root && isHiddenPublicPath(abs.slice(root.length + 1))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Up to this many bytes are read whole through the pinned descriptor, which then closes before the
|
|
109
|
+
// response leaves: on Bun a 200 KB asset is served in 0.17 ms against 0.28 ms through a node:fs
|
|
110
|
+
// stream. A larger body streams, so a video holds 64 KiB per request rather than the file.
|
|
111
|
+
const BUFFERED_BODY_MAX = 256 * 1024
|
|
112
|
+
|
|
113
|
+
/** `length` bytes of `handle` from `start`; fewer when the file shrank since its stat. */
|
|
114
|
+
async function readPinned(
|
|
115
|
+
handle: FileHandle,
|
|
116
|
+
start: number,
|
|
117
|
+
length: number,
|
|
118
|
+
): Promise<Uint8Array<ArrayBuffer>> {
|
|
119
|
+
const bytes = new Uint8Array(length)
|
|
120
|
+
let offset = 0
|
|
121
|
+
while (offset < length) {
|
|
122
|
+
const { bytesRead } = await handle.read(bytes, offset, length - offset, start + offset)
|
|
123
|
+
if (bytesRead === 0) break
|
|
124
|
+
offset += bytesRead
|
|
106
125
|
}
|
|
107
|
-
return
|
|
126
|
+
return offset === length ? bytes : bytes.subarray(0, offset)
|
|
108
127
|
}
|
|
109
128
|
|
|
110
129
|
/** HTTP dates carry second precision; comparing raw milliseconds makes every file look stale. */
|
|
@@ -194,7 +213,8 @@ export function servePublicDir(
|
|
|
194
213
|
if (
|
|
195
214
|
resolvedRoot === undefined ||
|
|
196
215
|
resolvedFile === undefined ||
|
|
197
|
-
(resolvedFile !== resolvedRoot && !resolvedFile.startsWith(resolvedRoot + sep))
|
|
216
|
+
(resolvedFile !== resolvedRoot && !resolvedFile.startsWith(resolvedRoot + sep)) ||
|
|
217
|
+
isHiddenPath(resolvedRoot, resolvedFile)
|
|
198
218
|
) {
|
|
199
219
|
return undefined
|
|
200
220
|
}
|
|
@@ -243,7 +263,8 @@ export function servePublicDir(
|
|
|
243
263
|
return new Response(null, { status: 304, headers })
|
|
244
264
|
}
|
|
245
265
|
|
|
246
|
-
|
|
266
|
+
// Range applies to GET only (RFC 9110 section 14.2); HEAD reports the whole representation.
|
|
267
|
+
const rangeHeader = head ? null : request.headers.get("range")
|
|
247
268
|
const range =
|
|
248
269
|
rangeHeader !== null && ifRangeMatches(request.headers.get("if-range"), lastModified)
|
|
249
270
|
? parseByteRange(rangeHeader, size)
|
|
@@ -255,7 +276,16 @@ export function servePublicDir(
|
|
|
255
276
|
return new Response(null, { status: 416, headers })
|
|
256
277
|
}
|
|
257
278
|
|
|
258
|
-
const body = (
|
|
279
|
+
const body = async (
|
|
280
|
+
start: number,
|
|
281
|
+
end: number,
|
|
282
|
+
): Promise<Uint8Array<ArrayBuffer> | ReadableStream<Uint8Array>> => {
|
|
283
|
+
const length = end - start + 1
|
|
284
|
+
if (length <= BUFFERED_BODY_MAX) {
|
|
285
|
+
const bytes = await readPinned(handle!, start, length)
|
|
286
|
+
headers.set("content-length", String(bytes.byteLength))
|
|
287
|
+
return bytes
|
|
288
|
+
}
|
|
259
289
|
const stream = handle!.createReadStream({ start, end })
|
|
260
290
|
// FileHandle.createReadStream owns closing the descriptor after EOF/abort.
|
|
261
291
|
handle = undefined
|
|
@@ -266,14 +296,14 @@ export function servePublicDir(
|
|
|
266
296
|
const { start, end } = range.ranges[0]!
|
|
267
297
|
headers.set("content-range", `bytes ${start}-${end}/${size}`)
|
|
268
298
|
headers.set("content-length", String(end - start + 1))
|
|
269
|
-
return new Response(head ? null : body(start, end), { status: 206, headers })
|
|
299
|
+
return new Response(head ? null : await body(start, end), { status: 206, headers })
|
|
270
300
|
}
|
|
271
301
|
|
|
272
302
|
// Multiple ranges would require assembling `multipart/byteranges`, and doing that for a file on
|
|
273
303
|
// disk means buffering the whole representation to serve a request that asked for less of it.
|
|
274
304
|
// RFC 9110 lets a server ignore Range entirely, so the full body is the conformant answer here.
|
|
275
305
|
headers.set("content-length", String(size))
|
|
276
|
-
return new Response(head ? null : body(), { headers })
|
|
306
|
+
return new Response(head || size === 0 ? null : await body(0, size - 1), { headers })
|
|
277
307
|
} finally {
|
|
278
308
|
if (handle !== undefined) {
|
|
279
309
|
try {
|
|
@@ -285,3 +315,68 @@ export function servePublicDir(
|
|
|
285
315
|
}
|
|
286
316
|
}
|
|
287
317
|
}
|
|
318
|
+
|
|
319
|
+
export interface PrerenderedPageReaderOptions {
|
|
320
|
+
/** Absolute path of the directory the build wrote the prerendered pages into. */
|
|
321
|
+
readonly dir: string
|
|
322
|
+
/** Each page path mapped to its file, relative to `dir`. */
|
|
323
|
+
readonly files: Readonly<Record<string, string>>
|
|
324
|
+
/** Bytes of HTML kept in memory once read (default 32 MiB); a page past the budget is read per request. */
|
|
325
|
+
readonly cacheBytes?: number
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Read a self-hosted build's prerendered pages for `createWebApp`'s `prerenderedPages`. Only the build's
|
|
330
|
+
* own files are reachable, each opened only once its real path is inside `dir`, so a link planted in the
|
|
331
|
+
* deployed tree cannot point a page elsewhere. A deploy's files do not change while it runs, so a page
|
|
332
|
+
* is kept after its first read; `undefined` (a missing or refused file) renders the page on demand.
|
|
333
|
+
*/
|
|
334
|
+
export function prerenderedPageReader(
|
|
335
|
+
options: PrerenderedPageReaderOptions,
|
|
336
|
+
): (pathname: string) => Promise<string | undefined> {
|
|
337
|
+
const root = resolve(options.dir)
|
|
338
|
+
const rootReal = realpath(root).catch(() => undefined)
|
|
339
|
+
const files = new Map(Object.entries(options.files))
|
|
340
|
+
const budget = options.cacheBytes ?? 32 * 1024 * 1024
|
|
341
|
+
const cache = new Map<string, string>()
|
|
342
|
+
let cachedBytes = 0
|
|
343
|
+
return async (pathname) => {
|
|
344
|
+
const cached = cache.get(pathname)
|
|
345
|
+
if (cached !== undefined) return cached
|
|
346
|
+
const rel = files.get(pathname)
|
|
347
|
+
if (rel === undefined) return undefined
|
|
348
|
+
const abs = resolve(root, rel)
|
|
349
|
+
if (!abs.startsWith(root + sep) || isHiddenPath(root, abs)) return undefined
|
|
350
|
+
const [resolvedRoot, resolvedFile] = await Promise.all([
|
|
351
|
+
rootReal,
|
|
352
|
+
realpath(abs).catch(() => undefined),
|
|
353
|
+
])
|
|
354
|
+
if (
|
|
355
|
+
resolvedRoot === undefined ||
|
|
356
|
+
resolvedFile === undefined ||
|
|
357
|
+
!resolvedFile.startsWith(resolvedRoot + sep)
|
|
358
|
+
) {
|
|
359
|
+
return undefined
|
|
360
|
+
}
|
|
361
|
+
let handle: FileHandle
|
|
362
|
+
try {
|
|
363
|
+
handle = await openResolved(resolvedFile)
|
|
364
|
+
} catch {
|
|
365
|
+
return undefined
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
if (!(await handle.stat()).isFile()) return undefined
|
|
369
|
+
const bytes = await handle.readFile()
|
|
370
|
+
const html = bytes.toString("utf8")
|
|
371
|
+
if (!cache.has(pathname) && cachedBytes + bytes.byteLength <= budget) {
|
|
372
|
+
cache.set(pathname, html)
|
|
373
|
+
cachedBytes += bytes.byteLength
|
|
374
|
+
}
|
|
375
|
+
return html
|
|
376
|
+
} catch {
|
|
377
|
+
return undefined
|
|
378
|
+
} finally {
|
|
379
|
+
await handle.close().catch(() => undefined)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
package/src/render-seam.ts
CHANGED
|
@@ -33,6 +33,61 @@ export interface RenderProps {
|
|
|
33
33
|
readonly boundaries?: BoundaryStates
|
|
34
34
|
/** The rendered chain's module ids and `handle` exports, for the adapter's `useMatches`. */
|
|
35
35
|
readonly matchChain?: MatchChain
|
|
36
|
+
/** The page's locale, when the app registers its locale routing (`createWebApp`'s `i18n`). Set on
|
|
37
|
+
* SSR only; the client names it from `path` (see {@link localeOfProps}). */
|
|
38
|
+
readonly locale?: PageLocale
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A served locale as a page renders in it: from the app's locale registry. */
|
|
42
|
+
export interface PageLocale {
|
|
43
|
+
/** The registry key, which is also the URL segment (`ur`). */
|
|
44
|
+
readonly key: string
|
|
45
|
+
/** The BCP-47 tag `<html lang>` and `Content-Language` carry (`ur-PK`). */
|
|
46
|
+
readonly lang: string
|
|
47
|
+
readonly dir: "ltr" | "rtl"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The served locales, handed to the client so it names a page's locale from its path the way the
|
|
51
|
+
* server does. */
|
|
52
|
+
export interface PageLocales {
|
|
53
|
+
/** The default locale's key. */
|
|
54
|
+
readonly default: string
|
|
55
|
+
/** Every served locale, in registry order. */
|
|
56
|
+
readonly locales: readonly PageLocale[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The handover global carrying {@link PageLocales}. */
|
|
60
|
+
export const LOCALES_GLOBAL = "__NIFRA_LOCALES__"
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The locale a path is in, by the rule `@nifrajs/i18n/routing`'s `localeOf` follows: a first path
|
|
64
|
+
* segment that names a served locale, ignoring ASCII case, or else the default.
|
|
65
|
+
*/
|
|
66
|
+
export function pageLocaleIn(config: PageLocales, path: string): PageLocale | undefined {
|
|
67
|
+
// One linear pass: separators, then the segment up to the next separator, query or hash.
|
|
68
|
+
const first = /^[/\\]*([^/\\?#]*)/.exec(path.replace(/[\t\n\r]/g, ""))?.[1]?.toLowerCase()
|
|
69
|
+
return (
|
|
70
|
+
config.locales.find((locale) => first !== "" && locale.key.toLowerCase() === first) ??
|
|
71
|
+
config.locales.find((locale) => locale.key === config.default)
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const isPageLocales = (value: unknown): value is PageLocales =>
|
|
76
|
+
typeof value === "object" && value !== null && "locales" in value && Array.isArray(value.locales)
|
|
77
|
+
|
|
78
|
+
/** The locale of the page at `path`, from the served locales the server handed over; `undefined`
|
|
79
|
+
* outside a browser page, or when the app registers no locale routing. */
|
|
80
|
+
export function handoverLocale(path: string): PageLocale | undefined {
|
|
81
|
+
const config: unknown = Reflect.get(globalThis, LOCALES_GLOBAL)
|
|
82
|
+
return isPageLocales(config) ? pageLocaleIn(config, path) : undefined
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The locale of the page `props` render: the server's answer on SSR, else the one the client names
|
|
86
|
+
* from `props.path`. What every adapter's `useLocale` reads. */
|
|
87
|
+
export function localeOfProps(props: RenderProps | undefined): PageLocale | undefined {
|
|
88
|
+
if (props === undefined) return undefined
|
|
89
|
+
if (props.locale !== undefined) return props.locale
|
|
90
|
+
return props.path === undefined ? undefined : handoverLocale(props.path)
|
|
36
91
|
}
|
|
37
92
|
|
|
38
93
|
/**
|
|
@@ -100,13 +155,16 @@ export interface RenderAdapter {
|
|
|
100
155
|
): ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>
|
|
101
156
|
/**
|
|
102
157
|
* Render the chain to a complete HTML string when no content is deferred. `options.nonce` is the
|
|
103
|
-
* document's CSP nonce, for any inline script the renderer emits (Solid's hydration data)
|
|
158
|
+
* document's CSP nonce, for any inline script the renderer emits (Solid's hydration data); a
|
|
159
|
+
* document that sends a policy keeps the nonce only when the returned HTML carries it. Return
|
|
160
|
+
* `undefined` when the render suspended on something only `renderToStream` waits for (React's
|
|
161
|
+
* `use` or `lazy` inside `<Suspense>`): the page is then streamed instead.
|
|
104
162
|
*/
|
|
105
163
|
renderToString?(
|
|
106
164
|
chain: readonly unknown[],
|
|
107
165
|
props: RenderProps,
|
|
108
166
|
options?: RenderStreamOptions,
|
|
109
|
-
): string | Promise<string>
|
|
167
|
+
): string | undefined | Promise<string | undefined>
|
|
110
168
|
/** Per-document bootstrap markup injected into `<head>` for client hydration. */
|
|
111
169
|
hydrationHead(nonce?: string): string
|
|
112
170
|
/**
|
|
@@ -151,5 +209,10 @@ export const ACTION_GLOBAL = "__NIFRA_ACTION__"
|
|
|
151
209
|
/** Dynamic-boundary states for hydration; absent when a route declares no boundaries. */
|
|
152
210
|
export const BOUNDARY_GLOBAL = "__NIFRA_BOUNDARIES__"
|
|
153
211
|
export const COMPONENT_GLOBAL = "__NIFRA_COMPONENTS__"
|
|
212
|
+
/** Locale key -> copy chunk URL, in a production build that split copy tables by locale. The name
|
|
213
|
+
* `@nifrajs/i18n/copy` reads. */
|
|
214
|
+
export const COPY_CHUNKS_GLOBAL = "__NIFRA_COPY__"
|
|
215
|
+
/** The registry `@nifrajs/i18n/copy` keeps its loaded chunks and definitions in. */
|
|
216
|
+
export const COPY_REALM = "nifra.copy"
|
|
154
217
|
/** Marker attribute used to find a non-default hydration container. */
|
|
155
218
|
export const ROOT_ATTRIBUTE = "data-nifra-root"
|
package/src/router.ts
CHANGED
|
@@ -310,8 +310,9 @@ export interface ClientRouterOptions {
|
|
|
310
310
|
/** Override the loader-data fetch (tests inject a stub; defaults to a same-origin JSON GET). */
|
|
311
311
|
readonly fetchData?: FetchRouteData
|
|
312
312
|
/** Ensure a route's code chunk is loaded before rendering (code-splitting). Awaited in parallel
|
|
313
|
-
* with the loader data, so `pending` covers both. Omit when the bundle isn't split.
|
|
314
|
-
|
|
313
|
+
* with the loader data, so `pending` covers both. Omit when the bundle isn't split. `path` is the URL
|
|
314
|
+
* the route is about to render, for code loaded per URL (a production build's copy chunks). */
|
|
315
|
+
readonly loadModule?: (routeId: string, path?: string) => Promise<void>
|
|
315
316
|
/** Terminal status → client route id. Generated entries populate this from `_404` and
|
|
316
317
|
* `_<status>` files so soft navigation renders the same boundary as a hard request. */
|
|
317
318
|
readonly statusRoutes?: Readonly<Record<number, string>>
|
|
@@ -667,7 +668,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
|
|
|
667
668
|
)
|
|
668
669
|
if (mine !== generation) return
|
|
669
670
|
if (loaded.terminalRouteId !== undefined) {
|
|
670
|
-
await loadModule?.(loaded.terminalRouteId)
|
|
671
|
+
await loadModule?.(loaded.terminalRouteId, state.path)
|
|
671
672
|
state = { ...state, routeId: loaded.terminalRouteId, params: {}, data: null }
|
|
672
673
|
} else {
|
|
673
674
|
state = {
|
|
@@ -788,7 +789,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
|
|
|
788
789
|
state = { ...state, pendingPath: at }
|
|
789
790
|
emit()
|
|
790
791
|
}
|
|
791
|
-
await loadModule?.(route.routeId)
|
|
792
|
+
await loadModule?.(route.routeId, at)
|
|
792
793
|
// Prefetched server data is one-shot; a clientLoader still runs over it without another request.
|
|
793
794
|
const prefetchedData = usePrefetch ? freshPrefetch(at) : undefined
|
|
794
795
|
if (usePrefetch) prefetched.delete(at)
|
|
@@ -801,7 +802,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
|
|
|
801
802
|
prefetchedData,
|
|
802
803
|
)
|
|
803
804
|
if (loaded.terminalRouteId !== undefined) {
|
|
804
|
-
await loadModule?.(loaded.terminalRouteId)
|
|
805
|
+
await loadModule?.(loaded.terminalRouteId, at)
|
|
805
806
|
if (options.layoutStatusRoutes?.[loaded.terminalRouteId] === true) {
|
|
806
807
|
throw new Error(
|
|
807
808
|
`[nifra/web] ${loaded.terminalRouteId} renders inside layouts; loading ${at} as a document`,
|
|
@@ -1215,7 +1216,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
|
|
|
1215
1216
|
// Unwrapped here, so a prefetched entry is the same shape `navigate` builds state from -
|
|
1216
1217
|
// otherwise a prefetch hit would publish an envelope where a miss publishes loader data.
|
|
1217
1218
|
const [, loaded] = await Promise.all([
|
|
1218
|
-
loadModule?.(matched.routeId),
|
|
1219
|
+
loadModule?.(matched.routeId, path),
|
|
1219
1220
|
loadRouteData(path, matched),
|
|
1220
1221
|
])
|
|
1221
1222
|
prefetched.delete(path) // a stale entry re-enters as the newest
|
package/src/search.ts
CHANGED
|
@@ -107,6 +107,7 @@ function encodeValue(value: unknown): string {
|
|
|
107
107
|
/** The default JSON-first codec. Repeated keys (`?t=a&t=b`) decode to an array; single keys to a value. */
|
|
108
108
|
const jsonCodec: SearchCodec = {
|
|
109
109
|
parse(raw, limits) {
|
|
110
|
+
if (raw === "" || raw === "?") return {}
|
|
110
111
|
const usp = new URLSearchParams(raw.startsWith("?") ? raw.slice(1) : raw)
|
|
111
112
|
// Two passes instead of the old `Set(keys) + getAll(key)` shape, which rescanned the complete
|
|
112
113
|
// URLSearchParams list once per distinct key (O(keys²)). Admission must be decided BEFORE any
|
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
import { createRequire } from "node:module"
|
|
2
2
|
import type * as TS from "typescript"
|
|
3
|
+
import { isTypeScriptCompiler } from "../internal/typescript-api.ts"
|
|
3
4
|
|
|
4
5
|
/** The code of the error a TypeScript without the in-process compiler API raises here. */
|
|
5
6
|
export const UNSUPPORTED_TYPESCRIPT = "NIFRA_TS_UNSUPPORTED"
|
|
6
7
|
|
|
7
|
-
function isCompiler(value: unknown): value is typeof TS {
|
|
8
|
-
return (
|
|
9
|
-
typeof value === "object" &&
|
|
10
|
-
value !== null &&
|
|
11
|
-
"createSourceFile" in value &&
|
|
12
|
-
typeof value.createSourceFile === "function"
|
|
13
|
-
)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
8
|
/**
|
|
17
9
|
* The TypeScript compiler API, resolved from `from` the way `import "typescript"` there would be.
|
|
18
10
|
* TypeScript 7's package ships no in-process compiler API (its root export is only `version`), so
|
|
@@ -20,7 +12,7 @@ function isCompiler(value: unknown): value is typeof TS {
|
|
|
20
12
|
*/
|
|
21
13
|
export function loadTypeScript(from: string = import.meta.url): typeof TS {
|
|
22
14
|
const loaded: unknown = createRequire(from)("typescript")
|
|
23
|
-
if (
|
|
15
|
+
if (isTypeScriptCompiler(loaded)) return loaded
|
|
24
16
|
const version =
|
|
25
17
|
typeof loaded === "object" &&
|
|
26
18
|
loaded !== null &&
|