@nifrajs/web 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +18 -0
- package/css-modules.d.ts +34 -0
- package/dist/build.d.ts +110 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +238 -0
- package/dist/build.js.map +1 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +204 -0
- package/dist/client.js.map +1 -0
- package/dist/deferred.d.ts +90 -0
- package/dist/deferred.d.ts.map +1 -0
- package/dist/deferred.js +310 -0
- package/dist/deferred.js.map +1 -0
- package/dist/dev-error.d.ts +17 -0
- package/dist/dev-error.d.ts.map +1 -0
- package/dist/dev-error.js +59 -0
- package/dist/dev-error.js.map +1 -0
- package/dist/dev.d.ts +25 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +83 -0
- package/dist/dev.js.map +1 -0
- package/dist/draft.d.ts +58 -0
- package/dist/draft.d.ts.map +1 -0
- package/dist/draft.js +55 -0
- package/dist/draft.js.map +1 -0
- package/dist/fonts-google.d.ts +109 -0
- package/dist/fonts-google.d.ts.map +1 -0
- package/dist/fonts-google.js +291 -0
- package/dist/fonts-google.js.map +1 -0
- package/dist/fonts.d.ts +67 -0
- package/dist/fonts.d.ts.map +1 -0
- package/dist/fonts.js +103 -0
- package/dist/fonts.js.map +1 -0
- package/dist/fs.d.ts +10 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +24 -0
- package/dist/fs.js.map +1 -0
- package/dist/index.d.ts +280 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +784 -0
- package/dist/index.js.map +1 -0
- package/dist/islands.d.ts +37 -0
- package/dist/islands.d.ts.map +1 -0
- package/dist/islands.js +107 -0
- package/dist/islands.js.map +1 -0
- package/dist/isr.d.ts +154 -0
- package/dist/isr.d.ts.map +1 -0
- package/dist/isr.js +331 -0
- package/dist/isr.js.map +1 -0
- package/dist/manifest.d.ts +178 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +213 -0
- package/dist/manifest.js.map +1 -0
- package/dist/plugins/css-modules.d.ts +40 -0
- package/dist/plugins/css-modules.d.ts.map +1 -0
- package/dist/plugins/css-modules.js +0 -0
- package/dist/plugins/css-modules.js.map +1 -0
- package/dist/plugins/kit.d.ts +44 -0
- package/dist/plugins/kit.d.ts.map +1 -0
- package/dist/plugins/kit.js +105 -0
- package/dist/plugins/kit.js.map +1 -0
- package/dist/plugins/scss.d.ts +27 -0
- package/dist/plugins/scss.d.ts.map +1 -0
- package/dist/plugins/scss.js +78 -0
- package/dist/plugins/scss.js.map +1 -0
- package/dist/prerender.d.ts +77 -0
- package/dist/prerender.d.ts.map +1 -0
- package/dist/prerender.js +139 -0
- package/dist/prerender.js.map +1 -0
- package/dist/query.d.ts +62 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +184 -0
- package/dist/query.js.map +1 -0
- package/dist/router.d.ts +169 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +531 -0
- package/dist/router.js.map +1 -0
- package/dist/vite.d.ts +49 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +144 -0
- package/dist/vite.js.map +1 -0
- package/package.json +127 -0
- package/src/build.ts +375 -0
- package/src/client.ts +218 -0
- package/src/deferred.ts +339 -0
- package/src/dev-error.ts +62 -0
- package/src/dev.ts +108 -0
- package/src/draft.ts +82 -0
- package/src/fonts-google.ts +397 -0
- package/src/fonts.ts +147 -0
- package/src/fs.ts +26 -0
- package/src/index.ts +1202 -0
- package/src/islands.ts +121 -0
- package/src/isr.ts +461 -0
- package/src/manifest.ts +366 -0
- package/src/plugins/css-modules.ts +0 -0
- package/src/plugins/kit.ts +123 -0
- package/src/plugins/scss.ts +108 -0
- package/src/prerender.ts +209 -0
- package/src/query.ts +250 -0
- package/src/router.ts +676 -0
- package/src/vite.ts +212 -0
package/src/fonts.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font optimization primitives — generate a **CLS-safe `@font-face`** rule and the matching
|
|
3
|
+
* **preload `<link>`** for a self-hosted font. The two fixes that matter most (the ones `next/font`
|
|
4
|
+
* automates) are right here: `font-display: swap` (text paints immediately, no invisible-text FOIT)
|
|
5
|
+
* plus a preload so the file downloads with the document, and optional metric overrides
|
|
6
|
+
* (`size-adjust`/`ascent-override`/…) so swapping from the fallback to the web font doesn't shift layout.
|
|
7
|
+
*
|
|
8
|
+
* Self-host the file (drop a `.woff2` in your assets — never hotlink Google's CDN), then:
|
|
9
|
+
*
|
|
10
|
+
* // fonts.css (imported by your app — the CSS pipeline bundles + links it)
|
|
11
|
+
* import { fontFace } from "@nifrajs/web"
|
|
12
|
+
* export default fontFace({ family: "Inter", src: [{ url: "/fonts/inter.woff2" }], weight: "100 900" })
|
|
13
|
+
*
|
|
14
|
+
* // a root layout's meta — becomes a <link rel="preload" as="font"> in <head>
|
|
15
|
+
* export const meta = { link: [fontPreload({ href: "/fonts/inter.woff2" })] }
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** `font-display` strategy. `swap` (the default here) paints fallback text immediately, then swaps. */
|
|
19
|
+
export type FontDisplay = "auto" | "block" | "swap" | "fallback" | "optional"
|
|
20
|
+
|
|
21
|
+
export interface FontSource {
|
|
22
|
+
/** URL of the self-hosted font file. */
|
|
23
|
+
readonly url: string
|
|
24
|
+
/** `format()` hint (`woff2`, `woff`, `truetype`, …). Inferred from the URL extension when omitted. */
|
|
25
|
+
readonly format?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface FontFace {
|
|
29
|
+
readonly family: string
|
|
30
|
+
/** One or more sources (best format first; the browser picks the first it supports). */
|
|
31
|
+
readonly src: readonly FontSource[]
|
|
32
|
+
/** `400`, `"700"`, or a variable-font range like `"100 900"`. */
|
|
33
|
+
readonly weight?: number | string
|
|
34
|
+
readonly style?: "normal" | "italic" | "oblique"
|
|
35
|
+
/** Defaults to `"swap"` (no flash of invisible text). */
|
|
36
|
+
readonly display?: FontDisplay
|
|
37
|
+
/** Restrict the glyphs this face covers, e.g. `"U+0000-00FF"` — lets the browser skip the download
|
|
38
|
+
* for pages without those characters. */
|
|
39
|
+
readonly unicodeRange?: string
|
|
40
|
+
/** Metric overrides that eliminate layout shift when the fallback swaps to the web font (the
|
|
41
|
+
* `next/font` technique). e.g. `sizeAdjust: "105%"`, `ascentOverride: "90%"`. */
|
|
42
|
+
readonly sizeAdjust?: string
|
|
43
|
+
readonly ascentOverride?: string
|
|
44
|
+
readonly descentOverride?: string
|
|
45
|
+
readonly lineGapOverride?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const FORMAT_BY_EXT: Readonly<Record<string, string>> = {
|
|
49
|
+
woff2: "woff2",
|
|
50
|
+
woff: "woff",
|
|
51
|
+
ttf: "truetype",
|
|
52
|
+
otf: "opentype",
|
|
53
|
+
eot: "embedded-opentype",
|
|
54
|
+
svg: "svg",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const MIME_BY_EXT: Readonly<Record<string, string>> = {
|
|
58
|
+
woff2: "font/woff2",
|
|
59
|
+
woff: "font/woff",
|
|
60
|
+
ttf: "font/ttf",
|
|
61
|
+
otf: "font/otf",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Lowercased file extension of a URL, ignoring any `?query`/`#hash`. */
|
|
65
|
+
function extOf(url: string): string {
|
|
66
|
+
const clean = url.split(/[?#]/, 1)[0] ?? url
|
|
67
|
+
const dot = clean.lastIndexOf(".")
|
|
68
|
+
return dot === -1 ? "" : clean.slice(dot + 1).toLowerCase()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Escape a value placed inside a double-quoted CSS string (`font-family`, `url("…")`, `format("…")`):
|
|
72
|
+
* neutralize the quote, the escape char, and newlines so a value can't break out of the declaration. */
|
|
73
|
+
function cssQuoted(value: string): string {
|
|
74
|
+
return value.replace(/[\\"]/g, "\\$&").replace(/[\r\n]+/g, " ")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Sanitize an **unquoted** CSS token value (`font-weight`, `unicode-range`, `size-adjust`, …): drop the
|
|
78
|
+
* characters that could end the declaration or the rule and inject more CSS. */
|
|
79
|
+
function cssToken(value: string): string {
|
|
80
|
+
return value
|
|
81
|
+
.replace(/[;{}<>]/g, "")
|
|
82
|
+
.replace(/[\r\n]+/g, " ")
|
|
83
|
+
.trim()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build a single `@font-face` CSS rule. Defaults to `font-display: swap`; infers each source's
|
|
88
|
+
* `format()` from its extension. All values are CSS-escaped, so a dynamic family/URL can't inject CSS.
|
|
89
|
+
* Put the result in a stylesheet your app imports (nifra's CSS pipeline bundles + links it).
|
|
90
|
+
*/
|
|
91
|
+
export function fontFace(face: FontFace): string {
|
|
92
|
+
if (face.src.length === 0) {
|
|
93
|
+
throw new Error("fontFace: `src` needs at least one source")
|
|
94
|
+
}
|
|
95
|
+
const src = face.src
|
|
96
|
+
.map((source) => {
|
|
97
|
+
const format = source.format ?? FORMAT_BY_EXT[extOf(source.url)]
|
|
98
|
+
const formatPart = format === undefined ? "" : ` format("${cssQuoted(format)}")`
|
|
99
|
+
return `url("${cssQuoted(source.url)}")${formatPart}`
|
|
100
|
+
})
|
|
101
|
+
.join(", ")
|
|
102
|
+
const declarations = [
|
|
103
|
+
`font-family: "${cssQuoted(face.family)}"`,
|
|
104
|
+
`src: ${src}`,
|
|
105
|
+
`font-display: ${face.display ?? "swap"}`,
|
|
106
|
+
]
|
|
107
|
+
if (face.weight !== undefined) declarations.push(`font-weight: ${cssToken(String(face.weight))}`)
|
|
108
|
+
if (face.style !== undefined) declarations.push(`font-style: ${face.style}`)
|
|
109
|
+
if (face.unicodeRange !== undefined)
|
|
110
|
+
declarations.push(`unicode-range: ${cssToken(face.unicodeRange)}`)
|
|
111
|
+
if (face.sizeAdjust !== undefined) declarations.push(`size-adjust: ${cssToken(face.sizeAdjust)}`)
|
|
112
|
+
if (face.ascentOverride !== undefined)
|
|
113
|
+
declarations.push(`ascent-override: ${cssToken(face.ascentOverride)}`)
|
|
114
|
+
if (face.descentOverride !== undefined)
|
|
115
|
+
declarations.push(`descent-override: ${cssToken(face.descentOverride)}`)
|
|
116
|
+
if (face.lineGapOverride !== undefined)
|
|
117
|
+
declarations.push(`line-gap-override: ${cssToken(face.lineGapOverride)}`)
|
|
118
|
+
return `@font-face {\n ${declarations.join(";\n ")};\n}`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface FontPreloadInput {
|
|
122
|
+
/** URL of the self-hosted font file to preload. */
|
|
123
|
+
readonly href: string
|
|
124
|
+
/** MIME type (`font/woff2`, …). Inferred from the extension when omitted. */
|
|
125
|
+
readonly type?: string
|
|
126
|
+
/** `crossorigin` mode — defaults to `"anonymous"`. Fonts are always fetched in CORS mode, and a
|
|
127
|
+
* preload whose `crossorigin` doesn't match the actual fetch is **wasted** (downloaded twice), so
|
|
128
|
+
* this is on by default. */
|
|
129
|
+
readonly crossOrigin?: "anonymous" | "use-credentials"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Build a font preload as a `<link>` attribute set for a route/layout's `meta.link` — nifra injects it
|
|
134
|
+
* into `<head>` (`<link rel="preload" as="font" type="font/woff2" crossorigin="anonymous">`). Values are
|
|
135
|
+
* escaped at injection by the head renderer. Preloading the font file removes a render-blocking round
|
|
136
|
+
* trip (the browser would otherwise discover the font only after parsing the CSS).
|
|
137
|
+
*/
|
|
138
|
+
export function fontPreload(input: FontPreloadInput): Record<string, string> {
|
|
139
|
+
const type = input.type ?? MIME_BY_EXT[extOf(input.href)]
|
|
140
|
+
return {
|
|
141
|
+
rel: "preload",
|
|
142
|
+
as: "font",
|
|
143
|
+
href: input.href,
|
|
144
|
+
...(type === undefined ? {} : { type }),
|
|
145
|
+
crossorigin: input.crossOrigin ?? "anonymous",
|
|
146
|
+
}
|
|
147
|
+
}
|
package/src/fs.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nifrajs/web/fs — filesystem route discovery. Separated from the main entry so `app.fetch`
|
|
3
|
+
* (and the rest of @nifrajs/web) stays fs-free + edge-portable: this runs at build/startup
|
|
4
|
+
* only. It scans a routes dir and feeds the pure `buildManifest`.
|
|
5
|
+
*/
|
|
6
|
+
import { readdirSync } from "node:fs"
|
|
7
|
+
import { buildManifest, type Manifest, type RouteModule } from "./manifest.ts"
|
|
8
|
+
|
|
9
|
+
/** Options for {@link discoverRoutes}. */
|
|
10
|
+
export interface DiscoverRoutesOptions {
|
|
11
|
+
/** Appended (as `?<importQuery>`) to each route's dynamic import — the dev server sets a
|
|
12
|
+
* changing value to bust Bun's module cache so SSR picks up edited route files. */
|
|
13
|
+
readonly importQuery?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Scan a `routes/` directory (recursively) and build the route manifest. */
|
|
17
|
+
export function discoverRoutes(dir: string, options: DiscoverRoutesOptions = {}): Manifest {
|
|
18
|
+
const files = (readdirSync(dir, { recursive: true }) as string[])
|
|
19
|
+
.map((file) => file.replaceAll("\\", "/")) // normalize Windows separators
|
|
20
|
+
.filter((file) => /\.(tsx|jsx|svelte|vue|mdx)$/.test(file))
|
|
21
|
+
const query = options.importQuery ? `?${options.importQuery}` : ""
|
|
22
|
+
return buildManifest(
|
|
23
|
+
files,
|
|
24
|
+
(file) => () => import(`${dir}/${file}${query}`) as Promise<RouteModule>,
|
|
25
|
+
)
|
|
26
|
+
}
|