@nifrajs/web 2.10.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +10 -0
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +7 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +5 -2
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +12 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +28 -7
- package/dist/client.js.map +1 -1
- package/dist/conformance.d.ts +1 -1
- package/dist/conformance.d.ts.map +1 -1
- package/dist/deferred.d.ts +1 -1
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +14 -3
- package/dist/deferred.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 +88 -17
- package/dist/dev.js.map +1 -1
- package/dist/fonts-google.d.ts +2 -2
- package/dist/fonts-google.d.ts.map +1 -1
- package/dist/fonts-google.js +12 -3
- package/dist/fonts-google.js.map +1 -1
- package/dist/index.d.ts +68 -119
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +78 -114
- 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.map +1 -1
- package/dist/isr.js +4 -1
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts +3 -1
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +23 -20
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +5 -3
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js.map +1 -1
- package/dist/navigation.d.ts +9 -0
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +8 -1
- package/dist/navigation.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/render-seam.d.ts +61 -0
- package/dist/render-seam.d.ts.map +1 -0
- package/dist/render-seam.js +22 -0
- package/dist/render-seam.js.map +1 -0
- 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/build-vite.ts +11 -0
- package/src/build.ts +18 -1
- package/src/client.ts +41 -7
- package/src/conformance.ts +1 -1
- package/src/deferred.ts +14 -2
- package/src/dev-ssr-graph.ts +311 -0
- package/src/dev.ts +91 -19
- package/src/fonts-google.ts +14 -5
- package/src/index.ts +155 -189
- package/src/internal/head-merge.ts +80 -0
- package/src/internal/server-boundary.ts +1 -2
- package/src/isr.ts +4 -1
- package/src/llms-txt.ts +24 -20
- package/src/manifest.ts +5 -3
- package/src/navigation.ts +14 -1
- package/src/plugins/css-modules.ts +0 -0
- package/src/plugins/kit.ts +71 -0
- package/src/render-seam.ts +76 -0
- package/src/vite.ts +29 -1
package/src/manifest.ts
CHANGED
|
@@ -222,9 +222,11 @@ export interface RouteModule {
|
|
|
222
222
|
*/
|
|
223
223
|
readonly getStaticPaths?: GetStaticPaths
|
|
224
224
|
/**
|
|
225
|
-
* ISR freshness for this route, in **seconds**. `createWebApp` emits it as an
|
|
226
|
-
* response header that `withISR` reads to set the page's cache TTL
|
|
227
|
-
* default). Older-than-`revalidate` cached pages are served stale while
|
|
225
|
+
* ISR freshness for this route, in **seconds**. `createWebApp` emits it as an
|
|
226
|
+
* `x-nifra-isr-revalidate` response header that `withISR` reads to set the page's cache TTL
|
|
227
|
+
* (overriding the wrapper's default). Older-than-`revalidate` cached pages are served stale while
|
|
228
|
+
* regenerating. (Distinct from the action-revalidation `x-nifra-revalidate` header - a CSV path
|
|
229
|
+
* list the client parses to refetch - so the two channels never alias.)
|
|
228
230
|
*/
|
|
229
231
|
readonly revalidate?: number
|
|
230
232
|
/** Optional bounded tags used by ISR on-demand invalidation (`?tag=...`). */
|
package/src/navigation.ts
CHANGED
|
@@ -18,6 +18,13 @@ import { serializeSearch } from "./search.ts"
|
|
|
18
18
|
export interface NavigateOptions {
|
|
19
19
|
/** Replace the current history entry instead of pushing a new one (like `history.replaceState`). */
|
|
20
20
|
readonly replace?: boolean
|
|
21
|
+
/**
|
|
22
|
+
* Opaque per-entry state stored on the created history entry (under `history.state.nifraState`, so
|
|
23
|
+
* it never collides with the router's own bookkeeping keys). Structured-cloneable only, like
|
|
24
|
+
* `history.pushState`'s first argument. Read it back via `history.state.nifraState`; the browser
|
|
25
|
+
* restores it automatically on back/forward. Ignored for history-delta navigations.
|
|
26
|
+
*/
|
|
27
|
+
readonly state?: unknown
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
/**
|
|
@@ -56,6 +63,7 @@ export interface NavigateTargetInput {
|
|
|
56
63
|
readonly to: string
|
|
57
64
|
readonly search?: Record<string, unknown>
|
|
58
65
|
readonly replace?: boolean
|
|
66
|
+
readonly state?: unknown
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
/**
|
|
@@ -71,6 +79,7 @@ export interface NavigateFunction {
|
|
|
71
79
|
readonly to: To
|
|
72
80
|
readonly search?: NavigateSearchOf<To>
|
|
73
81
|
readonly replace?: boolean
|
|
82
|
+
readonly state?: unknown
|
|
74
83
|
}): void
|
|
75
84
|
}
|
|
76
85
|
|
|
@@ -86,7 +95,11 @@ export function resolveNavigate(
|
|
|
86
95
|
): { readonly to: string | number; readonly options: NavigateOptions | undefined } {
|
|
87
96
|
if (typeof to !== "object") return { to, options }
|
|
88
97
|
const query = to.search !== undefined ? serializeSearch(to.search) : ""
|
|
89
|
-
|
|
98
|
+
if (to.replace !== true && to.state === undefined) return { to: to.to + query, options }
|
|
99
|
+
const resolved: { replace?: boolean; state?: unknown } = { ...options }
|
|
100
|
+
if (to.replace === true) resolved.replace = true
|
|
101
|
+
if (to.state !== undefined) resolved.state = to.state
|
|
102
|
+
return { to: to.to + query, options: resolved }
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
/**
|
|
Binary file
|
package/src/plugins/kit.ts
CHANGED
|
@@ -11,6 +11,16 @@ import type { BunPlugin } from "bun"
|
|
|
11
11
|
/** The argument Bun passes to a plugin's `setup` - Bun doesn't export the type, so derive it. */
|
|
12
12
|
export type PluginBuilder = Parameters<BunPlugin["setup"]>[0]
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Re-keys the app-owned imports of a module the SERVER half of a plugin just compiled, so an edit below
|
|
16
|
+
* a route reaches SSR instead of being served from Bun's import cache. A plugin that claims a file
|
|
17
|
+
* extension is the only code that ever sees that file's source, so it is the only place its imports can
|
|
18
|
+
* be versioned - every `generate: "ssr"` loader should return its output through this.
|
|
19
|
+
*
|
|
20
|
+
* A no-op on the client half and outside a dev server. See `../dev-ssr-graph.ts` for the whole design.
|
|
21
|
+
*/
|
|
22
|
+
export { rewriteSsrImports } from "../dev-ssr-graph.ts"
|
|
23
|
+
|
|
14
24
|
const packageRootCache = new Map<string, string>()
|
|
15
25
|
|
|
16
26
|
/** The nearest ancestor directory of `startDir` that holds a `package.json` (the file's package root),
|
|
@@ -57,6 +67,67 @@ export function hash8(input: string): string {
|
|
|
57
67
|
return h.toString(16).padStart(8, "0")
|
|
58
68
|
}
|
|
59
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Env flag a nifra dev server sets for its own lifetime, so a framework compiler plugin can tell a dev
|
|
72
|
+
* compile from a `nifra build` one. Exported for the dev server that sets it; plugins ask
|
|
73
|
+
* {@link devServerCompile} instead of reading it.
|
|
74
|
+
*/
|
|
75
|
+
export const DEV_HMR_ENV = "NIFRA_DEV_HMR"
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* True while a nifra dev server is running, i.e. the compile may emit HMR wiring (and, where the
|
|
79
|
+
* framework couples the two, dev-mode runtime output).
|
|
80
|
+
*
|
|
81
|
+
* The phase cannot be a plugin constructor argument: an app builds its `clientPlugins` once in
|
|
82
|
+
* `nifra.config.ts` and the same plugin objects serve both `nifra dev` and `nifra build`. Guarding the
|
|
83
|
+
* emitted code with `if (import.meta.hot)` is not sufficient either - `Bun.build` keeps the branch, so
|
|
84
|
+
* the HMR calls were measured shipping in production client chunks. The dev server announces itself in
|
|
85
|
+
* the environment instead, and `nifra build` never sets it. Read per compile (not captured at
|
|
86
|
+
* registration) so the flag is honoured however late the server sets it.
|
|
87
|
+
*/
|
|
88
|
+
export function devServerCompile(): boolean {
|
|
89
|
+
return process.env[DEV_HMR_ENV] === "1"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The app root and the routes directory of the running dev server, for plugins that need to tell one of
|
|
94
|
+
* the app's own components from a dependency or a route module. Set alongside {@link DEV_HMR_ENV}; read
|
|
95
|
+
* through {@link devHotComponent}.
|
|
96
|
+
*/
|
|
97
|
+
export const DEV_ROOT_ENV = "NIFRA_DEV_ROOT"
|
|
98
|
+
export const DEV_ROUTES_ENV = "NIFRA_DEV_ROUTES"
|
|
99
|
+
|
|
100
|
+
const under = (path: string, dir: string | undefined): boolean =>
|
|
101
|
+
dir !== undefined &&
|
|
102
|
+
dir !== "" &&
|
|
103
|
+
(path === dir || path.startsWith(dir.endsWith("/") ? dir : `${dir}/`))
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Whether a dev compile of `path` may wrap the module in component-level hot-patching - i.e. whether it
|
|
107
|
+
* is one of the app's OWN components rather than a dependency or a route module.
|
|
108
|
+
*
|
|
109
|
+
* The exclusions are not caution, they are two things such a wrapper cannot survive:
|
|
110
|
+
*
|
|
111
|
+
* - a **dependency**, whose components are the framework's own composition machinery. An adapter folds
|
|
112
|
+
* a layout chain through them, so they are entered from a snippet or as a dynamic component - and a
|
|
113
|
+
* wrapper there sits between the SSR markup and the component that is supposed to claim it.
|
|
114
|
+
* - a **route module**, for the same reason one level down: a route or layout IS a chain member, so it
|
|
115
|
+
* is always the dynamic child. It is also the wrong boundary to patch - a route module carries the
|
|
116
|
+
* loader and meta the server ran, and re-running those is a navigation, not a patch. Editing one
|
|
117
|
+
* finds no accepting module and reloads the page, which is the correct outcome.
|
|
118
|
+
*
|
|
119
|
+
* Both cost nothing in practice: the components an edit loop actually touches are the views, and a view
|
|
120
|
+
* that wants to hot-patch lives outside the routes directory. Where the split is unknown (no dev server,
|
|
121
|
+
* or a root that was never announced), the answer is `false` - a full reload is always correct, a
|
|
122
|
+
* mispatched tree is not.
|
|
123
|
+
*/
|
|
124
|
+
export function devHotComponent(path: string): boolean {
|
|
125
|
+
const root = process.env[DEV_ROOT_ENV]
|
|
126
|
+
if (!under(path, root)) return false
|
|
127
|
+
if (path.includes("/node_modules/")) return false
|
|
128
|
+
return !under(path, process.env[DEV_ROUTES_ENV])
|
|
129
|
+
}
|
|
130
|
+
|
|
60
131
|
/**
|
|
61
132
|
* Records compiled CSS and wires it into the client bundle through a virtual `?<namespace>` module -
|
|
62
133
|
* the idiom the Vue plugin established (`?vue-css`). Register one per plugin `setup`; call `emit` per
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic rendering protocol shared by the web runtime and every render adapter.
|
|
3
|
+
*
|
|
4
|
+
* This module is intentionally small and has no runtime dependencies. Keeping the protocol and the
|
|
5
|
+
* dev SSR-loader singleton here lets adapters and conformance tests depend on the seam directly
|
|
6
|
+
* without importing the document renderer through the root module. The singleton is stored on
|
|
7
|
+
* globalThis because the Bun and Vite dev pipelines can evaluate two copies in one process.
|
|
8
|
+
*/
|
|
9
|
+
import type { Submission } from "./router.ts"
|
|
10
|
+
|
|
11
|
+
/** The data handed to a route component. */
|
|
12
|
+
export interface RenderProps {
|
|
13
|
+
readonly data: unknown
|
|
14
|
+
/** Per-layout loader data, aligned index-for-index with the layout prefix of `chain`. */
|
|
15
|
+
readonly layoutData?: readonly unknown[]
|
|
16
|
+
readonly actionData?: unknown
|
|
17
|
+
/** True while a client navigation or submit is in flight (client-only; absent/false on SSR). */
|
|
18
|
+
readonly pending?: boolean
|
|
19
|
+
/** The path a client navigation is transitioning TO while `pending` (client-only; absent on SSR). */
|
|
20
|
+
readonly pendingPath?: string
|
|
21
|
+
/** The in-flight client submit, for optimistic UI (client-only; absent on SSR). */
|
|
22
|
+
readonly submission?: Submission
|
|
23
|
+
/** The matched route's decoded path params. */
|
|
24
|
+
readonly params?: Readonly<Record<string, string>>
|
|
25
|
+
/** The current URL's `pathname + search` (no hash). */
|
|
26
|
+
readonly path?: string
|
|
27
|
+
/** The route's typed, validated search params. */
|
|
28
|
+
readonly search?: Record<string, unknown>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The seam every render adapter implements. New adapters should prove these invariants with
|
|
33
|
+
* `assertRenderAdapterConformance`; framework-specific behavior remains locally tested.
|
|
34
|
+
*/
|
|
35
|
+
export interface RenderAdapter {
|
|
36
|
+
/**
|
|
37
|
+
* Render a route's layout `chain` (outermost layout → page) to a Web stream of HTML bytes,
|
|
38
|
+
* including the framework's hydration markers.
|
|
39
|
+
*/
|
|
40
|
+
renderToStream(
|
|
41
|
+
chain: readonly unknown[],
|
|
42
|
+
props: RenderProps,
|
|
43
|
+
): ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>
|
|
44
|
+
/** Render the chain to a complete HTML string when no content is deferred. */
|
|
45
|
+
renderToString?(chain: readonly unknown[], props: RenderProps): string | Promise<string>
|
|
46
|
+
/** Per-document bootstrap markup injected into `<head>` for client hydration. */
|
|
47
|
+
hydrationHead(nonce?: string): string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Loads a module through the dev server's module graph rather than the runtime's resolver. */
|
|
51
|
+
export type SsrModuleLoader = (id: string) => Promise<unknown>
|
|
52
|
+
|
|
53
|
+
const SSR_MODULE_LOADER_SLOT = Symbol.for("nifra.web.ssr-module-loader")
|
|
54
|
+
const loaderSlot = globalThis as { [SSR_MODULE_LOADER_SLOT]?: SsrModuleLoader | undefined }
|
|
55
|
+
|
|
56
|
+
/** Publish or clear the dev server's SSR module loader. */
|
|
57
|
+
export function setSsrModuleLoader(load: SsrModuleLoader | undefined): void {
|
|
58
|
+
loaderSlot[SSR_MODULE_LOADER_SLOT] = load
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Return the dev server's SSR module loader, when one owns SSR resolution. */
|
|
62
|
+
export function ssrModuleLoader(): SsrModuleLoader | undefined {
|
|
63
|
+
return loaderSlot[SSR_MODULE_LOADER_SLOT]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Global the server serializes loader data into; the client reads it to hydrate. */
|
|
67
|
+
export const DATA_GLOBAL = "__NIFRA_DATA__"
|
|
68
|
+
/** Per-layout loader data for hydration. */
|
|
69
|
+
export const LAYOUT_DATA_GLOBAL = "__NIFRA_LAYOUT_DATA__"
|
|
70
|
+
/** Global the server writes the matched route id into. */
|
|
71
|
+
export const ROUTE_GLOBAL = "__NIFRA_ROUTE__"
|
|
72
|
+
/** Global the server serializes an action's data return into (absent on GETs); the client reads it
|
|
73
|
+
* so hydration after a native form POST matches the server-rendered markup. */
|
|
74
|
+
export const ACTION_GLOBAL = "__NIFRA_ACTION__"
|
|
75
|
+
/** Marker attribute used to find a non-default hydration container. */
|
|
76
|
+
export const ROOT_ATTRIBUTE = "data-nifra-root"
|
package/src/vite.ts
CHANGED
|
@@ -20,9 +20,11 @@ import { relative, resolve as resolvePath } from "node:path"
|
|
|
20
20
|
import { createDevDiagnostics } from "./dev-diagnostics.ts"
|
|
21
21
|
import { listenOrExplain } from "./dev-port.ts"
|
|
22
22
|
import { discoverRoutes } from "./fs.ts"
|
|
23
|
-
import { DEFAULT_DEV_PORT, generateClientEntry } from "./index.ts"
|
|
23
|
+
import { DEFAULT_DEV_PORT, generateClientEntry, setSsrModuleLoader } from "./index.ts"
|
|
24
24
|
import { vitePublicEnvPrefix } from "./internal/server-boundary.ts"
|
|
25
25
|
import { importVite } from "./internal/vite-import.ts"
|
|
26
|
+
import { scopedName } from "./plugins/css-modules.ts"
|
|
27
|
+
import { DEV_ROOT_ENV, DEV_ROUTES_ENV, reproduciblePath } from "./plugins/kit.ts"
|
|
26
28
|
import { viteServerFnStub } from "./plugins/vite-server-fn.ts"
|
|
27
29
|
import { viteServerOnlyEmpty } from "./plugins/vite-server-only.ts"
|
|
28
30
|
|
|
@@ -296,6 +298,12 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
|
|
|
296
298
|
const routesDir = resolvePath(options.routesDir)
|
|
297
299
|
const port = options.port ?? DEFAULT_DEV_PORT
|
|
298
300
|
|
|
301
|
+
// Which files are the app's own components, for framework plugins that hot-patch at component
|
|
302
|
+
// granularity (`devHotComponent`). Announced in the environment rather than passed, because the
|
|
303
|
+
// plugin that reads it is constructed by the app, in its own config, before this server exists.
|
|
304
|
+
process.env[DEV_ROOT_ENV] = root
|
|
305
|
+
process.env[DEV_ROUTES_ENV] = routesDir
|
|
306
|
+
|
|
299
307
|
// Codegen the client entry with Vite-servable, root-relative specifiers (e.g. `/routes/index.tsx`).
|
|
300
308
|
const toUrl = (file: string): string => `/${relative(root, `${routesDir}/${file}`)}`
|
|
301
309
|
const writeClientEntry = (): void => {
|
|
@@ -442,10 +450,26 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
|
|
|
442
450
|
externalConditions: ssrConditions,
|
|
443
451
|
},
|
|
444
452
|
},
|
|
453
|
+
css: {
|
|
454
|
+
modules: {
|
|
455
|
+
// Vite ships its own CSS-Modules naming (`_name_<base64>`); the Bun pipeline and every
|
|
456
|
+
// `nifra build` use `scopedName`. Left alone, one class is called two different things
|
|
457
|
+
// depending on which pipeline served it, so a selector written against a generated name works
|
|
458
|
+
// in dev and vanishes in production - and the two dev pipelines disagree with each other. The
|
|
459
|
+
// name is defined once, in the plugin, and Vite is handed that definition.
|
|
460
|
+
generateScopedName: (name: string, filename: string): string =>
|
|
461
|
+
scopedName(reproduciblePath(filename), name),
|
|
462
|
+
},
|
|
463
|
+
},
|
|
445
464
|
...(options.define ? { define: options.define } : {}),
|
|
446
465
|
})
|
|
447
466
|
|
|
448
467
|
const ssrLoad = (absolutePath: string): Promise<unknown> => vite.ssrLoadModule(absolutePath)
|
|
468
|
+
// Published so an ADAPTER can reach this graph too. Route modules already load through it; an
|
|
469
|
+
// adapter that has to load a compiled asset of its own on the server (`@nifrajs/web-svelte` and its
|
|
470
|
+
// `Chain.svelte`) would otherwise take it from the runtime, which has no compiler for it and no
|
|
471
|
+
// reason to agree with Vite about which copy of the framework runtime it renders through.
|
|
472
|
+
setSsrModuleLoader(ssrLoad)
|
|
449
473
|
app = await options.createApp(entryUrl, ssrLoad)
|
|
450
474
|
|
|
451
475
|
// Re-create the app on change so a hard reload picks up a route ADD/REMOVE (the manifest comes
|
|
@@ -486,6 +510,10 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
|
|
|
486
510
|
return {
|
|
487
511
|
port: typeof address === "object" && address !== null ? address.port : port,
|
|
488
512
|
stop: async () => {
|
|
513
|
+
// Cleared with the server that owns it: the slot is process-global, so a stopped server leaving
|
|
514
|
+
// its loader behind would hand the next one's adapter a graph that is closed (tests start and
|
|
515
|
+
// stop several dev servers in one process).
|
|
516
|
+
setSsrModuleLoader(undefined)
|
|
489
517
|
server.close()
|
|
490
518
|
await vite.close()
|
|
491
519
|
},
|