@nifrajs/web 2.4.0 → 2.6.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.d.ts.map +1 -1
- package/dist/build.js +18 -9
- package/dist/build.js.map +1 -1
- package/dist/fonts-google.d.ts.map +1 -1
- package/dist/fonts-google.js +84 -16
- package/dist/fonts-google.js.map +1 -1
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +101 -58
- package/dist/index.js.map +1 -1
- package/dist/internal/js-string.d.ts +9 -0
- package/dist/internal/js-string.d.ts.map +1 -0
- package/dist/internal/js-string.js +18 -0
- package/dist/internal/js-string.js.map +1 -0
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +41 -7
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +4 -1
- package/dist/manifest.js.map +1 -1
- package/dist/plugins/svg.d.ts +9 -0
- package/dist/plugins/svg.d.ts.map +1 -1
- package/dist/plugins/svg.js +39 -5
- package/dist/plugins/svg.js.map +1 -1
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +10 -2
- package/dist/prerender.js.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +13 -2
- package/dist/router.js.map +1 -1
- package/dist/service-worker.d.ts.map +1 -1
- package/dist/service-worker.js +2 -1
- package/dist/service-worker.js.map +1 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +58 -1
- package/dist/vite.js.map +1 -1
- package/package.json +3 -2
- package/src/build.ts +17 -9
- package/src/fonts-google.ts +73 -15
- package/src/index.ts +135 -67
- package/src/internal/js-string.ts +19 -0
- package/src/internal/server-boundary.ts +40 -7
- package/src/manifest.ts +4 -1
- package/src/plugins/svg.ts +37 -5
- package/src/prerender.ts +10 -2
- package/src/router.ts +13 -2
- package/src/service-worker.ts +3 -1
- package/src/vite.ts +59 -1
package/src/manifest.ts
CHANGED
|
@@ -320,7 +320,10 @@ const errorIdFor = (dir: string): string => (dir === "" ? "_error" : `${dir}/_er
|
|
|
320
320
|
* the last segment.
|
|
321
321
|
*/
|
|
322
322
|
/** Every `[…]` marker in a file segment, so a part-literal segment can be spliced into a pattern. */
|
|
323
|
-
|
|
323
|
+
// `[` is excluded from the inner class as well as `]`: marker names are identifiers, and letting the
|
|
324
|
+
// class swallow `[` makes a run of unclosed brackets backtrack quadratically (each start position
|
|
325
|
+
// re-scans the rest of the segment before failing).
|
|
326
|
+
const FILE_MARKER = /\[([^\][]*)\]/g
|
|
324
327
|
const FILE_PARAM_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
325
328
|
|
|
326
329
|
/**
|
package/src/plugins/svg.ts
CHANGED
|
@@ -77,14 +77,46 @@ export interface SvgToJsxOptions {
|
|
|
77
77
|
readonly classProp?: string
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Strip the XML declaration, comments, and DOCTYPE from an SVG source by index scan. Shared by every
|
|
82
|
+
* adapter's SVG-component transform instead of chained regex `replace`s, for two reasons: the lazy
|
|
83
|
+
* `[\s\S]*?` scans backtrack quadratically on an unterminated marker, and sequential single-pass
|
|
84
|
+
* replaces can splice removed delimiters back together (`<<!---->!DOCTYPE …>` survives them). The
|
|
85
|
+
* scan removes each block in one forward pass, so neither failure mode exists. An unterminated
|
|
86
|
+
* marker swallows the rest of the input - the correct reading, matching an XML parser.
|
|
87
|
+
*/
|
|
88
|
+
export function stripSvgPreamble(xml: string): string {
|
|
89
|
+
const parts: string[] = []
|
|
90
|
+
let keepFrom = 0
|
|
91
|
+
let i = 0
|
|
92
|
+
while (i < xml.length) {
|
|
93
|
+
let skipEnd = -1
|
|
94
|
+
if (xml.startsWith("<?", i)) {
|
|
95
|
+
const close = xml.indexOf("?>", i + 2)
|
|
96
|
+
skipEnd = close === -1 ? xml.length : close + 2
|
|
97
|
+
} else if (xml.startsWith("<!--", i)) {
|
|
98
|
+
const close = xml.indexOf("-->", i + 4)
|
|
99
|
+
skipEnd = close === -1 ? xml.length : close + 3
|
|
100
|
+
} else if (xml.slice(i, i + 9).toLowerCase() === "<!doctype") {
|
|
101
|
+
const close = xml.indexOf(">", i + 9)
|
|
102
|
+
skipEnd = close === -1 ? xml.length : close + 1
|
|
103
|
+
}
|
|
104
|
+
if (skipEnd === -1) {
|
|
105
|
+
i++
|
|
106
|
+
} else {
|
|
107
|
+
parts.push(xml.slice(keepFrom, i))
|
|
108
|
+
i = skipEnd
|
|
109
|
+
keepFrom = skipEnd
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
parts.push(xml.slice(keepFrom))
|
|
113
|
+
return parts.join("").trim()
|
|
114
|
+
}
|
|
115
|
+
|
|
80
116
|
/** Convert an SVG XML string into a JSX-safe `<svg>…</svg>` element with `{...props}` spread on the root. */
|
|
81
117
|
export function svgToJsx(xml: string, options: SvgToJsxOptions = {}): string {
|
|
82
118
|
const classProp = options.classProp ?? "className"
|
|
83
|
-
let out = xml
|
|
84
|
-
.replace(/<\?xml[\s\S]*?\?>/g, "") // XML declaration
|
|
85
|
-
.replace(/<!--[\s\S]*?-->/g, "") // comments
|
|
86
|
-
.replace(/<!DOCTYPE[^>]*>/gi, "")
|
|
87
|
-
.trim()
|
|
119
|
+
let out = stripSvgPreamble(xml)
|
|
88
120
|
|
|
89
121
|
// Namespaced attributes → camelCase (xlink:href → xlinkHref, xmlns:xlink → xmlnsXlink).
|
|
90
122
|
out = out.replace(/\b([a-z]+):([a-z]+)=/gi, (_m, ns: string, name: string) => {
|
package/src/prerender.ts
CHANGED
|
@@ -52,9 +52,17 @@ export interface PrerenderResult {
|
|
|
52
52
|
readonly fallbacks: Readonly<Record<string, "ssr" | "404">>
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/** Strip trailing `/` runs by index scan - an unanchored-start `/\/+$/` replace backtracks
|
|
56
|
+
* quadratically on a long slash run, and route patterns are input here. */
|
|
57
|
+
function trimTrailingSlashes(path: string): string {
|
|
58
|
+
let end = path.length
|
|
59
|
+
while (end > 0 && path.charCodeAt(end - 1) === 47 /* '/' */) end--
|
|
60
|
+
return path.slice(0, end)
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
/** Map a route path to its output file: `/` → `index.html`, `/a/b` → `a/b/index.html`. */
|
|
56
64
|
export function htmlFileFor(pattern: string): string {
|
|
57
|
-
const trimmed = pattern.replace(/^\/+/, "")
|
|
65
|
+
const trimmed = trimTrailingSlashes(pattern.replace(/^\/+/, ""))
|
|
58
66
|
return trimmed === "" ? "index.html" : `${trimmed}/index.html`
|
|
59
67
|
}
|
|
60
68
|
|
|
@@ -203,7 +211,7 @@ export function cloudflarePagesRoutes(
|
|
|
203
211
|
const exclude = [...(options.staticGlobs ?? ["/assets/*"])]
|
|
204
212
|
for (const path of options.prerendered) {
|
|
205
213
|
exclude.push(path) // the prerendered document
|
|
206
|
-
exclude.push(path === "/" ? "/_data.json" : `${path
|
|
214
|
+
exclude.push(path === "/" ? "/_data.json" : `${trimTrailingSlashes(path)}/_data.json`) // its data
|
|
207
215
|
}
|
|
208
216
|
return { version: 1, include: ["/*"], exclude }
|
|
209
217
|
}
|
package/src/router.ts
CHANGED
|
@@ -322,10 +322,18 @@ const readResponseData = (res: Response, signal?: AbortSignal): Promise<unknown>
|
|
|
322
322
|
? parseNdjsonData(res.body, signal)
|
|
323
323
|
: res.json()
|
|
324
324
|
|
|
325
|
+
// Index scan, not `/\/+$/` - the unanchored-start trailing-run replace backtracks quadratically and
|
|
326
|
+
// the pathname is caller-controlled.
|
|
327
|
+
const trimTrailingSlashes = (path: string): string => {
|
|
328
|
+
let end = path.length
|
|
329
|
+
while (end > 0 && path.charCodeAt(end - 1) === 47 /* '/' */) end--
|
|
330
|
+
return path.slice(0, end)
|
|
331
|
+
}
|
|
332
|
+
|
|
325
333
|
// The static `_data.json` URL for a prerendered path: `/` → `/_data.json`, `/users/7` →
|
|
326
334
|
// `/users/7/_data.json` (mirrors the build's `dataFileFor`).
|
|
327
335
|
const dataUrlFor = (pathname: string): string =>
|
|
328
|
-
pathname === "/" ? "/_data.json" : `${pathname
|
|
336
|
+
pathname === "/" ? "/_data.json" : `${trimTrailingSlashes(pathname)}/_data.json`
|
|
329
337
|
|
|
330
338
|
const defaultFetchData: FetchRouteData = async (path, _match, signal, navigation) => {
|
|
331
339
|
// SSG fast path: if this path was prerendered, its loader data is a static file - fetch that (no
|
|
@@ -333,7 +341,10 @@ const defaultFetchData: FetchRouteData = async (path, _match, signal, navigation
|
|
|
333
341
|
// or a stale set), so it's always safe. Non-SSG apps have no global → the dynamic path, unchanged.
|
|
334
342
|
const prerendered = (globalThis as { [PRERENDERED_GLOBAL]?: unknown })[PRERENDERED_GLOBAL]
|
|
335
343
|
if (Array.isArray(prerendered)) {
|
|
336
|
-
|
|
344
|
+
// `search` + `slice` rather than `replace(/[?#].*$/)`: the trailing `.*$` backtracks
|
|
345
|
+
// quadratically on adversarial input, and the URL is caller-controlled.
|
|
346
|
+
const cut = path.search(/[?#]/)
|
|
347
|
+
const pathname = cut === -1 ? path : path.slice(0, cut)
|
|
337
348
|
if (prerendered.includes(pathname)) {
|
|
338
349
|
const staticRes = await fetch(dataUrlFor(pathname), { signal: signal ?? null })
|
|
339
350
|
if (staticRes.ok) return readResponseData(staticRes, signal)
|
package/src/service-worker.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface ServiceWorkerOptions {
|
|
|
45
45
|
const SW_URL = /^\/[\w\-./@%]*$/
|
|
46
46
|
|
|
47
47
|
/** A precache URL has to be same-origin, rooted, and free of anything that could break out of a list. */
|
|
48
|
+
import { jsStringLiteral } from "./internal/js-string.ts"
|
|
49
|
+
|
|
48
50
|
function assertPrecachable(url: string, label: string): void {
|
|
49
51
|
if (typeof url !== "string" || url === "") {
|
|
50
52
|
throw new Error(`[nifra/service-worker] ${label} must be a non-empty string`)
|
|
@@ -161,5 +163,5 @@ self.addEventListener("fetch", (event) => {
|
|
|
161
163
|
*/
|
|
162
164
|
export function serviceWorkerRegistration(scriptUrl = "/sw.js"): string {
|
|
163
165
|
assertPrecachable(scriptUrl, "scriptUrl")
|
|
164
|
-
return `if("serviceWorker"in navigator){addEventListener("load",function(){navigator.serviceWorker.register(${
|
|
166
|
+
return `if("serviceWorker"in navigator){addEventListener("load",function(){navigator.serviceWorker.register(${jsStringLiteral(scriptUrl)}).catch(function(){})})}`
|
|
165
167
|
}
|
package/src/vite.ts
CHANGED
|
@@ -271,6 +271,22 @@ export function normalizeRolldownPlugins(
|
|
|
271
271
|
})
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/**
|
|
275
|
+
* The npm package a bare module specifier belongs to (`@nifrajs/web-react/client` →
|
|
276
|
+
* `@nifrajs/web-react`, `some-adapter/client` → `some-adapter`), or `undefined` for a path-shaped
|
|
277
|
+
* specifier (relative/absolute), which names no package.
|
|
278
|
+
*/
|
|
279
|
+
function packageNameOf(specifier: string): string | undefined {
|
|
280
|
+
if (specifier.startsWith(".") || specifier.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(specifier))
|
|
281
|
+
return undefined
|
|
282
|
+
const parts = specifier.split("/")
|
|
283
|
+
return specifier.startsWith("@")
|
|
284
|
+
? parts.length >= 2
|
|
285
|
+
? `${parts[0]}/${parts[1]}`
|
|
286
|
+
: undefined
|
|
287
|
+
: parts[0]
|
|
288
|
+
}
|
|
289
|
+
|
|
274
290
|
/**
|
|
275
291
|
* Start the Vite-backed dev server: Vite serves/HMRs the client; nifra SSRs each request and Vite
|
|
276
292
|
* injects its HMR client + the framework refresh preamble via `transformIndexHtml`.
|
|
@@ -349,6 +365,34 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
|
|
|
349
365
|
|
|
350
366
|
// `conditions: ["bun"]` makes Vite resolve nifra's workspace packages (`@nifrajs/web-react/client`, …) to
|
|
351
367
|
// their TS **source** - so the dev server needs no prior `dist` build of the adapter packages.
|
|
368
|
+
//
|
|
369
|
+
// That covers the CLIENT half only: Vite resolves SSR through `ssr.resolve.*`, which ignores
|
|
370
|
+
// `resolve.conditions`. Left at its defaults, SSR resolves a package's `default` export (`dist/…`)
|
|
371
|
+
// while the app's own imports of the same package run under Bun and resolve the `bun` export
|
|
372
|
+
// (`src/…`). Two consequences, both real bugs:
|
|
373
|
+
// 1. The adapter package loads TWICE - the app's Bun import (the adapter passed to `createWebApp`)
|
|
374
|
+
// and the route modules' Vite-SSR import - so `createContext` runs twice and the routing hooks
|
|
375
|
+
// (`useSearch`/`useParams`/`useLocation`) read a context the render never provided: they
|
|
376
|
+
// SSR-render their EMPTY defaults in dev while the loader (same request!) sees the real values.
|
|
377
|
+
// 2. SSR reads `dist`, which nothing in the dev loop rebuilds - a stale `dist` then 500s inside
|
|
378
|
+
// framework code and reads exactly like a framework regression.
|
|
379
|
+
// Fix, below: `ssr.resolve.{conditions,externalConditions}` mirror the client conditions (so SSR
|
|
380
|
+
// resolves the same files Bun does), and the adapter package + `@nifrajs/web` are `ssr.external`
|
|
381
|
+
// (so Bun's OWN module cache serves them to route modules - one instance, one `RouterContext`).
|
|
382
|
+
const resolveConditions = [
|
|
383
|
+
...(options.conditions ?? []),
|
|
384
|
+
"bun",
|
|
385
|
+
"module",
|
|
386
|
+
"browser",
|
|
387
|
+
"development",
|
|
388
|
+
]
|
|
389
|
+
// SSR must not pick `browser` builds; otherwise identical to the client list so both halves agree.
|
|
390
|
+
const ssrConditions = [...(options.conditions ?? []), "bun", "module", "node", "development"]
|
|
391
|
+
// The adapter package (from `clientModule`, e.g. `@nifrajs/web-react/client` → `@nifrajs/web-react`)
|
|
392
|
+
// - identity-sensitive (its `RouterContext` must be the ONE the Bun-imported adapter provides). A
|
|
393
|
+
// path-shaped `clientModule` (tests) names no package; there is then nothing to externalize.
|
|
394
|
+
const adapterPackage = packageNameOf(options.clientModule)
|
|
395
|
+
const ssrExternal = [...(adapterPackage !== undefined ? [adapterPackage] : []), "@nifrajs/web"]
|
|
352
396
|
const usePolling = options.poll ?? process.env.CHOKIDAR_USEPOLLING === "1"
|
|
353
397
|
// Never `import("vite")` directly here: the guard in `importVite` has to run first, and the dev
|
|
354
398
|
// server importing vite unguarded is precisely what poisoned the module for the whole process.
|
|
@@ -376,7 +420,7 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
|
|
|
376
420
|
// reads it, and the dev server is a client bundler like any other.
|
|
377
421
|
plugins: [viteServerFnStub(), viteServerOnlyEmpty(), ...plugins],
|
|
378
422
|
resolve: {
|
|
379
|
-
conditions:
|
|
423
|
+
conditions: resolveConditions,
|
|
380
424
|
// Dedupe React to ONE copy. In a multi-root workspace a shared package can pull react/react-dom
|
|
381
425
|
// from a SIBLING app's node_modules, so the dev server loads two React cores → a second hook
|
|
382
426
|
// dispatcher → `resolveDispatcher().useState` null on any hook-using route (the error points at the
|
|
@@ -384,6 +428,20 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
|
|
|
384
428
|
// dev matches prod. No-op for non-React apps (the package simply isn't present to dedupe).
|
|
385
429
|
dedupe: ["react", "react-dom"],
|
|
386
430
|
},
|
|
431
|
+
ssr: {
|
|
432
|
+
// Explicit listing forces external even for workspace-LINKED packages (Vite's default keeps
|
|
433
|
+
// linked deps internal), so a monorepo dev run gets the same single-instance guarantee as an
|
|
434
|
+
// npm-installed one.
|
|
435
|
+
external: ssrExternal,
|
|
436
|
+
resolve: {
|
|
437
|
+
// Non-externalized deps (a linked `@platform/*`, say): Vite's runner evaluates them, but from
|
|
438
|
+
// the same files Bun would pick - `bun`-conditioned source, never a stale `dist`.
|
|
439
|
+
conditions: ssrConditions,
|
|
440
|
+
// Externalized deps: Vite resolves the specifier, Bun imports the result. `bun` first so the
|
|
441
|
+
// resolved file IS the one already in Bun's module cache (one evaluation, shared context).
|
|
442
|
+
externalConditions: ssrConditions,
|
|
443
|
+
},
|
|
444
|
+
},
|
|
387
445
|
...(options.define ? { define: options.define } : {}),
|
|
388
446
|
})
|
|
389
447
|
|