@nifrajs/web 5.0.0 → 5.2.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 +2970 -0
- package/README.md +8 -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 +12 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +43 -6
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +7 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +39 -22
- package/dist/client.js.map +1 -1
- package/dist/dev-port.d.ts +3 -2
- package/dist/dev-port.d.ts.map +1 -1
- package/dist/dev-port.js +2 -6
- package/dist/dev-port.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -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/package-entries.d.ts +34 -0
- package/dist/internal/package-entries.d.ts.map +1 -0
- package/dist/internal/package-entries.js +324 -0
- package/dist/internal/package-entries.js.map +1 -0
- package/dist/internal/page-execution.d.ts +19 -1
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +123 -14
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +15 -2
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/private-env.d.ts +8 -0
- package/dist/internal/private-env.d.ts.map +1 -1
- package/dist/internal/private-env.js +25 -10
- package/dist/internal/private-env.js.map +1 -1
- package/dist/internal/render-document.d.ts +15 -46
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +59 -62
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/route-path.d.ts +6 -0
- package/dist/internal/route-path.d.ts.map +1 -1
- package/dist/internal/route-path.js +11 -0
- package/dist/internal/route-path.js.map +1 -1
- package/dist/internal/secret-scan.d.ts.map +1 -1
- package/dist/internal/secret-scan.js +103 -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 +38 -2
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +30 -3
- package/dist/internal/web-app.js.map +1 -1
- package/dist/internal/without-query.d.ts +4 -0
- package/dist/internal/without-query.d.ts.map +1 -0
- package/dist/internal/without-query.js +9 -0
- package/dist/internal/without-query.js.map +1 -0
- package/dist/internal/zone-graph.d.ts.map +1 -1
- package/dist/internal/zone-graph.js +17 -6
- package/dist/internal/zone-graph.js.map +1 -1
- package/dist/isr.d.ts +1 -1
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +15 -2
- package/dist/isr.js.map +1 -1
- package/dist/manifest.d.ts +9 -0
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +1 -0
- package/dist/manifest.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/plugins/vite-leak-guard.d.ts +6 -0
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
- package/dist/plugins/vite-leak-guard.js +80 -7
- package/dist/plugins/vite-leak-guard.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
- package/dist/plugins/vite-zone-guard.js +17 -21
- package/dist/plugins/vite-zone-guard.js.map +1 -1
- package/dist/plugins/zone-guard.d.ts.map +1 -1
- package/dist/plugins/zone-guard.js +44 -3
- package/dist/plugins/zone-guard.js.map +1 -1
- package/dist/prerender.d.ts +5 -1
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +41 -19
- package/dist/prerender.js.map +1 -1
- package/dist/render-seam.d.ts +60 -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 +13 -9
- package/dist/router.js.map +1 -1
- package/dist/seo.d.ts +49 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +48 -0
- package/dist/seo.js.map +1 -0
- 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 +1 -28
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +3 -69
- package/dist/vite.js.map +1 -1
- package/dist/zones.d.ts +30 -8
- package/dist/zones.d.ts.map +1 -1
- package/dist/zones.js +185 -46
- package/dist/zones.js.map +1 -1
- package/package.json +10 -4
- package/src/build-plan.ts +21 -0
- package/src/build-vite.ts +60 -3
- package/src/build.ts +57 -4
- package/src/client.ts +42 -22
- package/src/dev-port.ts +4 -10
- package/src/index.ts +8 -4
- package/src/internal/codegen.ts +32 -3
- package/src/internal/copy-split.ts +220 -0
- package/src/internal/package-entries.ts +331 -0
- package/src/internal/page-execution.ts +150 -13
- package/src/internal/parity.ts +15 -2
- package/src/internal/private-env.ts +27 -13
- package/src/internal/render-document.ts +100 -81
- package/src/internal/route-path.ts +16 -0
- package/src/internal/secret-scan.ts +101 -1
- package/src/internal/typescript-api.ts +11 -0
- package/src/internal/vite-plugin-compat.ts +81 -0
- package/src/internal/web-app.ts +71 -6
- package/src/internal/without-query.ts +8 -0
- package/src/internal/zone-graph.ts +19 -6
- package/src/isr.ts +13 -2
- package/src/manifest.ts +10 -0
- package/src/plugins/import-glob.ts +392 -0
- package/src/plugins/kit.ts +36 -0
- package/src/plugins/stylex.ts +2 -1
- package/src/plugins/vite-leak-guard.ts +99 -7
- package/src/plugins/vite-zone-guard.ts +21 -19
- package/src/plugins/zone-guard.ts +41 -2
- package/src/prerender.ts +39 -18
- package/src/render-seam.ts +88 -2
- package/src/router.ts +16 -11
- package/src/seo.ts +68 -0
- package/src/server-components/typescript.ts +2 -10
- package/src/vite.ts +3 -81
- package/src/zones.ts +237 -52
|
@@ -3,7 +3,12 @@ import {
|
|
|
3
3
|
NIFRA_BACKEND_BIND_PLATFORM,
|
|
4
4
|
NIFRA_PLATFORM_CLIENT_IP_DERIVED,
|
|
5
5
|
} from "@nifrajs/core/mount"
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
isSameOriginForm,
|
|
8
|
+
type Platform,
|
|
9
|
+
type ResponseResult,
|
|
10
|
+
status as statusResult,
|
|
11
|
+
} from "@nifrajs/core/server"
|
|
7
12
|
import {
|
|
8
13
|
type BoundaryRequestCtx,
|
|
9
14
|
type BoundaryStates,
|
|
@@ -29,7 +34,7 @@ import type {
|
|
|
29
34
|
ShouldRevalidate,
|
|
30
35
|
} from "../manifest.ts"
|
|
31
36
|
import type { NonceResolver } from "../nonce.ts"
|
|
32
|
-
import type { MatchChain, RenderAdapter } from "../render-seam.ts"
|
|
37
|
+
import type { MatchChain, PageLocale, PageLocales, RenderAdapter } from "../render-seam.ts"
|
|
33
38
|
import {
|
|
34
39
|
createMatcher,
|
|
35
40
|
DATA_HEADER,
|
|
@@ -49,9 +54,11 @@ import {
|
|
|
49
54
|
isStatusSignal,
|
|
50
55
|
type LoadedLayoutModules,
|
|
51
56
|
layoutErrorId,
|
|
57
|
+
notFound,
|
|
52
58
|
type RenderAssemblyCache,
|
|
53
59
|
type RenderedPage,
|
|
54
60
|
type RevalidateResult,
|
|
61
|
+
redirect,
|
|
55
62
|
renderPageResult,
|
|
56
63
|
STATUS_SIGNAL,
|
|
57
64
|
STATUS_TEXT,
|
|
@@ -167,11 +174,31 @@ function appTarget(location: string, requestUrl: string): string {
|
|
|
167
174
|
} catch {
|
|
168
175
|
return location // the browser fails on it the same way when it loads it
|
|
169
176
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
177
|
+
if (target.origin !== new URL(requestUrl).origin) return location
|
|
178
|
+
// A path starting `//` (from `/.//host`) is a protocol-relative URL to whoever resolves it next.
|
|
179
|
+
return target.pathname.startsWith("//")
|
|
180
|
+
? target.href
|
|
181
|
+
: target.pathname + target.search + target.hash
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The app's locale routing, as page execution reads it. */
|
|
185
|
+
export interface PageI18n {
|
|
186
|
+
readonly locales: PageLocales
|
|
187
|
+
/** The served locale key a `pathname + search` is in. */
|
|
188
|
+
localeOf(path: string): string
|
|
189
|
+
/** The answer for a `[lang]` / `[[lang]]` segment's value (`undefined` when an optional one is
|
|
190
|
+
* absent), as `@nifrajs/i18n/routing`'s `matchSegment` gives it. */
|
|
191
|
+
matchSegment(
|
|
192
|
+
value: string | undefined,
|
|
193
|
+
pathname: string,
|
|
194
|
+
):
|
|
195
|
+
| { readonly kind: "ok" | "not-found" }
|
|
196
|
+
| { readonly kind: "redirect"; readonly location: string }
|
|
173
197
|
}
|
|
174
198
|
|
|
199
|
+
// The route directories whose first segment names the locale.
|
|
200
|
+
const LOCALE_SEGMENTS: ReadonlySet<string> = new Set(["[lang]", "[[lang]]"])
|
|
201
|
+
|
|
175
202
|
export interface PageExecutionOptions<Env = unknown> {
|
|
176
203
|
readonly serverComponents?: import("../server-components/types.ts").ServerComponentRuntime
|
|
177
204
|
readonly rerenderOwnerGET?: (
|
|
@@ -191,6 +218,10 @@ export interface PageExecutionOptions<Env = unknown> {
|
|
|
191
218
|
readonly prerenderedPaths?: readonly string[]
|
|
192
219
|
/** Where pages fetch {@link prerenderedPaths} from when the list is too large to inline. */
|
|
193
220
|
readonly prerenderedListUrl?: string
|
|
221
|
+
/** Locale key -> copy chunk URL, from a build that split copy tables by locale. */
|
|
222
|
+
readonly copyChunks?: Readonly<Record<string, string>>
|
|
223
|
+
/** The app's locale routing: each document's locale, and the check on a `[lang]` segment. */
|
|
224
|
+
readonly i18n?: PageI18n
|
|
194
225
|
readonly staticFallbacks?: Readonly<Record<string, "ssr" | "404">>
|
|
195
226
|
readonly staticBoundaryCache?: StaticBoundaryCache
|
|
196
227
|
/** Per-request CSP nonce for framework-owned executable document scripts. */
|
|
@@ -294,6 +325,23 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
294
325
|
options.prerenderedListUrl === undefined
|
|
295
326
|
? { prerenderedPaths: options.prerenderedPaths ?? [] }
|
|
296
327
|
: { prerenderedListUrl: options.prerenderedListUrl }
|
|
328
|
+
const copyOption = options.copyChunks === undefined ? {} : { copyChunks: options.copyChunks }
|
|
329
|
+
const { i18n } = options
|
|
330
|
+
const pageLocales = new Map(i18n?.locales.locales.map((locale) => [locale.key, locale]))
|
|
331
|
+
/** The request's locale for a document, with the served locales the client names locales from. */
|
|
332
|
+
const localeOption = (req: Request): { locale?: PageLocale; locales?: PageLocales } => {
|
|
333
|
+
if (i18n === undefined) return {}
|
|
334
|
+
const locale = pageLocales.get(i18n.localeOf(pathOf(req)))
|
|
335
|
+
return locale === undefined ? { locales: i18n.locales } : { locale, locales: i18n.locales }
|
|
336
|
+
}
|
|
337
|
+
/** Whether a route sits under a `[lang]` or `[[lang]]` directory, which the locale routing checks. */
|
|
338
|
+
const localeGuarded = (route: RouteEntry): boolean => {
|
|
339
|
+
if (i18n === undefined) return false
|
|
340
|
+
for (const segment of route.id.split("/")) {
|
|
341
|
+
if (!(segment.startsWith("(") && segment.endsWith(")"))) return LOCALE_SEGMENTS.has(segment)
|
|
342
|
+
}
|
|
343
|
+
return false
|
|
344
|
+
}
|
|
297
345
|
const matchManifestRoute = createMatcher(
|
|
298
346
|
manifest.routes.map((route) => ({ routeId: route.id, pattern: route.pattern })),
|
|
299
347
|
)
|
|
@@ -415,6 +463,25 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
415
463
|
return value
|
|
416
464
|
}
|
|
417
465
|
|
|
466
|
+
// Development only. A layout loader that refuses access reads as a guard, and is not one: a client
|
|
467
|
+
// navigation can skip it and an action below it runs without it.
|
|
468
|
+
const warnsUngatedRefusal =
|
|
469
|
+
typeof process !== "undefined" && process.env?.NODE_ENV === "development"
|
|
470
|
+
const warnedUngated = new Set<string>()
|
|
471
|
+
const warnUngatedRefusal = (layoutId: string, outcome: unknown): void => {
|
|
472
|
+
if (warnedUngated.has(layoutId)) return
|
|
473
|
+
// notFound() and gone() say the record is missing, which a layout may decide without guarding.
|
|
474
|
+
const refusesAccess = isStatusSignal(outcome)
|
|
475
|
+
? outcome.status === 401 || outcome.status === 403
|
|
476
|
+
: isControlFlow(outcome)
|
|
477
|
+
if (!refusesAccess) return
|
|
478
|
+
warnedUngated.add(layoutId)
|
|
479
|
+
const file = manifest.layouts?.[layoutId]?.file ?? layoutId
|
|
480
|
+
console.warn(
|
|
481
|
+
`[nifra/web] the loader in "${file}" refused a request, but that layout is not a gate. A client navigation that keeps the layout on screen can skip its loader, and an action below it runs without it, so it does not protect the pages under it. Export \`gate = true\` from that file, or move the check into middleware. (Shown in development only.)`,
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
|
|
418
485
|
/**
|
|
419
486
|
* Run layout loaders, awaiting gates before starting the page loader and retaining only safe data.
|
|
420
487
|
* `gated` carries gates that already ran for this request, so none runs twice.
|
|
@@ -469,15 +536,25 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
469
536
|
if (value !== null && (typeof value === "object" || typeof value === "function")) {
|
|
470
537
|
pending.push(
|
|
471
538
|
(async () => {
|
|
539
|
+
let settled: unknown
|
|
472
540
|
try {
|
|
473
|
-
|
|
541
|
+
settled = await value
|
|
474
542
|
} catch (err) {
|
|
543
|
+
if (warnsUngatedRefusal) warnUngatedRefusal(layoutId, err)
|
|
475
544
|
throw tagLayoutError(err, layoutId)
|
|
476
545
|
}
|
|
546
|
+
// A returned redirect or Response refuses exactly as a thrown one: kept as layout data,
|
|
547
|
+
// it would ship beside the page loader's data.
|
|
548
|
+
if (isControlFlow(settled)) {
|
|
549
|
+
if (warnsUngatedRefusal) warnUngatedRefusal(layoutId, settled)
|
|
550
|
+
throw tagLayoutError(settled, layoutId)
|
|
551
|
+
}
|
|
552
|
+
results[index] = settled
|
|
477
553
|
})(),
|
|
478
554
|
)
|
|
479
555
|
} else results[index] = value
|
|
480
556
|
} catch (err) {
|
|
557
|
+
if (warnsUngatedRefusal) warnUngatedRefusal(layoutId, err)
|
|
481
558
|
// Preserve a synchronous throw as a pending layout failure, just like the async lane.
|
|
482
559
|
pending.push(Promise.reject(tagLayoutError(err, layoutId)))
|
|
483
560
|
}
|
|
@@ -517,6 +594,21 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
517
594
|
ctx: LoaderContext,
|
|
518
595
|
controls: PageResponseControls,
|
|
519
596
|
): Promise<void> => {
|
|
597
|
+
if (i18n !== undefined && localeGuarded(route)) {
|
|
598
|
+
// Before any app middleware, as a root `_middleware` would run: an unserved locale, or a page
|
|
599
|
+
// missing from it, never reaches the app's code.
|
|
600
|
+
const match = i18n.matchSegment(ctx.params.lang, pathOf(ctx.request))
|
|
601
|
+
if (match.kind === "redirect") {
|
|
602
|
+
throw tagLayoutError(redirect(match.location, { status: 308 }), "_layout")
|
|
603
|
+
}
|
|
604
|
+
if (match.kind === "not-found") {
|
|
605
|
+
try {
|
|
606
|
+
notFound()
|
|
607
|
+
} catch (signal) {
|
|
608
|
+
throw tagLayoutError(signal, "_layout")
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
520
612
|
const ids = route.middlewareIds ?? []
|
|
521
613
|
for (let i = 0; i < ids.length; i++) {
|
|
522
614
|
const id = ids[i] as string
|
|
@@ -740,6 +832,7 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
740
832
|
chain: components,
|
|
741
833
|
data,
|
|
742
834
|
...(wrapped === undefined ? {} : { head: wrapped.head }),
|
|
835
|
+
...localeOption(req),
|
|
743
836
|
clientEntry,
|
|
744
837
|
routeId: errorId,
|
|
745
838
|
status: 500,
|
|
@@ -903,6 +996,8 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
903
996
|
// The build keys stylesheets by route, so a page inside the root layout links the whole-app one.
|
|
904
997
|
...(chain === undefined ? stylesOf(routeId) : stylesFrom(options.styles)),
|
|
905
998
|
...prerenderedHandover,
|
|
999
|
+
...copyOption,
|
|
1000
|
+
...localeOption(req),
|
|
906
1001
|
...titleOption,
|
|
907
1002
|
}
|
|
908
1003
|
if (chain === undefined) return renderPageResult(statusPage)
|
|
@@ -1051,6 +1146,8 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
1051
1146
|
...preloadOf(route.id),
|
|
1052
1147
|
...stylesOf(route.id),
|
|
1053
1148
|
...prerenderedHandover,
|
|
1149
|
+
...copyOption,
|
|
1150
|
+
...localeOption(req),
|
|
1054
1151
|
...(responseHeaders === undefined ? {} : { headers: responseHeaders }),
|
|
1055
1152
|
// A response that sets a cookie is one visitor's: it never advertises ISR freshness.
|
|
1056
1153
|
...(mod.revalidate !== undefined && !personalized && options.cacheChannel?.enabled === true
|
|
@@ -1177,6 +1274,7 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
1177
1274
|
matchChain: matchChainOf(page.layoutIds, layoutModules, id, mod),
|
|
1178
1275
|
status: 404,
|
|
1179
1276
|
hydrate: false,
|
|
1277
|
+
...localeOption(req),
|
|
1180
1278
|
...(layoutData !== undefined ? { layoutData } : {}),
|
|
1181
1279
|
...stylesOf(id),
|
|
1182
1280
|
...(headers === undefined ? {} : { headers }),
|
|
@@ -1268,12 +1366,14 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
1268
1366
|
return {
|
|
1269
1367
|
get: (route) => {
|
|
1270
1368
|
const is404Fallback = options.staticFallbacks?.[route.pattern] === "404"
|
|
1271
|
-
|
|
1369
|
+
// One shell per locale: `<html lang>` is in it, and one route serves every locale.
|
|
1370
|
+
let assemblySlots = new Map<string, RenderAssemblyCache>()
|
|
1272
1371
|
let assemblySlotMod: unknown
|
|
1273
1372
|
let assemblySlotLayouts: LoadedLayoutModules | undefined
|
|
1274
1373
|
const assemblyCacheFor = (
|
|
1275
1374
|
mod: RouteModule,
|
|
1276
1375
|
layouts: LoadedLayoutModules,
|
|
1376
|
+
req: Request,
|
|
1277
1377
|
): RenderAssemblyCache | undefined => {
|
|
1278
1378
|
if (typeof mod.meta === "function") return undefined
|
|
1279
1379
|
for (const layout of layouts) if (typeof layout.meta === "function") return undefined
|
|
@@ -1282,11 +1382,17 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
1282
1382
|
assemblySlotLayouts.length === layouts.length &&
|
|
1283
1383
|
assemblySlotLayouts.every((layout, index) => layout === layouts[index])
|
|
1284
1384
|
if (assemblySlotMod !== mod || !layoutsUnchanged) {
|
|
1285
|
-
|
|
1385
|
+
assemblySlots = new Map()
|
|
1286
1386
|
assemblySlotMod = mod
|
|
1287
1387
|
assemblySlotLayouts = layouts
|
|
1288
1388
|
}
|
|
1289
|
-
|
|
1389
|
+
const key = i18n === undefined ? "" : i18n.localeOf(pathOf(req))
|
|
1390
|
+
let slot = assemblySlots.get(key)
|
|
1391
|
+
if (slot === undefined) {
|
|
1392
|
+
slot = {}
|
|
1393
|
+
assemblySlots.set(key, slot)
|
|
1394
|
+
}
|
|
1395
|
+
return slot
|
|
1290
1396
|
}
|
|
1291
1397
|
|
|
1292
1398
|
return withResponseControls(async (c, controls) => {
|
|
@@ -1328,7 +1434,9 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
1328
1434
|
search: loaderSearch(mod.searchSchema, c.req),
|
|
1329
1435
|
set: controls.scope(PAGE_SCOPE),
|
|
1330
1436
|
}
|
|
1331
|
-
if (route.middlewareIds !== undefined
|
|
1437
|
+
if (route.middlewareIds !== undefined || localeGuarded(route)) {
|
|
1438
|
+
await runMiddleware(route, ctx, controls)
|
|
1439
|
+
}
|
|
1332
1440
|
run = await runLayoutChain(
|
|
1333
1441
|
route,
|
|
1334
1442
|
ctx,
|
|
@@ -1474,7 +1582,7 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
1474
1582
|
layoutModules ?? (await loadLayoutModules(route)),
|
|
1475
1583
|
layoutRetained,
|
|
1476
1584
|
boundaryStates,
|
|
1477
|
-
layoutModules === undefined ? undefined : assemblyCacheFor(mod, layoutModules),
|
|
1585
|
+
layoutModules === undefined ? undefined : assemblyCacheFor(mod, layoutModules, c.req),
|
|
1478
1586
|
responseHeaders,
|
|
1479
1587
|
controls.personalized,
|
|
1480
1588
|
componentState,
|
|
@@ -1493,6 +1601,32 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
1493
1601
|
headers: { allow: "GET", "content-type": "text/plain; charset=utf-8" },
|
|
1494
1602
|
})
|
|
1495
1603
|
}
|
|
1604
|
+
const isDataRequest = c.req.headers.get(DATA_HEADER) !== null
|
|
1605
|
+
// Before anything runs: a form posts cross-site without a preflight and carries the visitor's
|
|
1606
|
+
// cookies, so an action is a CSRF target unless its route names the origins it accepts.
|
|
1607
|
+
const origin = c.req.headers.get("origin")
|
|
1608
|
+
const allowedOrigins = mod.allowedOrigins
|
|
1609
|
+
const allowed =
|
|
1610
|
+
allowedOrigins === undefined
|
|
1611
|
+
? origin === null ||
|
|
1612
|
+
isSameOriginForm(origin, c.req.headers.get("sec-fetch-site"), c.req)
|
|
1613
|
+
: typeof allowedOrigins === "function"
|
|
1614
|
+
? allowedOrigins(origin) === true
|
|
1615
|
+
: origin !== null && allowedOrigins.includes(origin)
|
|
1616
|
+
if (!allowed) {
|
|
1617
|
+
return isDataRequest
|
|
1618
|
+
? Response.json(
|
|
1619
|
+
{ ok: false, error: "forbidden_origin" },
|
|
1620
|
+
{ status: 403, headers: { "cache-control": PRIVATE_NO_STORE } },
|
|
1621
|
+
)
|
|
1622
|
+
: new Response("Forbidden", {
|
|
1623
|
+
status: 403,
|
|
1624
|
+
headers: {
|
|
1625
|
+
"content-type": "text/plain; charset=utf-8",
|
|
1626
|
+
"cache-control": PRIVATE_NO_STORE,
|
|
1627
|
+
},
|
|
1628
|
+
})
|
|
1629
|
+
}
|
|
1496
1630
|
const requestApi = apiFor(c)
|
|
1497
1631
|
const actionContext: LoaderContext = {
|
|
1498
1632
|
params: c.params,
|
|
@@ -1504,11 +1638,12 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
1504
1638
|
search: loaderSearch(mod.searchSchema, c.req),
|
|
1505
1639
|
set: controls.scope(ACTION_SCOPE),
|
|
1506
1640
|
}
|
|
1507
|
-
const isDataRequest = c.req.headers.get(DATA_HEADER) !== null
|
|
1508
1641
|
let gated: GatedLayouts
|
|
1509
1642
|
let result: unknown
|
|
1510
1643
|
try {
|
|
1511
|
-
if (route.middlewareIds !== undefined
|
|
1644
|
+
if (route.middlewareIds !== undefined || localeGuarded(route)) {
|
|
1645
|
+
await runMiddleware(route, actionContext, controls)
|
|
1646
|
+
}
|
|
1512
1647
|
gated = await runLayoutGates(route, actionContext, controls)
|
|
1513
1648
|
result = await mod.action(actionContext)
|
|
1514
1649
|
} catch (err) {
|
|
@@ -1697,6 +1832,8 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
1697
1832
|
...preloadOf(route.id),
|
|
1698
1833
|
...stylesOf(route.id),
|
|
1699
1834
|
...prerenderedHandover,
|
|
1835
|
+
...copyOption,
|
|
1836
|
+
...localeOption(c.req),
|
|
1700
1837
|
...(responseHeaders === undefined ? {} : { headers: responseHeaders }),
|
|
1701
1838
|
...(nonce === undefined ? {} : { nonce }),
|
|
1702
1839
|
...cspOption,
|
package/src/internal/parity.ts
CHANGED
|
@@ -586,6 +586,17 @@ const linkedRepoBoundary = async (packageRoot: string): Promise<string> => {
|
|
|
586
586
|
return packageRoot
|
|
587
587
|
}
|
|
588
588
|
|
|
589
|
+
/** Where a real package folder lives when only its files are links: Bun installs a `file:` folder
|
|
590
|
+
* dependency as a directory of per-file symlinks, so its imports resolve beside the linked files. */
|
|
591
|
+
const fileLinkedDir = async (dir: string): Promise<string | undefined> => {
|
|
592
|
+
const manifest = join(dir, "package.json")
|
|
593
|
+
try {
|
|
594
|
+
return (await lstat(manifest)).isSymbolicLink() ? dirname(await realpath(manifest)) : undefined
|
|
595
|
+
} catch {
|
|
596
|
+
return undefined
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
589
600
|
const linkedPackageRoots = async (
|
|
590
601
|
importerRoots: readonly string[],
|
|
591
602
|
manifests: readonly Record<string, unknown>[],
|
|
@@ -605,8 +616,10 @@ const linkedPackageRoots = async (
|
|
|
605
616
|
}
|
|
606
617
|
const candidate = join(importerRoot, "node_modules", ...name.split("/"))
|
|
607
618
|
const link = await lstat(candidate).catch(() => undefined)
|
|
608
|
-
if (link === undefined
|
|
609
|
-
const resolved =
|
|
619
|
+
if (link === undefined) continue
|
|
620
|
+
const resolved = link.isSymbolicLink()
|
|
621
|
+
? await realpath(candidate).catch(() => undefined)
|
|
622
|
+
: await fileLinkedDir(candidate)
|
|
610
623
|
if (resolved === undefined || pathInside(realScanRoot, resolved)) continue
|
|
611
624
|
found.set(resolved, true)
|
|
612
625
|
}
|
|
@@ -32,12 +32,12 @@ const BEFORE_EXPRESSION = new Set([
|
|
|
32
32
|
"instanceof",
|
|
33
33
|
])
|
|
34
34
|
|
|
35
|
-
type Token =
|
|
35
|
+
export type Token =
|
|
36
36
|
| { readonly kind: "id" | "punct" | "string"; readonly value: string }
|
|
37
37
|
| { readonly kind: "other" }
|
|
38
38
|
|
|
39
39
|
/** JavaScript tokens, with strings, template text, comments and regular expressions skipped. */
|
|
40
|
-
function tokenize(code: string): Token[] {
|
|
40
|
+
export function tokenize(code: string): Token[] {
|
|
41
41
|
const tokens: Token[] = []
|
|
42
42
|
// One entry per open `${`: how many `{` are open inside it.
|
|
43
43
|
const templates: number[] = []
|
|
@@ -152,24 +152,37 @@ function envReads(tokens: readonly Token[]): EnvRead[] {
|
|
|
152
152
|
const token = tokens[at]
|
|
153
153
|
return token?.kind === "id" ? token.value : undefined
|
|
154
154
|
}
|
|
155
|
-
|
|
155
|
+
// The tokenizer reads `?.` as `.`, so `?.[` and `?.(` arrive as `.` before the bracket.
|
|
156
|
+
const open = (at: number, bracket: "[" | "("): number =>
|
|
157
|
+
is(at, "punct", ".") && is(at + 1, "punct", bracket) ? at + 1 : at
|
|
158
|
+
/** The string key of `["X"]` (or `?.["X"]`) at `at`, and the index after it. */
|
|
159
|
+
const keyAt = (at: number): { readonly key: string; readonly next: number } | undefined => {
|
|
160
|
+
const bracket = open(at, "[")
|
|
161
|
+
const key =
|
|
162
|
+
is(bracket, "punct", "[") && is(bracket + 2, "punct", "]") ? stringAt(bracket + 1) : undefined
|
|
163
|
+
return key === undefined ? undefined : { key, next: bracket + 3 }
|
|
164
|
+
}
|
|
165
|
+
/** A property read after an env object that ends at `at`: `.X`, `["X"]` or `?.["X"]`. */
|
|
156
166
|
const member = (base: string, at: number, importMeta: boolean): EnvRead => {
|
|
157
167
|
const property = is(at, "punct", ".") ? idAt(at + 1) : undefined
|
|
158
168
|
if (property !== undefined)
|
|
159
169
|
return { expression: `${base}.${property}`, name: property, importMeta }
|
|
160
|
-
const key =
|
|
170
|
+
const key = keyAt(at)?.key
|
|
161
171
|
if (key !== undefined)
|
|
162
172
|
return { expression: `${base}[${JSON.stringify(key)}]`, name: key, importMeta }
|
|
163
173
|
return { expression: base, importMeta }
|
|
164
174
|
}
|
|
175
|
+
/** The index after `.env`, `["env"]` or `?.["env"]` at `at`, or `-1`. */
|
|
176
|
+
const envAt = (at: number): number => {
|
|
177
|
+
if (is(at, "punct", ".") && is(at + 1, "id", "env")) return at + 2
|
|
178
|
+
const bracket = keyAt(at)
|
|
179
|
+
return bracket?.key === "env" ? bracket.next : -1
|
|
180
|
+
}
|
|
165
181
|
for (let i = 0; i < tokens.length; i++) {
|
|
166
182
|
const head = idAt(i)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
is(i + 2, "id", "env")
|
|
171
|
-
) {
|
|
172
|
-
reads.push(member(`${head}.env`, i + 3, false))
|
|
183
|
+
const env = head === "process" || head === "Bun" ? envAt(i + 1) : -1
|
|
184
|
+
if (env !== -1) {
|
|
185
|
+
reads.push(member(`${head}.env`, env, false))
|
|
173
186
|
} else if (
|
|
174
187
|
head === "import" &&
|
|
175
188
|
is(i + 1, "punct", ".") &&
|
|
@@ -180,12 +193,13 @@ function envReads(tokens: readonly Token[]): EnvRead[] {
|
|
|
180
193
|
reads.push(member("import.meta.env", i + 5, true))
|
|
181
194
|
} else if (head === "Deno" && is(i + 1, "punct", ".") && is(i + 2, "id", "env")) {
|
|
182
195
|
const method = is(i + 3, "punct", ".") ? idAt(i + 4) : undefined
|
|
196
|
+
const call = open(i + 5, "(")
|
|
183
197
|
const name =
|
|
184
|
-
(method === "get" || method === "has") && is(
|
|
185
|
-
? stringAt(
|
|
198
|
+
(method === "get" || method === "has") && is(call, "punct", "(")
|
|
199
|
+
? stringAt(call + 1)
|
|
186
200
|
: undefined
|
|
187
201
|
reads.push(
|
|
188
|
-
name !== undefined && is(
|
|
202
|
+
name !== undefined && is(call + 2, "punct", ")")
|
|
189
203
|
? { expression: `Deno.env.${method}(${JSON.stringify(name)})`, name, importMeta: false }
|
|
190
204
|
: {
|
|
191
205
|
expression: method === undefined ? "Deno.env" : `Deno.env.${method}()`,
|