@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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -0
  3. package/css-modules.d.ts +34 -0
  4. package/dist/build.d.ts +110 -0
  5. package/dist/build.d.ts.map +1 -0
  6. package/dist/build.js +238 -0
  7. package/dist/build.js.map +1 -0
  8. package/dist/client.d.ts +32 -0
  9. package/dist/client.d.ts.map +1 -0
  10. package/dist/client.js +204 -0
  11. package/dist/client.js.map +1 -0
  12. package/dist/deferred.d.ts +90 -0
  13. package/dist/deferred.d.ts.map +1 -0
  14. package/dist/deferred.js +310 -0
  15. package/dist/deferred.js.map +1 -0
  16. package/dist/dev-error.d.ts +17 -0
  17. package/dist/dev-error.d.ts.map +1 -0
  18. package/dist/dev-error.js +59 -0
  19. package/dist/dev-error.js.map +1 -0
  20. package/dist/dev.d.ts +25 -0
  21. package/dist/dev.d.ts.map +1 -0
  22. package/dist/dev.js +83 -0
  23. package/dist/dev.js.map +1 -0
  24. package/dist/draft.d.ts +58 -0
  25. package/dist/draft.d.ts.map +1 -0
  26. package/dist/draft.js +55 -0
  27. package/dist/draft.js.map +1 -0
  28. package/dist/fonts-google.d.ts +109 -0
  29. package/dist/fonts-google.d.ts.map +1 -0
  30. package/dist/fonts-google.js +291 -0
  31. package/dist/fonts-google.js.map +1 -0
  32. package/dist/fonts.d.ts +67 -0
  33. package/dist/fonts.d.ts.map +1 -0
  34. package/dist/fonts.js +103 -0
  35. package/dist/fonts.js.map +1 -0
  36. package/dist/fs.d.ts +10 -0
  37. package/dist/fs.d.ts.map +1 -0
  38. package/dist/fs.js +24 -0
  39. package/dist/fs.js.map +1 -0
  40. package/dist/index.d.ts +280 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +784 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/islands.d.ts +37 -0
  45. package/dist/islands.d.ts.map +1 -0
  46. package/dist/islands.js +107 -0
  47. package/dist/islands.js.map +1 -0
  48. package/dist/isr.d.ts +154 -0
  49. package/dist/isr.d.ts.map +1 -0
  50. package/dist/isr.js +331 -0
  51. package/dist/isr.js.map +1 -0
  52. package/dist/manifest.d.ts +178 -0
  53. package/dist/manifest.d.ts.map +1 -0
  54. package/dist/manifest.js +213 -0
  55. package/dist/manifest.js.map +1 -0
  56. package/dist/plugins/css-modules.d.ts +40 -0
  57. package/dist/plugins/css-modules.d.ts.map +1 -0
  58. package/dist/plugins/css-modules.js +0 -0
  59. package/dist/plugins/css-modules.js.map +1 -0
  60. package/dist/plugins/kit.d.ts +44 -0
  61. package/dist/plugins/kit.d.ts.map +1 -0
  62. package/dist/plugins/kit.js +105 -0
  63. package/dist/plugins/kit.js.map +1 -0
  64. package/dist/plugins/scss.d.ts +27 -0
  65. package/dist/plugins/scss.d.ts.map +1 -0
  66. package/dist/plugins/scss.js +78 -0
  67. package/dist/plugins/scss.js.map +1 -0
  68. package/dist/prerender.d.ts +77 -0
  69. package/dist/prerender.d.ts.map +1 -0
  70. package/dist/prerender.js +139 -0
  71. package/dist/prerender.js.map +1 -0
  72. package/dist/query.d.ts +62 -0
  73. package/dist/query.d.ts.map +1 -0
  74. package/dist/query.js +184 -0
  75. package/dist/query.js.map +1 -0
  76. package/dist/router.d.ts +169 -0
  77. package/dist/router.d.ts.map +1 -0
  78. package/dist/router.js +531 -0
  79. package/dist/router.js.map +1 -0
  80. package/dist/vite.d.ts +49 -0
  81. package/dist/vite.d.ts.map +1 -0
  82. package/dist/vite.js +144 -0
  83. package/dist/vite.js.map +1 -0
  84. package/package.json +127 -0
  85. package/src/build.ts +375 -0
  86. package/src/client.ts +218 -0
  87. package/src/deferred.ts +339 -0
  88. package/src/dev-error.ts +62 -0
  89. package/src/dev.ts +108 -0
  90. package/src/draft.ts +82 -0
  91. package/src/fonts-google.ts +397 -0
  92. package/src/fonts.ts +147 -0
  93. package/src/fs.ts +26 -0
  94. package/src/index.ts +1202 -0
  95. package/src/islands.ts +121 -0
  96. package/src/isr.ts +461 -0
  97. package/src/manifest.ts +366 -0
  98. package/src/plugins/css-modules.ts +0 -0
  99. package/src/plugins/kit.ts +123 -0
  100. package/src/plugins/scss.ts +108 -0
  101. package/src/prerender.ts +209 -0
  102. package/src/query.ts +250 -0
  103. package/src/router.ts +676 -0
  104. 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
+ }