@nifrajs/web 5.3.0 → 5.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/CHANGELOG.md +86 -0
- package/dist/build-plan.d.ts +6 -0
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js +25 -0
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts +2 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +2 -0
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +17 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +174 -54
- package/dist/build.js.map +1 -1
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +20 -6
- package/dist/deferred.js.map +1 -1
- package/dist/dev.d.ts +5 -0
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +7 -6
- package/dist/dev.js.map +1 -1
- package/dist/diagnostic.d.ts +6 -0
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +41 -0
- package/dist/diagnostic.js.map +1 -1
- package/dist/host-policy.d.ts +72 -0
- package/dist/host-policy.d.ts.map +1 -0
- package/dist/host-policy.js +149 -0
- package/dist/host-policy.js.map +1 -0
- package/dist/host-reach.d.ts +49 -0
- package/dist/host-reach.d.ts.map +1 -0
- package/dist/host-reach.js +144 -0
- package/dist/host-reach.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/certified-components.d.ts +8 -0
- package/dist/internal/certified-components.d.ts.map +1 -0
- package/dist/internal/certified-components.js +8 -0
- package/dist/internal/certified-components.js.map +1 -0
- package/dist/internal/codegen.d.ts +8 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +15 -4
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/node-adapter.d.ts +19 -0
- package/dist/internal/node-adapter.d.ts.map +1 -0
- package/dist/internal/node-adapter.js +48 -0
- package/dist/internal/node-adapter.js.map +1 -0
- package/dist/internal/output-guard.d.ts +6 -0
- package/dist/internal/output-guard.d.ts.map +1 -1
- package/dist/internal/output-guard.js +118 -16
- package/dist/internal/output-guard.js.map +1 -1
- package/dist/internal/page-execution.d.ts +2 -0
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +16 -11
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +3 -2
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/public-url.d.ts +0 -7
- package/dist/internal/public-url.d.ts.map +1 -1
- package/dist/internal/public-url.js +0 -13
- package/dist/internal/public-url.js.map +1 -1
- package/dist/internal/render-document.d.ts +3 -1
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +10 -4
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/web-app.d.ts +15 -1
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +8 -0
- package/dist/internal/web-app.js.map +1 -1
- package/dist/public-dir.d.ts +5 -7
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +54 -96
- package/dist/public-dir.js.map +1 -1
- package/dist/router.d.ts +2 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +9 -8
- package/dist/router.js.map +1 -1
- package/dist/server-components/vite.d.ts.map +1 -1
- package/dist/server-components/vite.js +8 -6
- package/dist/server-components/vite.js.map +1 -1
- package/dist/service-worker.d.ts +3 -1
- package/dist/service-worker.d.ts.map +1 -1
- package/dist/service-worker.js +29 -8
- package/dist/service-worker.js.map +1 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +3 -3
- package/dist/vite.js.map +1 -1
- package/dist/zones.d.ts +5 -1
- package/dist/zones.d.ts.map +1 -1
- package/dist/zones.js +17 -4
- package/dist/zones.js.map +1 -1
- package/package.json +4 -4
- package/src/build-plan.ts +26 -0
- package/src/build-vite.ts +4 -0
- package/src/build.ts +228 -68
- package/src/deferred.ts +20 -6
- package/src/dev.ts +7 -6
- package/src/diagnostic.ts +43 -0
- package/src/host-policy.ts +219 -0
- package/src/host-reach.ts +177 -0
- package/src/index.ts +10 -2
- package/src/internal/certified-components.ts +7 -0
- package/src/internal/codegen.ts +24 -5
- package/src/internal/node-adapter.ts +57 -0
- package/src/internal/output-guard.ts +166 -15
- package/src/internal/page-execution.ts +23 -9
- package/src/internal/parity.ts +3 -2
- package/src/internal/public-url.ts +0 -13
- package/src/internal/render-document.ts +11 -4
- package/src/internal/web-app.ts +27 -1
- package/src/public-dir.ts +70 -97
- package/src/router.ts +9 -8
- package/src/server-components/vite.ts +11 -6
- package/src/service-worker.ts +32 -9
- package/src/vite.ts +3 -3
- package/src/zones.ts +27 -5
package/src/build.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatBuildLog } from "./diagnostic.ts"
|
|
1
2
|
import { publicEnvDefines } from "./internal/public-env.ts"
|
|
2
3
|
|
|
3
4
|
export { publicEnvDefines } from "./internal/public-env.ts"
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
singleCopyPlugin as declaredSingleCopyPlugin,
|
|
26
27
|
readSingleCopyDeclaration,
|
|
27
28
|
} from "@nifrajs/core/single-copy"
|
|
29
|
+
import { isHiddenPath } from "@nifrajs/core/static-file"
|
|
28
30
|
import { type BunPlugin, Glob } from "bun"
|
|
29
31
|
import {
|
|
30
32
|
aggregateSizeReport,
|
|
@@ -32,12 +34,14 @@ import {
|
|
|
32
34
|
type BuildTarget,
|
|
33
35
|
type BuildTargetPlan,
|
|
34
36
|
type Bundler,
|
|
37
|
+
buildAssetPaths,
|
|
35
38
|
type ChunkSize,
|
|
36
39
|
detectNodeBuiltinsInClient,
|
|
37
40
|
detectServerOnlyInClient,
|
|
38
41
|
formatNodeBuiltinLeak,
|
|
39
42
|
formatServerOnlyLeak,
|
|
40
43
|
parseBuildTarget,
|
|
44
|
+
parseManifestBuildAssets,
|
|
41
45
|
parseManifestClientEntry,
|
|
42
46
|
parseManifestCopyChunks,
|
|
43
47
|
parseManifestCssLoading,
|
|
@@ -51,10 +55,12 @@ import { sanitizeOutputNames } from "./chunk-names.ts"
|
|
|
51
55
|
import { type CssLoadingMode, normalizeCssCodeSplit, normalizeCssLoading } from "./css-contract.ts"
|
|
52
56
|
import { discoverRoutes } from "./fs.ts"
|
|
53
57
|
import { generateClientEntry, generateServerManifest } from "./index.ts"
|
|
58
|
+
import { missingClientEntry } from "./internal/codegen.ts"
|
|
54
59
|
import { loadCopySplit } from "./internal/copy-split.ts"
|
|
55
60
|
import { assertDisposableDir } from "./internal/disposable-dir.ts"
|
|
56
61
|
import { entryChunks } from "./internal/entry-chunks.ts"
|
|
57
62
|
import { dedupePolicyFor } from "./internal/identity-policy.ts"
|
|
63
|
+
import { nodeAdapterRefusal } from "./internal/node-adapter.ts"
|
|
58
64
|
import {
|
|
59
65
|
assertDevelopmentProductionParity,
|
|
60
66
|
assertIdentityParity,
|
|
@@ -65,7 +71,7 @@ import {
|
|
|
65
71
|
servablePrerenderedPages,
|
|
66
72
|
} from "./internal/prerendered-pages.ts"
|
|
67
73
|
import { privateEnvCheck } from "./internal/private-env.ts"
|
|
68
|
-
import {
|
|
74
|
+
import { publicUrlPath } from "./internal/public-url.ts"
|
|
69
75
|
import {
|
|
70
76
|
assertNoSecrets,
|
|
71
77
|
buildEnvironment,
|
|
@@ -100,7 +106,7 @@ import { assertNoImportGlob, importGlobPlugin } from "./plugins/import-glob.ts"
|
|
|
100
106
|
import { zoneGuardPlugin } from "./plugins/zone-guard.ts"
|
|
101
107
|
// `buildTarget(static)` drives the SSG prerender engine directly (it's also re-exported below).
|
|
102
108
|
import { type PrerenderOptions, prerenderRoutes } from "./prerender.ts"
|
|
103
|
-
import {
|
|
109
|
+
import { IMMUTABLE, ONE_DAY } from "./public-dir.ts"
|
|
104
110
|
import { createZoneClassifier } from "./zones.ts"
|
|
105
111
|
|
|
106
112
|
export * from "./build-plan.ts"
|
|
@@ -360,7 +366,7 @@ export async function copyPublicDir(from: string, to: string): Promise<string[]>
|
|
|
360
366
|
const sourceReal = await realpath(source)
|
|
361
367
|
const copied: string[] = []
|
|
362
368
|
for await (const rel of new Glob("**/*").scan({ cwd: source, dot: true, onlyFiles: false })) {
|
|
363
|
-
if (
|
|
369
|
+
if (isHiddenPath(rel)) continue
|
|
364
370
|
const candidate = join(source, rel)
|
|
365
371
|
const candidateStat = await lstat(candidate)
|
|
366
372
|
if (candidateStat.isSymbolicLink()) {
|
|
@@ -394,6 +400,7 @@ export function resyncServerManifestSource(
|
|
|
394
400
|
): string {
|
|
395
401
|
const cssLoading = parseManifestCssLoading(source)
|
|
396
402
|
const copyChunks = parseManifestCopyChunks(source)
|
|
403
|
+
const buildAssets = parseManifestBuildAssets(source)
|
|
397
404
|
return generateServerManifest(manifest, {
|
|
398
405
|
resolve: (file) => `${routesPrefix}${file}`,
|
|
399
406
|
clientEntry: parseManifestClientEntry(source) ?? "",
|
|
@@ -401,6 +408,7 @@ export function resyncServerManifestSource(
|
|
|
401
408
|
routeStyles: parseManifestRouteStyles(source),
|
|
402
409
|
...(cssLoading !== undefined ? { cssLoading } : {}),
|
|
403
410
|
...(copyChunks !== undefined ? { copyChunks } : {}),
|
|
411
|
+
...(buildAssets !== undefined ? { buildAssets } : {}),
|
|
404
412
|
lazy: MANIFEST_IS_LAZY.test(source),
|
|
405
413
|
})
|
|
406
414
|
}
|
|
@@ -567,7 +575,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
567
575
|
if (verdict !== undefined) throw new Error(verdict)
|
|
568
576
|
if (refused.size > 0) throw new Error(formatRefusedFiles(refused, root))
|
|
569
577
|
throw new Error(
|
|
570
|
-
`[nifra/web] client build failed:\n${result.logs.map((l) =>
|
|
578
|
+
`[nifra/web] client build failed:\n${result.logs.map((l) => formatBuildLog(l, root)).join("\n")}`,
|
|
571
579
|
)
|
|
572
580
|
}
|
|
573
581
|
|
|
@@ -814,9 +822,16 @@ export interface BuildServerOptions {
|
|
|
814
822
|
readonly serverEntry: string
|
|
815
823
|
/** Output directory for the bundled worker (absolute path). */
|
|
816
824
|
readonly outDir: string
|
|
825
|
+
/**
|
|
826
|
+
* The manifest `buildClient` returned. Supplies {@link clientEntry}, {@link styles},
|
|
827
|
+
* {@link routeStyles}, {@link copyChunks} and {@link buildAssets} at once, so none can be forgotten
|
|
828
|
+
* (a forgotten `styles` ships unstyled pages); an option given explicitly still wins.
|
|
829
|
+
*/
|
|
830
|
+
readonly client?: BuildManifest
|
|
817
831
|
/** The content-hashed client entry URL (from `buildClient`'s manifest) - **baked** into the generated
|
|
818
|
-
* server manifest, since a disk-less worker can't read `manifest.json` at runtime.
|
|
819
|
-
|
|
832
|
+
* server manifest, since a disk-less worker can't read `manifest.json` at runtime. Required unless
|
|
833
|
+
* {@link client} is given. */
|
|
834
|
+
readonly clientEntry?: string
|
|
820
835
|
/** The app's aggregate stylesheet URLs (`buildClient`'s `BuildManifest.css`) - baked into the generated
|
|
821
836
|
* manifest so the server entry hands them to `createWebApp` (→ `<link rel="stylesheet">`). Omit ⇒ no CSS
|
|
822
837
|
* link (the built SSR page would otherwise render unstyled). */
|
|
@@ -825,6 +840,12 @@ export interface BuildServerOptions {
|
|
|
825
840
|
readonly routeStyles?: Readonly<Record<string, readonly string[]>> | undefined
|
|
826
841
|
/** Copy chunk URLs by locale (`buildClient`'s `BuildManifest.copyChunks`) - baked as `copyChunks`. */
|
|
827
842
|
readonly copyChunks?: Readonly<Record<string, string>> | undefined
|
|
843
|
+
/**
|
|
844
|
+
* Every file the build emitted, by request path (`buildClient`'s `assets` plus its `publicFiles`):
|
|
845
|
+
* baked as `buildAssets` and `isBuildAsset(pathname)` for a host policy or a hand-written static
|
|
846
|
+
* handler. Defaults from {@link client}; without either, the manifest exports neither name.
|
|
847
|
+
*/
|
|
848
|
+
readonly buildAssets?: readonly string[] | undefined
|
|
828
849
|
/** How framework-owned stylesheet links should activate before hydration (default `"blocking"`). */
|
|
829
850
|
readonly cssLoading?: CssLoadingMode
|
|
830
851
|
/** Route/layout file → import specifier in the generated manifest (default: a relative path from the
|
|
@@ -1096,6 +1117,12 @@ function formatRefusedFiles(refused: ReadonlyMap<string, string>, root: string):
|
|
|
1096
1117
|
return `[nifra/web] the browser build reached code that may not ship to a browser:\n${lines.join("\n")}`
|
|
1097
1118
|
}
|
|
1098
1119
|
|
|
1120
|
+
export {
|
|
1121
|
+
checkHostReach,
|
|
1122
|
+
type HostReachOptions,
|
|
1123
|
+
type HostReachReport,
|
|
1124
|
+
type HostReachResult,
|
|
1125
|
+
} from "./host-reach.ts"
|
|
1099
1126
|
export { importGlobPlugin } from "./plugins/import-glob.ts"
|
|
1100
1127
|
export { zoneGuardPlugin } from "./plugins/zone-guard.ts"
|
|
1101
1128
|
|
|
@@ -1151,7 +1178,26 @@ const solidWebServerPlugin = (from: string): BunPlugin => ({
|
|
|
1151
1178
|
* silently ships a broken worker.
|
|
1152
1179
|
*/
|
|
1153
1180
|
export async function buildServer(options: BuildServerOptions): Promise<ServerBuild> {
|
|
1154
|
-
const { routesDir, serverEntry, outDir,
|
|
1181
|
+
const { routesDir, serverEntry, outDir, client } = options
|
|
1182
|
+
if (
|
|
1183
|
+
client !== undefined &&
|
|
1184
|
+
(typeof client !== "object" ||
|
|
1185
|
+
client === null ||
|
|
1186
|
+
typeof client.entry !== "string" ||
|
|
1187
|
+
!Array.isArray(client.assets))
|
|
1188
|
+
) {
|
|
1189
|
+
throw new TypeError(
|
|
1190
|
+
"[nifra/web] buildServer `client` must be the manifest buildClient returned ({ entry, assets, ... })",
|
|
1191
|
+
)
|
|
1192
|
+
}
|
|
1193
|
+
const clientEntry = options.clientEntry ?? client?.entry
|
|
1194
|
+
if (clientEntry === undefined) throw missingClientEntry(clientEntry)
|
|
1195
|
+
const styles = options.styles ?? client?.css
|
|
1196
|
+
const routeStyles = options.routeStyles ?? client?.routeStyles
|
|
1197
|
+
const copyChunks = options.copyChunks ?? client?.copyChunks
|
|
1198
|
+
const cssLoading = options.cssLoading ?? client?.cssLoading
|
|
1199
|
+
const buildAssets =
|
|
1200
|
+
options.buildAssets ?? (client === undefined ? undefined : buildAssetPaths(client))
|
|
1155
1201
|
const entryDir = dirname(serverEntry)
|
|
1156
1202
|
const manifestFile = options.manifestFile ?? "server-manifest.ts"
|
|
1157
1203
|
// Default: import routes relative from the generated manifest (next to serverEntry) to routesDir.
|
|
@@ -1176,7 +1222,8 @@ export async function buildServer(options: BuildServerOptions): Promise<ServerBu
|
|
|
1176
1222
|
styles,
|
|
1177
1223
|
routeStyles,
|
|
1178
1224
|
...(cssLoading !== undefined ? { cssLoading } : {}),
|
|
1179
|
-
...(
|
|
1225
|
+
...(copyChunks !== undefined ? { copyChunks } : {}),
|
|
1226
|
+
...(buildAssets !== undefined ? { buildAssets } : {}),
|
|
1180
1227
|
lazy,
|
|
1181
1228
|
}),
|
|
1182
1229
|
)
|
|
@@ -1212,8 +1259,9 @@ export async function buildServer(options: BuildServerOptions): Promise<ServerBu
|
|
|
1212
1259
|
],
|
|
1213
1260
|
})
|
|
1214
1261
|
if (!result.success) {
|
|
1262
|
+
const root = resolvePath(options.root ?? dirname(routesDir))
|
|
1215
1263
|
throw new Error(
|
|
1216
|
-
`[nifra/web] server build failed:\n${result.logs.map((l) =>
|
|
1264
|
+
`[nifra/web] server build failed:\n${result.logs.map((l) => formatBuildLog(l, root)).join("\n")}`,
|
|
1217
1265
|
)
|
|
1218
1266
|
}
|
|
1219
1267
|
// Named as the metafile names outputs: relative to `outdir`.
|
|
@@ -1282,6 +1330,7 @@ export const SERVER_ENTRY_OPTIONS = [
|
|
|
1282
1330
|
"csp",
|
|
1283
1331
|
"nonce",
|
|
1284
1332
|
"i18n",
|
|
1333
|
+
"hostPolicy",
|
|
1285
1334
|
] as const
|
|
1286
1335
|
export type ServerEntryOption = (typeof SERVER_ENTRY_OPTIONS)[number]
|
|
1287
1336
|
/** Each importable option mapped to the specifier of the module that exports it. */
|
|
@@ -1306,6 +1355,13 @@ const PLATFORM_CLIENT_IP_HEADERS: Readonly<Partial<Record<BuildTarget, string>>>
|
|
|
1306
1355
|
vercel: "x-real-ip",
|
|
1307
1356
|
}
|
|
1308
1357
|
|
|
1358
|
+
// A static path a host's policy refuses is not answered from disk: it falls through to the app, whose
|
|
1359
|
+
// own copy of the policy refuses it the same way it refuses a page.
|
|
1360
|
+
const HOST_ALLOWS_STATIC =
|
|
1361
|
+
' && hostPolicy.allows(request.headers.get("host") ?? "", pathname, buildAssets)'
|
|
1362
|
+
const NODE_STATIC_ALLOW =
|
|
1363
|
+
" allow: (host: string, pathname: string) => hostPolicy.allows(host, pathname, buildAssets),"
|
|
1364
|
+
|
|
1309
1365
|
/** Where a self-hosting build writes its prerendered pages, beside `server.js`. */
|
|
1310
1366
|
const PRERENDERED_DIR = "_prerendered"
|
|
1311
1367
|
|
|
@@ -1379,8 +1435,9 @@ export function generateServerEntry(options: {
|
|
|
1379
1435
|
if (target === "static") {
|
|
1380
1436
|
throw new Error("[nifra/web] generateServerEntry: `static` has no server entry (SSG only)")
|
|
1381
1437
|
}
|
|
1382
|
-
// Bun
|
|
1383
|
-
//
|
|
1438
|
+
// Bun serves the client bundle through `servePublicDir` and Node through @nifrajs/node's own reader,
|
|
1439
|
+
// both on core's static-file policy. Deno bundles for the browser condition, where `node:fs` is a
|
|
1440
|
+
// stub, so its entry runs the same checks on Deno's own file API.
|
|
1384
1441
|
const nodeFs = target === "bun" || target === "node"
|
|
1385
1442
|
const prerendered = Object.entries(options.prerenderedPages ?? {})
|
|
1386
1443
|
if (prerendered.length > 0 && !nodeFs) {
|
|
@@ -1388,13 +1445,10 @@ export function generateServerEntry(options: {
|
|
|
1388
1445
|
`[nifra/web] generateServerEntry: prerendered pages are served by the bun and node targets, not ${target}`,
|
|
1389
1446
|
)
|
|
1390
1447
|
}
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
? 'import { createWebApp, prerenderedPageReader, servePublicDir } from "@nifrajs/web"'
|
|
1396
|
-
: 'import { createWebApp, servePublicDir } from "@nifrajs/web"',
|
|
1397
|
-
]
|
|
1448
|
+
const webImports = ["createWebApp"]
|
|
1449
|
+
if (prerendered.length > 0) webImports.push("prerenderedPageReader")
|
|
1450
|
+
if (target === "bun") webImports.push("servePublicDir")
|
|
1451
|
+
const lines: string[] = [`import { ${webImports.join(", ")} } from "@nifrajs/web"`]
|
|
1398
1452
|
if (backendImport !== undefined) lines.push('import { inProcessClient } from "@nifrajs/client"')
|
|
1399
1453
|
lines.push(`import { adapter } from ${JSON.stringify(adapterImport)}`)
|
|
1400
1454
|
if (useImport !== undefined) lines.push(`import { use } from ${JSON.stringify(useImport)}`)
|
|
@@ -1404,16 +1458,29 @@ export function generateServerEntry(options: {
|
|
|
1404
1458
|
if (backendImport !== undefined) {
|
|
1405
1459
|
lines.push(`import { backend } from ${JSON.stringify(backendImport)}`)
|
|
1406
1460
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1461
|
+
// A host policy is checked before the static files too, which takes the build's own file set.
|
|
1462
|
+
const hostPolicy = importedOptions.includes("hostPolicy")
|
|
1463
|
+
const manifestImports = [
|
|
1464
|
+
...(hostPolicy ? ["buildAssets"] : []),
|
|
1465
|
+
"clientEntry",
|
|
1466
|
+
...(options.copyChunks === true ? ["copyChunks"] : []),
|
|
1467
|
+
"cssLoading",
|
|
1468
|
+
"manifest",
|
|
1469
|
+
"styles",
|
|
1470
|
+
"routeStyles",
|
|
1471
|
+
]
|
|
1472
|
+
lines.push(`import { ${manifestImports.join(", ")} } from "./server-manifest"`)
|
|
1412
1473
|
// Cloudflare needs the fetch-handler shape; the others call app.fetch directly.
|
|
1413
1474
|
if (target === "cloudflare") lines.push('import { toFetchHandler } from "@nifrajs/core/server"')
|
|
1414
1475
|
if (target === "node") lines.push('import { serve } from "@nifrajs/node"')
|
|
1415
1476
|
if (nodeFs) lines.push('import { fileURLToPath } from "node:url"')
|
|
1416
|
-
if (target === "deno")
|
|
1477
|
+
if (target === "deno") {
|
|
1478
|
+
lines.push(
|
|
1479
|
+
'import { parseByteRange } from "@nifrajs/core/range"',
|
|
1480
|
+
'import { CONTENT_SNAPSHOT_MAX, contentTags, contentTypeOf, ifRangeAllowsPartial, isHiddenPath, isNotModified, lastModifiedOf, streamedLastModifiedOf } from "@nifrajs/core/static-file"',
|
|
1481
|
+
'import { createHash } from "node:crypto"',
|
|
1482
|
+
)
|
|
1483
|
+
}
|
|
1417
1484
|
if (prerendered.length > 0) {
|
|
1418
1485
|
const headers = Object.fromEntries(prerendered.map(([path, page]) => [path, page.headers]))
|
|
1419
1486
|
const files = Object.fromEntries(prerendered.map(([path, page]) => [path, page.file]))
|
|
@@ -1436,6 +1503,7 @@ export function generateServerEntry(options: {
|
|
|
1436
1503
|
...(useImport !== undefined ? [" use,"] : []),
|
|
1437
1504
|
...(prerendered.length > 0 ? [" prerenderedPages,"] : []),
|
|
1438
1505
|
...importedOptions.map((name) => ` ${name},`),
|
|
1506
|
+
...(hostPolicy ? [" buildAssets,"] : []),
|
|
1439
1507
|
" manifest,",
|
|
1440
1508
|
" clientEntry,",
|
|
1441
1509
|
" styles,",
|
|
@@ -1468,7 +1536,7 @@ export function generateServerEntry(options: {
|
|
|
1468
1536
|
" fetch(request: Request, env: Assets, ctx: ExecutionContext) {",
|
|
1469
1537
|
" const { pathname } = new URL(request.url)",
|
|
1470
1538
|
" // Only the build's own outputs: the hashed bundle prefix and the files it copied.",
|
|
1471
|
-
|
|
1539
|
+
` if (env?.ASSETS !== undefined && (pathname.startsWith("/assets/") || PUBLIC_FILES.has(pathname))${hostPolicy ? HOST_ALLOWS_STATIC : ""}) {`,
|
|
1472
1540
|
" return env.ASSETS.fetch(request)",
|
|
1473
1541
|
" }",
|
|
1474
1542
|
" return handler.fetch(request, env as never, ctx)",
|
|
@@ -1494,6 +1562,36 @@ export function generateServerEntry(options: {
|
|
|
1494
1562
|
...(hashedFiles !== undefined
|
|
1495
1563
|
? [`const HASHED_FILES = new Set(${JSON.stringify(hashedFiles)})`]
|
|
1496
1564
|
: []),
|
|
1565
|
+
)
|
|
1566
|
+
if (target === "node") {
|
|
1567
|
+
// The app itself with the adapter's own reader, not a `{ fetch }` wrapper around app.fetch: the
|
|
1568
|
+
// wrapper hid the node-direct lane, so every page built an undici Request and drained a Response,
|
|
1569
|
+
// and every file went through one too. The root mount serves only the files the build listed.
|
|
1570
|
+
lines.push(
|
|
1571
|
+
"await serve(app, {",
|
|
1572
|
+
" port: Number(process.env.PORT ?? 3000),",
|
|
1573
|
+
" static: [",
|
|
1574
|
+
" {",
|
|
1575
|
+
' dir: fileURLToPath(new URL("./assets/", import.meta.url)),',
|
|
1576
|
+
' prefix: "/assets",',
|
|
1577
|
+
...(hashedFiles !== undefined ? [" hashedFiles: HASHED_FILES,"] : []),
|
|
1578
|
+
` cacheControl: ${JSON.stringify(ONE_DAY)},`,
|
|
1579
|
+
...(hostPolicy ? [NODE_STATIC_ALLOW] : []),
|
|
1580
|
+
" },",
|
|
1581
|
+
" {",
|
|
1582
|
+
" dir: fileURLToPath(STATIC_ROOT),",
|
|
1583
|
+
' prefix: "/",',
|
|
1584
|
+
" files: PUBLIC_FILES,",
|
|
1585
|
+
" immutable: false,",
|
|
1586
|
+
` cacheControl: ${JSON.stringify(ONE_DAY)},`,
|
|
1587
|
+
...(hostPolicy ? [NODE_STATIC_ALLOW] : []),
|
|
1588
|
+
" },",
|
|
1589
|
+
" ],",
|
|
1590
|
+
"})",
|
|
1591
|
+
)
|
|
1592
|
+
return `${lines.join("\n")}\n`
|
|
1593
|
+
}
|
|
1594
|
+
lines.push(
|
|
1497
1595
|
"const staticPath = (pathname: string): string | undefined => {",
|
|
1498
1596
|
" if (PUBLIC_FILES.has(pathname)) return '.' + pathname",
|
|
1499
1597
|
' if (!pathname.startsWith("/assets/")) return undefined',
|
|
@@ -1505,11 +1603,10 @@ export function generateServerEntry(options: {
|
|
|
1505
1603
|
' segment !== "." && segment !== ".." && /^[A-Za-z0-9._-]+$/.test(segment)',
|
|
1506
1604
|
" return segments.every(safe) ? '.' + pathname : undefined",
|
|
1507
1605
|
"}",
|
|
1508
|
-
// The handler `nifra dev` serves public/ with: its type table, nosniff and cache policy,
|
|
1509
|
-
//
|
|
1510
|
-
//
|
|
1511
|
-
|
|
1512
|
-
...(nodeFs
|
|
1606
|
+
// The handler `nifra dev` serves public/ with: its type table, nosniff and cache policy, content
|
|
1607
|
+
// tags, byte ranges and conditional requests, and a descriptor opened only once the real path is
|
|
1608
|
+
// inside the root, so a link planted in the deployed tree cannot reach a file outside it.
|
|
1609
|
+
...(target === "bun"
|
|
1513
1610
|
? [
|
|
1514
1611
|
"const serveStatic = servePublicDir({",
|
|
1515
1612
|
" dir: fileURLToPath(STATIC_ROOT),",
|
|
@@ -1531,7 +1628,7 @@ export function generateServerEntry(options: {
|
|
|
1531
1628
|
" async fetch(req, server) {",
|
|
1532
1629
|
" const { pathname } = new URL(req.url)",
|
|
1533
1630
|
// Braced: the next emitted line opens with `(`, which would otherwise call the returned value.
|
|
1534
|
-
|
|
1631
|
+
` if (staticPath(pathname) !== undefined${hostPolicy ? HOST_ALLOWS_STATIC.replaceAll("request", "req") : ""}) {`,
|
|
1535
1632
|
" return staticResponse(req)",
|
|
1536
1633
|
" }",
|
|
1537
1634
|
` ${markFramed}`,
|
|
@@ -1545,28 +1642,13 @@ export function generateServerEntry(options: {
|
|
|
1545
1642
|
)
|
|
1546
1643
|
return `${lines.join("\n")}\n`
|
|
1547
1644
|
}
|
|
1548
|
-
if (target === "node") {
|
|
1549
|
-
lines.push(
|
|
1550
|
-
"await serve(",
|
|
1551
|
-
" {",
|
|
1552
|
-
" async fetch(req, platform) {",
|
|
1553
|
-
" const { pathname } = new URL(req.url)",
|
|
1554
|
-
" if (staticPath(pathname) !== undefined) return staticResponse(req)",
|
|
1555
|
-
" return app.fetch(req, platform)",
|
|
1556
|
-
" },",
|
|
1557
|
-
" },",
|
|
1558
|
-
" { port: Number(process.env.PORT ?? 3000) },",
|
|
1559
|
-
")",
|
|
1560
|
-
)
|
|
1561
|
-
return `${lines.join("\n")}\n`
|
|
1562
|
-
}
|
|
1563
1645
|
// deno
|
|
1564
1646
|
lines.push(
|
|
1565
1647
|
"// @ts-ignore - Deno global is present on the Deno runtime this output targets.",
|
|
1566
1648
|
'Deno.serve({ port: Number(Deno.env.get("PORT") ?? "3000") }, async (req, info) => {',
|
|
1567
1649
|
" const { pathname } = new URL(req.url)",
|
|
1568
1650
|
" const filePath = staticPath(pathname)",
|
|
1569
|
-
|
|
1651
|
+
` if (filePath !== undefined${hostPolicy ? HOST_ALLOWS_STATIC.replaceAll("request", "req") : ""}) {`,
|
|
1570
1652
|
" return staticResponse(req, filePath, pathname)",
|
|
1571
1653
|
" }",
|
|
1572
1654
|
` ${markFramed}`,
|
|
@@ -1577,34 +1659,40 @@ export function generateServerEntry(options: {
|
|
|
1577
1659
|
}
|
|
1578
1660
|
|
|
1579
1661
|
/**
|
|
1580
|
-
* The Deno entry's static handler: `servePublicDir`'s checks on Deno's file API. The real
|
|
1581
|
-
* inside the root and cross no hidden segment, and the opened file must still be the one
|
|
1582
|
-
* names, so a link planted in the deployed tree cannot reach a file outside it or a `.env`
|
|
1583
|
-
*
|
|
1584
|
-
*
|
|
1662
|
+
* The Deno entry's static handler: `servePublicDir`'s checks and answers on Deno's file API. The real
|
|
1663
|
+
* path must sit inside the root and cross no hidden segment, and the opened file must still be the one
|
|
1664
|
+
* that path names, so a link planted in the deployed tree cannot reach a file outside it or a `.env`
|
|
1665
|
+
* inside it. The file is typed by its real path, tagged by its content and dated only by an mtime its
|
|
1666
|
+
* own last write set, through the same policy as servePublicDir: a current cached copy is a 304, and a
|
|
1667
|
+
* range survives only an `If-Range` naming that tag. A file is read once to tag it, and one small
|
|
1668
|
+
* enough is then served from those bytes; otherwise one byte range is served from a seek, several as
|
|
1669
|
+
* the whole file.
|
|
1585
1670
|
*/
|
|
1586
1671
|
function denoStaticResponse(hashed: boolean): string[] {
|
|
1587
1672
|
return [
|
|
1588
|
-
|
|
1589
|
-
"
|
|
1590
|
-
' "content-type": TYPES[pathname.slice(pathname.lastIndexOf(".")).toLowerCase()] ?? "application/octet-stream",',
|
|
1673
|
+
"const staticHeaders = (pathname: string, file: string): Record<string, string> => ({",
|
|
1674
|
+
' "content-type": contentTypeOf(file),',
|
|
1591
1675
|
` "cache-control": ${hashed ? "HASHED_FILES.has(pathname)" : 'pathname.startsWith("/assets/")'} ? ${JSON.stringify(IMMUTABLE)} : ${JSON.stringify(ONE_DAY)},`,
|
|
1592
1676
|
' "x-content-type-options": "nosniff",',
|
|
1593
1677
|
' "accept-ranges": "bytes",',
|
|
1594
1678
|
"})",
|
|
1595
1679
|
"// @ts-ignore - Deno global is present on the Deno runtime this output targets.",
|
|
1596
1680
|
"const STATIC_REAL: string = await Deno.realPath(STATIC_ROOT)",
|
|
1597
|
-
"
|
|
1598
|
-
|
|
1681
|
+
"// One per process: the tag of a file is remembered by its real path.",
|
|
1682
|
+
"// A stat costs Deno more than hashing a snapshot of up to 24 KiB, so only a larger one is restatted.",
|
|
1683
|
+
"const STATIC_TAGS = contentTags(createHash, { rehashMax: 24 * 1024 })",
|
|
1684
|
+
"// Up to this many bytes are read whole to tag a file, and the response is then served from them.",
|
|
1685
|
+
"const STATIC_INLINE_MAX = CONTENT_SNAPSHOT_MAX",
|
|
1599
1686
|
"const staticResponse = async (req: Request, filePath: string, pathname: string): Promise<Response> => {",
|
|
1600
1687
|
' const missing = () => new Response("not found", { status: 404 })',
|
|
1601
1688
|
' if (req.method !== "GET" && req.method !== "HEAD") return missing()',
|
|
1602
1689
|
" let file, info",
|
|
1690
|
+
' let real = ""',
|
|
1603
1691
|
" try {",
|
|
1604
1692
|
" // @ts-ignore - Deno global is present on the Deno runtime this output targets.",
|
|
1605
|
-
"
|
|
1693
|
+
" real = await Deno.realPath(new URL(filePath, STATIC_ROOT))",
|
|
1606
1694
|
' if (!real.startsWith(STATIC_REAL + (STATIC_REAL.includes("\\\\") ? "\\\\" : "/"))) return missing()',
|
|
1607
|
-
" if (
|
|
1695
|
+
" if (isHiddenPath(real.slice(STATIC_REAL.length + 1))) return missing()",
|
|
1608
1696
|
" // @ts-ignore - Deno global is present on the Deno runtime this output targets.",
|
|
1609
1697
|
" file = await Deno.open(real)",
|
|
1610
1698
|
" // Deno.open follows links, so a directory swapped for one after the check above redirects it.",
|
|
@@ -1622,12 +1710,60 @@ function denoStaticResponse(hashed: boolean): string[] {
|
|
|
1622
1710
|
" file?.close()",
|
|
1623
1711
|
" return missing()",
|
|
1624
1712
|
" }",
|
|
1713
|
+
" if (!info.isFile) {",
|
|
1714
|
+
" file.close()",
|
|
1715
|
+
" return missing()",
|
|
1716
|
+
" }",
|
|
1625
1717
|
" const size: number = info.size",
|
|
1626
|
-
" const headers = new Headers(staticHeaders(pathname))",
|
|
1627
|
-
|
|
1628
|
-
|
|
1718
|
+
" const headers = new Headers(staticHeaders(pathname, real))",
|
|
1719
|
+
" // Deno reports a field the platform does not keep as null; NaN never matches a remembered version.",
|
|
1720
|
+
" const versionOf = (stat: { dev: number; ino: number | null; size: number; mtime: Date | null; ctime: Date | null }) => ({",
|
|
1721
|
+
" dev: stat.dev,",
|
|
1722
|
+
" ino: stat.ino ?? Number.NaN,",
|
|
1723
|
+
" size: stat.size,",
|
|
1724
|
+
" mtimeMs: stat.mtime?.getTime() ?? Number.NaN,",
|
|
1725
|
+
" ctimeMs: stat.ctime?.getTime() ?? Number.NaN,",
|
|
1726
|
+
" })",
|
|
1727
|
+
" const version = versionOf(info)",
|
|
1728
|
+
" const snapshot = size <= STATIC_INLINE_MAX",
|
|
1729
|
+
" const lastModified = snapshot ? lastModifiedOf(version) : streamedLastModifiedOf(version)",
|
|
1730
|
+
' const ifNoneMatch = req.headers.get("if-none-match")',
|
|
1731
|
+
' const ifModifiedSince = req.headers.get("if-modified-since")',
|
|
1732
|
+
" let whole: Uint8Array<ArrayBuffer> | undefined",
|
|
1733
|
+
" // A HEAD or a 304 sends none of the file, so the tag remembered for this settled version",
|
|
1734
|
+
" // answers it from the stat alone: a write since that tag would show in the stat.",
|
|
1735
|
+
" let etag: string | undefined = snapshot ? STATIC_TAGS.cached(real, version) : undefined",
|
|
1736
|
+
" try {",
|
|
1737
|
+
' if (snapshot && (etag === undefined || !(req.method === "HEAD" || isNotModified(ifNoneMatch, ifModifiedSince, etag, lastModified)))) {',
|
|
1738
|
+
" whole = new Uint8Array(size)",
|
|
1739
|
+
" let filled = 0",
|
|
1740
|
+
" while (filled < size) {",
|
|
1741
|
+
" const read: number | null = await file.read(whole.subarray(filled))",
|
|
1742
|
+
" if (read === null || read === 0) break",
|
|
1743
|
+
" filled += read",
|
|
1744
|
+
" }",
|
|
1745
|
+
" if (filled < size) whole = whole.subarray(0, filled)",
|
|
1746
|
+
" etag = await STATIC_TAGS.ofSnapshot(real, version, whole, async () => versionOf(await file.stat()))",
|
|
1747
|
+
" }",
|
|
1748
|
+
" } catch {",
|
|
1749
|
+
" file.close()",
|
|
1750
|
+
" return missing()",
|
|
1751
|
+
" }",
|
|
1752
|
+
' if (etag !== undefined) headers.set("etag", etag)',
|
|
1753
|
+
' if (lastModified !== undefined) headers.set("last-modified", new Date(lastModified).toUTCString())',
|
|
1754
|
+
" if (isNotModified(ifNoneMatch, ifModifiedSince, etag, lastModified)) {",
|
|
1755
|
+
" file.close()",
|
|
1756
|
+
' headers.delete("content-type")',
|
|
1757
|
+
" return new Response(null, { status: 304, headers })",
|
|
1758
|
+
" }",
|
|
1759
|
+
' const rangeHeader = req.method === "GET" ? req.headers.get("range") : null',
|
|
1760
|
+
" const range =",
|
|
1761
|
+
" // A streamed body's date is weak: a write during the stream must never splice two versions.",
|
|
1762
|
+
' rangeHeader !== null && ifRangeAllowsPartial(req.headers.get("if-range"), etag, snapshot ? lastModified : undefined)',
|
|
1763
|
+
" ? parseByteRange(rangeHeader, size)",
|
|
1764
|
+
' : { kind: "none" as const }',
|
|
1765
|
+
' if (req.method === "HEAD" || range.kind === "unsatisfiable") {',
|
|
1629
1766
|
" file.close()",
|
|
1630
|
-
" if (!info.isFile) return missing()",
|
|
1631
1767
|
' if (range.kind !== "unsatisfiable") {',
|
|
1632
1768
|
' headers.set("content-length", String(size))',
|
|
1633
1769
|
" return new Response(null, { headers })",
|
|
@@ -1638,10 +1774,16 @@ function denoStaticResponse(hashed: boolean): string[] {
|
|
|
1638
1774
|
" }",
|
|
1639
1775
|
' if (range.kind === "satisfiable" && range.ranges.length === 1) {',
|
|
1640
1776
|
" const { start, end } = range.ranges[0]!",
|
|
1777
|
+
' headers.set("content-range", "bytes " + start + "-" + end + "/" + size)',
|
|
1778
|
+
" if (whole !== undefined) {",
|
|
1779
|
+
" file.close()",
|
|
1780
|
+
" const part = whole.subarray(start, end + 1)",
|
|
1781
|
+
' headers.set("content-length", String(part.byteLength))',
|
|
1782
|
+
" return new Response(part, { status: 206, headers })",
|
|
1783
|
+
" }",
|
|
1641
1784
|
" // @ts-ignore - Deno global is present on the Deno runtime this output targets.",
|
|
1642
1785
|
" await file.seek(start, Deno.SeekMode.Start)",
|
|
1643
1786
|
" let left = end - start + 1",
|
|
1644
|
-
' headers.set("content-range", "bytes " + start + "-" + end + "/" + size)',
|
|
1645
1787
|
' headers.set("content-length", String(left))',
|
|
1646
1788
|
" const body = new ReadableStream<Uint8Array>({",
|
|
1647
1789
|
" async pull(controller) {",
|
|
@@ -1663,6 +1805,11 @@ function denoStaticResponse(hashed: boolean): string[] {
|
|
|
1663
1805
|
" })",
|
|
1664
1806
|
" return new Response(body, { status: 206, headers })",
|
|
1665
1807
|
" }",
|
|
1808
|
+
" if (whole !== undefined) {",
|
|
1809
|
+
" file.close()",
|
|
1810
|
+
' headers.set("content-length", String(whole.byteLength))',
|
|
1811
|
+
" return new Response(whole, { headers })",
|
|
1812
|
+
" }",
|
|
1666
1813
|
' headers.set("content-length", String(size))',
|
|
1667
1814
|
" return new Response(file.readable, { headers })",
|
|
1668
1815
|
"}",
|
|
@@ -1814,6 +1961,7 @@ export const bunBundler: Bundler = {
|
|
|
1814
1961
|
...(input.styles !== undefined ? { styles: input.styles } : {}),
|
|
1815
1962
|
...(input.routeStyles !== undefined ? { routeStyles: input.routeStyles } : {}),
|
|
1816
1963
|
...(input.copyChunks !== undefined ? { copyChunks: input.copyChunks } : {}),
|
|
1964
|
+
...(input.buildAssets !== undefined ? { buildAssets: input.buildAssets } : {}),
|
|
1817
1965
|
...(input.plugins ? { plugins: input.plugins as BunPlugin[] } : {}),
|
|
1818
1966
|
...(input.define ? { define: input.define } : {}),
|
|
1819
1967
|
...(input.cssLoading !== undefined ? { cssLoading: input.cssLoading } : {}),
|
|
@@ -1924,6 +2072,11 @@ export async function buildTargetWith(
|
|
|
1924
2072
|
]
|
|
1925
2073
|
assertDisposableDir("outDir", outDir, sources)
|
|
1926
2074
|
assertDisposableDir("workDir", workDir, sources)
|
|
2075
|
+
// Before anything is cleared, so a refused build leaves the last deploy dir in place.
|
|
2076
|
+
if (targetPlan.target === "node") {
|
|
2077
|
+
const refusal = nodeAdapterRefusal(workDir)
|
|
2078
|
+
if (refusal !== undefined) throw new Error(refusal)
|
|
2079
|
+
}
|
|
1927
2080
|
const { rmSync } = await import("node:fs")
|
|
1928
2081
|
rmSync(outDir, { recursive: true, force: true })
|
|
1929
2082
|
rmSync(workDir, { recursive: true, force: true })
|
|
@@ -2067,6 +2220,7 @@ export async function buildTargetWith(
|
|
|
2067
2220
|
...(client.css !== undefined ? { styles: client.css } : {}),
|
|
2068
2221
|
...(client.routeStyles !== undefined ? { routeStyles: client.routeStyles } : {}),
|
|
2069
2222
|
...(client.copyChunks !== undefined ? { copyChunks: client.copyChunks } : {}),
|
|
2223
|
+
buildAssets: buildAssetPaths(client),
|
|
2070
2224
|
...(options.serverPlugins ? { plugins: options.serverPlugins } : {}),
|
|
2071
2225
|
define: { "process.env.NODE_ENV": '"production"', ...(options.define ?? {}) },
|
|
2072
2226
|
...(client.cssLoading !== undefined
|
|
@@ -2085,10 +2239,16 @@ export async function buildTargetWith(
|
|
|
2085
2239
|
cpSync(worker, `${outDir}/${targetPlan.outputFile}`)
|
|
2086
2240
|
// The app's real patterns, so a directory is only collapsed into a glob once the route table
|
|
2087
2241
|
// proves nothing can be served beneath it.
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2242
|
+
// A host policy must see every file request, which Pages answers from the CDN before the worker
|
|
2243
|
+
// when the path is excluded, so with one nothing is: each file costs an invocation, and the
|
|
2244
|
+
// worker serves the allowed ones from ASSETS.
|
|
2245
|
+
const rules =
|
|
2246
|
+
options.optionImports?.hostPolicy === undefined
|
|
2247
|
+
? cloudflareRouteRules(
|
|
2248
|
+
publicFiles,
|
|
2249
|
+
discoverRoutes(routesDir).routes.map((route) => route.pattern),
|
|
2250
|
+
)
|
|
2251
|
+
: { include: ["/*"], exclude: [], omitted: [] }
|
|
2092
2252
|
writeFileSync(
|
|
2093
2253
|
`${outDir}/_routes.json`,
|
|
2094
2254
|
`${JSON.stringify({ version: 1, include: rules.include, exclude: rules.exclude }, null, 2)}\n`,
|
package/src/deferred.ts
CHANGED
|
@@ -47,6 +47,20 @@ function isDeferred(value: unknown): value is Deferred<unknown> {
|
|
|
47
47
|
)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** `target[key] = value` runs the prototype setter for a `__proto__` key; define it as data instead. */
|
|
51
|
+
function setOwn(target: Record<string, unknown>, key: string, value: unknown): void {
|
|
52
|
+
if (key === "__proto__") {
|
|
53
|
+
Object.defineProperty(target, key, {
|
|
54
|
+
value,
|
|
55
|
+
enumerable: true,
|
|
56
|
+
writable: true,
|
|
57
|
+
configurable: true,
|
|
58
|
+
})
|
|
59
|
+
} else {
|
|
60
|
+
target[key] = value
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
/**
|
|
51
65
|
* Split a loader (or action) result into the **component-facing** data (deferred values carry their
|
|
52
66
|
* assigned `id` + promise, for `<Await>`) and the **client-serializable** data (deferred values
|
|
@@ -113,8 +127,8 @@ export function prepareDeferred(
|
|
|
113
127
|
const r = split(v)
|
|
114
128
|
if (r === undefined) {
|
|
115
129
|
if (fc !== undefined && cl !== undefined) {
|
|
116
|
-
fc
|
|
117
|
-
cl
|
|
130
|
+
setOwn(fc, k, v)
|
|
131
|
+
setOwn(cl, k, v)
|
|
118
132
|
}
|
|
119
133
|
continue
|
|
120
134
|
}
|
|
@@ -125,12 +139,12 @@ export function prepareDeferred(
|
|
|
125
139
|
if (!Object.hasOwn(input, prev)) continue
|
|
126
140
|
if (prev === k) break
|
|
127
141
|
const prevValue = input[prev]
|
|
128
|
-
fc
|
|
129
|
-
cl
|
|
142
|
+
setOwn(fc, prev, prevValue)
|
|
143
|
+
setOwn(cl, prev, prevValue)
|
|
130
144
|
}
|
|
131
145
|
}
|
|
132
|
-
fc
|
|
133
|
-
cl
|
|
146
|
+
setOwn(fc, k, r.fc)
|
|
147
|
+
setOwn(cl, k, r.cl)
|
|
134
148
|
}
|
|
135
149
|
return fc === undefined || cl === undefined ? undefined : { fc, cl }
|
|
136
150
|
}
|