@nifrajs/web 2.9.1 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +12 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +16 -0
- package/dist/client.js.map +1 -1
- package/dist/dev-ssr-graph.d.ts +33 -0
- package/dist/dev-ssr-graph.d.ts.map +1 -0
- package/dist/dev-ssr-graph.js +285 -0
- package/dist/dev-ssr-graph.js.map +1 -0
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +76 -18
- package/dist/dev.js.map +1 -1
- package/dist/index.d.ts +28 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +53 -79
- package/dist/index.js.map +1 -1
- package/dist/internal/head-merge.d.ts +27 -0
- package/dist/internal/head-merge.d.ts.map +1 -0
- package/dist/internal/head-merge.js +65 -0
- package/dist/internal/head-merge.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +1 -2
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +1 -2
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/isr.d.ts +32 -3
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +181 -16
- package/dist/isr.js.map +1 -1
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js.map +1 -1
- package/dist/plugins/css-modules.d.ts +12 -0
- package/dist/plugins/css-modules.d.ts.map +1 -1
- package/dist/plugins/css-modules.js +0 -0
- package/dist/plugins/css-modules.js.map +1 -1
- package/dist/plugins/kit.d.ts +54 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +66 -0
- package/dist/plugins/kit.js.map +1 -1
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +71 -4
- package/dist/public-dir.js.map +1 -1
- package/dist/route-manifest.d.ts +3 -1
- package/dist/route-manifest.d.ts.map +1 -1
- package/dist/route-manifest.js +3 -0
- package/dist/route-manifest.js.map +1 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +27 -1
- package/dist/vite.js.map +1 -1
- package/package.json +3 -3
- package/src/client.ts +28 -0
- package/src/dev-ssr-graph.ts +311 -0
- package/src/dev.ts +81 -20
- package/src/index.ts +63 -71
- package/src/internal/head-merge.ts +80 -0
- package/src/internal/server-boundary.ts +1 -2
- package/src/isr.ts +216 -19
- package/src/manifest.ts +2 -0
- package/src/plugins/css-modules.ts +0 -0
- package/src/plugins/kit.ts +71 -0
- package/src/public-dir.ts +74 -4
- package/src/route-manifest.ts +10 -1
- package/src/vite.ts +29 -1
package/src/index.ts
CHANGED
|
@@ -25,9 +25,13 @@ import {
|
|
|
25
25
|
} from "./deferred.ts"
|
|
26
26
|
import { isDraftEnabled } from "./draft.ts"
|
|
27
27
|
import { trustedHeadAttributes } from "./internal/head-attributes.ts"
|
|
28
|
+
// Head resolution/merging. Defined in an internal module so `@nifrajs/web/client` can export the same
|
|
29
|
+
// two functions to the generated client entry without dragging THIS module (and its server code) into
|
|
30
|
+
// the browser graph - see internal/head-merge.ts. Re-exported here so the public API is unchanged.
|
|
31
|
+
import { mergeHeads, resolveMeta } from "./internal/head-merge.ts"
|
|
28
32
|
import { jsStringLiteral } from "./internal/js-string.ts"
|
|
29
33
|
import { EXECUTABLE_SCRIPT_TYPES, INERT_SCRIPT_TYPES } from "./internal/script-types.ts"
|
|
30
|
-
import { ISR_REVALIDATE_HEADER } from "./isr.ts"
|
|
34
|
+
import { ISR_REVALIDATE_HEADER, ISR_REVALIDATE_TAGS_HEADER, serializeISRTags } from "./isr.ts"
|
|
31
35
|
import { generateLlmsTxt } from "./llms-txt.ts"
|
|
32
36
|
import type {
|
|
33
37
|
LayoutEntry,
|
|
@@ -83,6 +87,7 @@ export {
|
|
|
83
87
|
type CachedResponse,
|
|
84
88
|
type CacheStore,
|
|
85
89
|
ISR_REVALIDATE_HEADER,
|
|
90
|
+
ISR_REVALIDATE_TAGS_HEADER,
|
|
86
91
|
ISR_STATUS_HEADER,
|
|
87
92
|
type ISRApp,
|
|
88
93
|
type ISROptions,
|
|
@@ -194,7 +199,7 @@ export {
|
|
|
194
199
|
type SubmitOptions,
|
|
195
200
|
} from "./router.ts"
|
|
196
201
|
// Deferred loader data (`defer()` + the `Deferred<T>` type) - consumed by the adapter's `<Await>`.
|
|
197
|
-
export { type Deferred, defer }
|
|
202
|
+
export { type Deferred, defer, mergeHeads, resolveMeta }
|
|
198
203
|
|
|
199
204
|
/** Phantom brand key for {@link ServerOnly}. A unique symbol so the brand can't be forged by a plain
|
|
200
205
|
* object literal; `unique symbol` keys never appear in the emitted JS, so the brand is purely
|
|
@@ -310,6 +315,43 @@ export interface RenderAdapter {
|
|
|
310
315
|
hydrationHead(nonce?: string): string
|
|
311
316
|
}
|
|
312
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Loads a module through the DEV SERVER'S module graph rather than the runtime's. Takes an absolute
|
|
320
|
+
* file path or a bare specifier; resolution, compilation and caching are the dev server's.
|
|
321
|
+
*/
|
|
322
|
+
export type SsrModuleLoader = (id: string) => Promise<unknown>
|
|
323
|
+
|
|
324
|
+
/* A `globalThis` singleton for the same reason `@nifrajs/web-react`'s router context is one: in dev
|
|
325
|
+
* this module is evaluated twice in one process (the CLI's copy under Bun, and the app's copy), and a
|
|
326
|
+
* module-level `let` would leave the adapter reading a slot the dev server set on the other copy. */
|
|
327
|
+
const SSR_MODULE_LOADER_SLOT = Symbol.for("nifra.web.ssr-module-loader")
|
|
328
|
+
/* Explicitly `| undefined`: clearing the slot is part of the contract (a dev server clears it on
|
|
329
|
+
* stop), and under `exactOptionalPropertyTypes` "absent" and "set to undefined" are separate types. */
|
|
330
|
+
const loaderSlot = globalThis as { [SSR_MODULE_LOADER_SLOT]?: SsrModuleLoader | undefined }
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Publishes the dev server's SSR module loader. Called by a dev server that owns SSR resolution
|
|
334
|
+
* itself; pass `undefined` to clear it on shutdown.
|
|
335
|
+
*/
|
|
336
|
+
export function setSsrModuleLoader(load: SsrModuleLoader | undefined): void {
|
|
337
|
+
loaderSlot[SSR_MODULE_LOADER_SLOT] = load
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The dev server's SSR module loader, or `undefined` when nothing owns SSR resolution but the runtime
|
|
342
|
+
* - the Bun dev pipeline, and every production build, where the adapter's assets are compiled ahead
|
|
343
|
+
* of time and a plain `import` is already the right thing.
|
|
344
|
+
*
|
|
345
|
+
* An adapter that must load a COMPILED asset at runtime (a `.svelte` component, say) has to prefer
|
|
346
|
+
* this over a bare `import`, and has to take the framework's SERVER RENDERER from it as well, not
|
|
347
|
+
* just the component. A component compiled by the dev server renders through the renderer the dev
|
|
348
|
+
* server resolved; mixing the two puts two copies of the framework runtime in one component tree,
|
|
349
|
+
* where context written by one half is invisible to the other.
|
|
350
|
+
*/
|
|
351
|
+
export function ssrModuleLoader(): SsrModuleLoader | undefined {
|
|
352
|
+
return loaderSlot[SSR_MODULE_LOADER_SLOT]
|
|
353
|
+
}
|
|
354
|
+
|
|
313
355
|
/** Global the server serializes loader data into; the client reads it to hydrate. */
|
|
314
356
|
export const DATA_GLOBAL = "__NIFRA_DATA__"
|
|
315
357
|
/** Per-layout loader data for hydration. Emitted ONLY when some layout in the chain has a loader, so
|
|
@@ -427,6 +469,8 @@ export interface RenderPageOptions {
|
|
|
427
469
|
/** ISR: route freshness in seconds, emitted as the `x-nifra-isr-revalidate` header for a `withISR`
|
|
428
470
|
* wrapper to read. Omit ⇒ no header (the wrapper's default TTL applies). */
|
|
429
471
|
readonly revalidate?: number
|
|
472
|
+
/** ISR invalidation tags emitted as a bounded `x-nifra-isr-tags` header. */
|
|
473
|
+
readonly revalidateTags?: readonly string[]
|
|
430
474
|
/** Matched route id; written to `window.__NIFRA_ROUTE__` so the client hydrates this chain. */
|
|
431
475
|
readonly routeId?: string
|
|
432
476
|
/** The matched route's decoded path params - surfaced to the page as `params` (via {@link RenderProps})
|
|
@@ -541,6 +585,7 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
541
585
|
styles = [],
|
|
542
586
|
prerenderedPaths = [],
|
|
543
587
|
revalidate,
|
|
588
|
+
revalidateTags = [],
|
|
544
589
|
routeId,
|
|
545
590
|
status = 200,
|
|
546
591
|
title = "nifra",
|
|
@@ -695,6 +740,8 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
695
740
|
// dedicated header (not the action-revalidation `x-nifra-revalidate`) so the TTL channel never aliases
|
|
696
741
|
// the client's path-list channel.
|
|
697
742
|
if (revalidate !== undefined) headers[ISR_REVALIDATE_HEADER] = String(revalidate)
|
|
743
|
+
const serializedTags = serializeISRTags(revalidateTags)
|
|
744
|
+
if (serializedTags !== undefined) headers[ISR_REVALIDATE_TAGS_HEADER] = serializedTags
|
|
698
745
|
const renderProps: RenderProps = {
|
|
699
746
|
data: forComponent,
|
|
700
747
|
actionData: actionSplit?.forComponent,
|
|
@@ -1281,12 +1328,6 @@ function headTags(head: Meta | undefined): string {
|
|
|
1281
1328
|
return out
|
|
1282
1329
|
}
|
|
1283
1330
|
|
|
1284
|
-
/** Resolve a route's `meta` (static or a function of the loader data + params) to a {@link Meta}. */
|
|
1285
|
-
export function resolveMeta(meta: MetaInput | undefined, args: MetaArgs): Meta {
|
|
1286
|
-
if (meta === undefined) return {}
|
|
1287
|
-
return typeof meta === "function" ? meta(args) : meta
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
1331
|
/**
|
|
1291
1332
|
* A `<link rel="canonical">` descriptor for a route's `meta.link`. The canonical URL tells search
|
|
1292
1333
|
* engines which URL is authoritative for a page (deduping query-string / tracking variants).
|
|
@@ -1373,68 +1414,6 @@ export function unsafeInlineScript(
|
|
|
1373
1414
|
}
|
|
1374
1415
|
}
|
|
1375
1416
|
|
|
1376
|
-
/**
|
|
1377
|
-
* Merge a route's `<head>` contributions from its layout chain + the page into one {@link Meta}.
|
|
1378
|
-
*
|
|
1379
|
-
* The head contract (see {@link CreateWebAppOptions} and `LayoutEntry`): a `_layout.tsx` may export
|
|
1380
|
-
* `meta` to put sitewide tags (`hreflang`, `preconnect`, a default `<title>`, …) on every page below
|
|
1381
|
-
* it. `heads` is passed **outermost layout → … → innermost layout → page** (the same order as the
|
|
1382
|
-
* render chain), and merges:
|
|
1383
|
-
* - **`title`** - *nearest-wins*: the last defined value across the list, so the page overrides an
|
|
1384
|
-
* inner layout, which overrides an outer one. A layout `title` is the section default; an undefined
|
|
1385
|
-
* page `title` keeps it.
|
|
1386
|
-
* - **`meta` / `link`** - *concatenated* in list order (outermost first, page last). Duplicate-tag
|
|
1387
|
-
* de-duplication is the caller's concern; the framework emits exactly what's declared so a layout
|
|
1388
|
-
* can ship N `<link rel="alternate" hreflang>` tags and a page can add its own canonical.
|
|
1389
|
-
*
|
|
1390
|
-
* Returns a fresh object whose identity is stable per `heads` *content* only when every entry is a
|
|
1391
|
-
* static (by-reference) `Meta` and there is exactly one - otherwise a new object each call. That is
|
|
1392
|
-
* fine: {@link headTags}'s memo is keyed on identity, so a per-request merge simply recomputes (its
|
|
1393
|
-
* content can vary with loader data anyway).
|
|
1394
|
-
*/
|
|
1395
|
-
export function mergeHeads(heads: readonly Meta[]): Meta {
|
|
1396
|
-
// Single-head fast path (a route with no layout `meta`, by far the common case) - return the
|
|
1397
|
-
// resolved object by reference so headTags' identity-keyed memo hits across requests for static meta.
|
|
1398
|
-
if (heads.length === 1) return heads[0] as Meta
|
|
1399
|
-
let title: string | undefined
|
|
1400
|
-
let lang: string | undefined
|
|
1401
|
-
let dir: Meta["dir"]
|
|
1402
|
-
const meta: MetaDescriptor[] = []
|
|
1403
|
-
const link: LinkDescriptor[] = []
|
|
1404
|
-
const script: ScriptDescriptor[] = []
|
|
1405
|
-
const unsafeScript: UnsafeScriptDescriptor[] = []
|
|
1406
|
-
for (const h of heads) {
|
|
1407
|
-
if (h.title !== undefined) title = h.title // nearest-wins: later (more specific) overrides
|
|
1408
|
-
if (h.lang !== undefined) lang = h.lang // nearest-wins, like title
|
|
1409
|
-
if (h.dir !== undefined) dir = h.dir
|
|
1410
|
-
if (h.meta !== undefined) meta.push(...h.meta)
|
|
1411
|
-
if (h.link !== undefined) link.push(...h.link)
|
|
1412
|
-
if (h.script !== undefined) script.push(...h.script) // concatenated like meta/link (outermost first)
|
|
1413
|
-
if (h.unsafeScript !== undefined) unsafeScript.push(...h.unsafeScript)
|
|
1414
|
-
}
|
|
1415
|
-
// Build the result with only the fields that were actually contributed - an empty `meta`/`link`/
|
|
1416
|
-
// `script` array would otherwise be a spurious (if harmless) key. A mutable local; the cast to `Meta`
|
|
1417
|
-
// is sound because a key is assigned only when defined (so `exactOptionalPropertyTypes` never sees
|
|
1418
|
-
// `undefined`).
|
|
1419
|
-
const merged: {
|
|
1420
|
-
title?: string
|
|
1421
|
-
meta?: Meta["meta"]
|
|
1422
|
-
link?: Meta["link"]
|
|
1423
|
-
script?: Meta["script"]
|
|
1424
|
-
unsafeScript?: Meta["unsafeScript"]
|
|
1425
|
-
lang?: string
|
|
1426
|
-
dir?: Meta["dir"]
|
|
1427
|
-
} = {}
|
|
1428
|
-
if (title !== undefined) merged.title = title
|
|
1429
|
-
if (meta.length > 0) merged.meta = meta
|
|
1430
|
-
if (link.length > 0) merged.link = link
|
|
1431
|
-
if (script.length > 0) merged.script = script
|
|
1432
|
-
if (unsafeScript.length > 0) merged.unsafeScript = unsafeScript
|
|
1433
|
-
if (lang !== undefined) merged.lang = lang
|
|
1434
|
-
if (dir !== undefined) merged.dir = dir
|
|
1435
|
-
return merged as Meta
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
1417
|
export interface CreateWebAppOptions {
|
|
1439
1418
|
readonly adapter: RenderAdapter
|
|
1440
1419
|
readonly manifest: Manifest
|
|
@@ -2225,6 +2204,7 @@ export function createWebApp<Env = unknown>(
|
|
|
2225
2204
|
...stylesOf(route.id),
|
|
2226
2205
|
prerenderedPaths: options.prerenderedPaths ?? [],
|
|
2227
2206
|
...(mod.revalidate !== undefined ? { revalidate: mod.revalidate } : {}),
|
|
2207
|
+
...(mod.revalidateTags !== undefined ? { revalidateTags: mod.revalidateTags } : {}),
|
|
2228
2208
|
...(mod.islandScripts !== undefined ? { islandScripts: mod.islandScripts } : {}),
|
|
2229
2209
|
...titleOption,
|
|
2230
2210
|
...assemblyCacheFor(mod, chainLayoutModules),
|
|
@@ -2440,7 +2420,9 @@ export function generateClientEntry(
|
|
|
2440
2420
|
}
|
|
2441
2421
|
|
|
2442
2422
|
return `${[
|
|
2443
|
-
|
|
2423
|
+
// `/client`, never the root: the root's graph carries the server (renderPage, the static-file
|
|
2424
|
+
// server), and Vite's dev server evaluates what it is given instead of tree-shaking it.
|
|
2425
|
+
'import { createClientRouter, createMatcher, mergeHeads, resolveMeta } from "@nifrajs/web/client"',
|
|
2444
2426
|
'import { applyHead, installForms, installHistory, signalHydrated } from "@nifrajs/web/client"',
|
|
2445
2427
|
// Namespace import: `errorBoundary` is optional (an adapter may not export it). A namespace member
|
|
2446
2428
|
// access yields `undefined` if absent - unlike a named import, which would be a link error.
|
|
@@ -2457,6 +2439,16 @@ export function generateClientEntry(
|
|
|
2457
2439
|
'`/client` entry (e.g. "@nifrajs/web-react/client"), or re-export mountRouter from it.',
|
|
2458
2440
|
)})`,
|
|
2459
2441
|
"const errorBoundary = __adapter.errorBoundary",
|
|
2442
|
+
// A hot update that the framework cannot actually apply must end in a reload, not in nothing.
|
|
2443
|
+
//
|
|
2444
|
+
// Bun's dev server applies React Fast Refresh to JSX modules. For React that patches the component
|
|
2445
|
+
// and preserves state. For Preact it does not - the module still LOOKS Fast-Refresh-able, so the
|
|
2446
|
+
// update is accepted and then swallowed: `bun:beforeUpdate`/`bun:afterUpdate` both fire, the server
|
|
2447
|
+
// logs a rebuild, and the page silently keeps rendering the old code. An edit that appears to do
|
|
2448
|
+
// nothing is the worst failure mode in a dev loop - worse than a reload, because there is no signal
|
|
2449
|
+
// to act on. An adapter that knows its framework is in that position says so, and the entry turns
|
|
2450
|
+
// the swallowed update into an honest full reload.
|
|
2451
|
+
'if (import.meta.hot && __adapter.hotUpdateNeedsReload) import.meta.hot.on("bun:afterUpdate", () => location.reload())',
|
|
2460
2452
|
`const errorRouteIds = new Set(${JSON.stringify(errorRouteIds)})`,
|
|
2461
2453
|
// Each route is a lazy loader: dynamic imports → Bun.build (splitting) emits one chunk per
|
|
2462
2454
|
// route, shared layouts/deps deduped into shared chunks, so a route's code loads only when
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Head resolution + merging, in its own module so the CLIENT can have it without the server.
|
|
3
|
+
*
|
|
4
|
+
* These two are pure functions over `Meta`, but they used to live in `index.ts` - the module that also
|
|
5
|
+
* carries `renderPage`, `createWebApp` and the static-file server. The generated client entry imported
|
|
6
|
+
* them from `@nifrajs/web`, which under a bundler was harmless (server code tree-shakes away) and under
|
|
7
|
+
* Vite's dev server was fatal: it serves each module as-is, so pulling `index.ts` into the browser graph
|
|
8
|
+
* executed `public-dir.ts` and the page died on `Module "node:fs/promises" has been externalized`
|
|
9
|
+
* before hydrating. Splitting them out means the client entry never names the server module at all,
|
|
10
|
+
* rather than relying on a bundler to remove it afterwards.
|
|
11
|
+
*
|
|
12
|
+
* `index.ts` and `client.ts` both re-export from here, so the public API is unchanged in both places.
|
|
13
|
+
*/
|
|
14
|
+
import type {
|
|
15
|
+
LinkDescriptor,
|
|
16
|
+
Meta,
|
|
17
|
+
MetaArgs,
|
|
18
|
+
MetaDescriptor,
|
|
19
|
+
MetaInput,
|
|
20
|
+
ScriptDescriptor,
|
|
21
|
+
UnsafeScriptDescriptor,
|
|
22
|
+
} from "../manifest.ts"
|
|
23
|
+
|
|
24
|
+
/** Resolve a route's `meta` export - a static object, or a function of the route's data/params. */
|
|
25
|
+
export function resolveMeta(meta: MetaInput | undefined, args: MetaArgs): Meta {
|
|
26
|
+
if (meta === undefined) return {}
|
|
27
|
+
return typeof meta === "function" ? meta(args) : meta
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Merge a layout chain's heads, outermost first: `title`/`lang`/`dir` are nearest-wins, and
|
|
32
|
+
* `meta`/`link`/`script` concatenate in chain order.
|
|
33
|
+
*
|
|
34
|
+
* Returns a fresh object whose identity is stable per `heads` *content* only when every entry is a
|
|
35
|
+
* static (by-reference) `Meta` and there is exactly one - otherwise a new object each call. That is
|
|
36
|
+
* fine: `headTags`'s memo is keyed on identity, so a per-request merge simply recomputes (its
|
|
37
|
+
* content can vary with loader data anyway).
|
|
38
|
+
*/
|
|
39
|
+
export function mergeHeads(heads: readonly Meta[]): Meta {
|
|
40
|
+
// Single-head fast path (a route with no layout `meta`, by far the common case) - return the
|
|
41
|
+
// resolved object by reference so headTags' identity-keyed memo hits across requests for static meta.
|
|
42
|
+
if (heads.length === 1) return heads[0] as Meta
|
|
43
|
+
let title: string | undefined
|
|
44
|
+
let lang: string | undefined
|
|
45
|
+
let dir: Meta["dir"]
|
|
46
|
+
const meta: MetaDescriptor[] = []
|
|
47
|
+
const link: LinkDescriptor[] = []
|
|
48
|
+
const script: ScriptDescriptor[] = []
|
|
49
|
+
const unsafeScript: UnsafeScriptDescriptor[] = []
|
|
50
|
+
for (const h of heads) {
|
|
51
|
+
if (h.title !== undefined) title = h.title // nearest-wins: later (more specific) overrides
|
|
52
|
+
if (h.lang !== undefined) lang = h.lang // nearest-wins, like title
|
|
53
|
+
if (h.dir !== undefined) dir = h.dir
|
|
54
|
+
if (h.meta !== undefined) meta.push(...h.meta)
|
|
55
|
+
if (h.link !== undefined) link.push(...h.link)
|
|
56
|
+
if (h.script !== undefined) script.push(...h.script) // concatenated like meta/link (outermost first)
|
|
57
|
+
if (h.unsafeScript !== undefined) unsafeScript.push(...h.unsafeScript)
|
|
58
|
+
}
|
|
59
|
+
// Build the result with only the fields that were actually contributed - an empty `meta`/`link`/
|
|
60
|
+
// `script` array would otherwise be a spurious (if harmless) key. A mutable local; the cast to `Meta`
|
|
61
|
+
// is sound because a key is assigned only when defined (so `exactOptionalPropertyTypes` never sees
|
|
62
|
+
// `undefined`).
|
|
63
|
+
const merged: {
|
|
64
|
+
title?: string
|
|
65
|
+
meta?: Meta["meta"]
|
|
66
|
+
link?: Meta["link"]
|
|
67
|
+
script?: Meta["script"]
|
|
68
|
+
unsafeScript?: Meta["unsafeScript"]
|
|
69
|
+
lang?: string
|
|
70
|
+
dir?: Meta["dir"]
|
|
71
|
+
} = {}
|
|
72
|
+
if (title !== undefined) merged.title = title
|
|
73
|
+
if (meta.length > 0) merged.meta = meta
|
|
74
|
+
if (link.length > 0) merged.link = link
|
|
75
|
+
if (script.length > 0) merged.script = script
|
|
76
|
+
if (unsafeScript.length > 0) merged.unsafeScript = unsafeScript
|
|
77
|
+
if (lang !== undefined) merged.lang = lang
|
|
78
|
+
if (dir !== undefined) merged.dir = dir
|
|
79
|
+
return merged as Meta
|
|
80
|
+
}
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The client build must never ship server-only code, or server-only config, to a browser. Three
|
|
5
5
|
* conventions enforce that, and each has to mean the SAME thing in every pipeline that builds the client
|
|
6
|
-
* - the Bun client build, the Vite dev server, the Vite production build, and `nifra dev --bun
|
|
7
|
-
* (which cannot transform, so it refuses to start instead):
|
|
6
|
+
* - the Bun client build, the Vite dev server, the Vite production build, and `nifra dev --bun`:
|
|
8
7
|
*
|
|
9
8
|
* - `*.server` modules - emptied, so a secret / `node:` / native import subtree never reaches the client.
|
|
10
9
|
* - `*.fn` modules - replaced with HTTP stubs, so the function bodies (and their imports) stay server-side.
|