@nifrajs/web 0.1.0-alpha.1 → 0.1.0-alpha.3
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/README.md +8 -0
- package/dist/build.d.ts +252 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +755 -6
- package/dist/build.js.map +1 -1
- package/dist/chunk-names.d.ts +4 -0
- package/dist/chunk-names.d.ts.map +1 -0
- package/dist/chunk-names.js +46 -0
- package/dist/chunk-names.js.map +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +19 -2
- package/dist/client.js.map +1 -1
- package/dist/dev.d.ts +1 -1
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +2 -1
- package/dist/dev.js.map +1 -1
- package/dist/fonts-google.d.ts +2 -1
- package/dist/fonts-google.d.ts.map +1 -1
- package/dist/fonts-google.js.map +1 -1
- package/dist/fonts.d.ts +2 -1
- package/dist/fonts.d.ts.map +1 -1
- package/dist/fonts.js +0 -16
- package/dist/fonts.js.map +1 -1
- package/dist/index.d.ts +138 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +268 -23
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +82 -5
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js.map +1 -1
- package/dist/server-only.d.ts +21 -0
- package/dist/server-only.d.ts.map +1 -0
- package/dist/server-only.js +25 -0
- package/dist/server-only.js.map +1 -0
- package/dist/vite.d.ts +21 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +68 -4
- package/dist/vite.js.map +1 -1
- package/package.json +7 -2
- package/src/build.ts +1066 -12
- package/src/chunk-names.ts +45 -0
- package/src/client.ts +19 -2
- package/src/dev.ts +3 -2
- package/src/fonts-google.ts +3 -2
- package/src/fonts.ts +2 -1
- package/src/index.ts +373 -32
- package/src/manifest.ts +84 -6
- package/src/server-only.ts +25 -0
- package/src/vite.ts +82 -5
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL-safe output chunk naming. Bun names a code-split chunk after its source file's basename, so a
|
|
3
|
+
* dynamic-route file like `[slug].tsx` emits `[slug]-<hash>.js` — the `[ ]` make a URL a static server
|
|
4
|
+
* (server-bun, CF Pages) reject with 400, so the lazy import fails and the route silently never
|
|
5
|
+
* hydrates (SSR looks perfect; all interactivity is dead). `sanitizeOutputNames` renames every output
|
|
6
|
+
* whose basename isn't URL-path-safe (`[slug]` → `_slug_`) and rewrites the references to it inside the
|
|
7
|
+
* other JS/CSS chunks (where the bootstrap's lazy `import("…/[slug]-hash.js")` lives). The returned
|
|
8
|
+
* unsafe→safe basename map lets the build manifest point at the renamed files.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, renameSync, writeFileSync } from "node:fs"
|
|
11
|
+
|
|
12
|
+
const basename = (path: string): string => path.slice(path.lastIndexOf("/") + 1)
|
|
13
|
+
const URL_UNSAFE_NAME = /[^A-Za-z0-9._-]/g
|
|
14
|
+
|
|
15
|
+
export function sanitizeOutputNames(
|
|
16
|
+
outputs: readonly { readonly path: string }[],
|
|
17
|
+
): Map<string, string> {
|
|
18
|
+
const renames = new Map<string, string>()
|
|
19
|
+
for (const out of outputs) {
|
|
20
|
+
const base = basename(out.path)
|
|
21
|
+
const safe = base.replace(URL_UNSAFE_NAME, "_")
|
|
22
|
+
if (safe !== base) renames.set(base, safe)
|
|
23
|
+
}
|
|
24
|
+
if (renames.size === 0) return renames
|
|
25
|
+
// The hashed basename is a unique token, so replacing it literally inside the other chunks is safe.
|
|
26
|
+
for (const out of outputs) {
|
|
27
|
+
if (!out.path.endsWith(".js") && !out.path.endsWith(".css")) continue
|
|
28
|
+
let text = readFileSync(out.path, "utf8")
|
|
29
|
+
let changed = false
|
|
30
|
+
for (const [from, to] of renames) {
|
|
31
|
+
if (text.includes(from)) {
|
|
32
|
+
text = text.split(from).join(to)
|
|
33
|
+
changed = true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (changed) writeFileSync(out.path, text)
|
|
37
|
+
}
|
|
38
|
+
for (const out of outputs) {
|
|
39
|
+
const safe = renames.get(basename(out.path))
|
|
40
|
+
if (safe !== undefined) {
|
|
41
|
+
renameSync(out.path, out.path.slice(0, out.path.lastIndexOf("/") + 1) + safe)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return renames
|
|
45
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -186,14 +186,31 @@ export function installHistory(
|
|
|
186
186
|
export function applyHead(head: Meta): void {
|
|
187
187
|
if (head.title !== undefined) document.title = head.title
|
|
188
188
|
for (const el of document.head.querySelectorAll("[data-nifra]")) el.remove()
|
|
189
|
-
|
|
189
|
+
// Values follow the same HTML attribute conventions as the SSR `tagAttrs`: a string sets the value,
|
|
190
|
+
// `true` sets the bare boolean attribute, `false`/`undefined` skip it — so a soft-nav head matches
|
|
191
|
+
// the server-rendered one exactly (no hydration drift on the managed tags).
|
|
192
|
+
const add = (tag: "meta" | "link", attrs: Record<string, string | boolean | undefined>): void => {
|
|
190
193
|
const el = document.createElement(tag)
|
|
191
|
-
for (const [name, value] of Object.entries(attrs))
|
|
194
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
195
|
+
if (value === undefined || value === false) continue
|
|
196
|
+
el.setAttribute(name, value === true ? "" : value)
|
|
197
|
+
}
|
|
192
198
|
el.setAttribute("data-nifra", "")
|
|
193
199
|
document.head.appendChild(el)
|
|
194
200
|
}
|
|
195
201
|
for (const m of head.meta ?? []) add("meta", m)
|
|
196
202
|
for (const l of head.link ?? []) add("link", l)
|
|
203
|
+
// `<script>` slot (JSON-LD etc.) — soft-nav parity with the SSR `headTags` script render. Set the body
|
|
204
|
+
// via `textContent`, NOT `innerHTML`: `textContent` assigns the raw string without HTML re-parsing, so
|
|
205
|
+
// a `</script>` (or `<!--`/`]]>`) payload in the JSON-LD can't break out — the DOM-native equivalent of
|
|
206
|
+
// the server's `escapeScriptContent`. `type` defaults to `application/ld+json` (matching the SSR side).
|
|
207
|
+
for (const s of head.script ?? []) {
|
|
208
|
+
const el = document.createElement("script")
|
|
209
|
+
el.setAttribute("type", s.type ?? "application/ld+json")
|
|
210
|
+
el.textContent = s.content
|
|
211
|
+
el.setAttribute("data-nifra", "")
|
|
212
|
+
document.head.appendChild(el)
|
|
213
|
+
}
|
|
197
214
|
}
|
|
198
215
|
|
|
199
216
|
export function installForms(router: ClientRouter): () => void {
|
package/src/dev.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { readdirSync, unwatchFile, watchFile } from "node:fs"
|
|
9
9
|
import { type BuildClientOptions, buildClient } from "./build.ts"
|
|
10
|
+
import { DEFAULT_DEV_PORT } from "./index.ts"
|
|
10
11
|
|
|
11
12
|
/** Minimal app surface the dev server needs — `createWebApp(...)` satisfies it. */
|
|
12
13
|
interface FetchApp {
|
|
@@ -22,7 +23,7 @@ export interface DevServerOptions extends Omit<BuildClientOptions, "minify"> {
|
|
|
22
23
|
readonly createApp: (clientEntry: string, importQuery: string) => FetchApp | Promise<FetchApp>
|
|
23
24
|
/** Directories to watch (default: `[routesDir]`). */
|
|
24
25
|
readonly watch?: readonly string[]
|
|
25
|
-
/** Port to listen on (default
|
|
26
|
+
/** Port to listen on (default {@link DEFAULT_DEV_PORT}). */
|
|
26
27
|
readonly port?: number
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -38,7 +39,7 @@ const reloadClient = `<script>(()=>{const x=()=>{const w=new WebSocket((location
|
|
|
38
39
|
|
|
39
40
|
/** Start the dev server: build → serve → watch → rebuild + reload on change. */
|
|
40
41
|
export async function createDevServer(options: DevServerOptions): Promise<DevServer> {
|
|
41
|
-
const { createApp, port =
|
|
42
|
+
const { createApp, port = DEFAULT_DEV_PORT, outDir, routesDir } = options
|
|
42
43
|
const publicPath = options.publicPath ?? "/assets/"
|
|
43
44
|
let version = 0
|
|
44
45
|
const rebuild = async (): Promise<FetchApp> => {
|
package/src/fonts-google.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { writeFile as fsWriteFile, mkdir } from "node:fs/promises"
|
|
25
25
|
import { dirname, join } from "node:path"
|
|
26
26
|
import { type FontDisplay, fontFace, fontPreload } from "./fonts.ts"
|
|
27
|
+
import type { LinkDescriptor } from "./manifest.ts"
|
|
27
28
|
|
|
28
29
|
/** Options describing the Google font to fetch + self-host. */
|
|
29
30
|
export interface GoogleFontOptions {
|
|
@@ -82,7 +83,7 @@ export interface LoadGoogleFontResult {
|
|
|
82
83
|
readonly assets: readonly FontAsset[]
|
|
83
84
|
/** `fontPreload()` link-attribute sets — spread the ones you want into a layout's `meta.link`.
|
|
84
85
|
* Preloading *every* weight/subset is wasteful; usually preload just the primary subset + weight. */
|
|
85
|
-
readonly preloads: readonly
|
|
86
|
+
readonly preloads: readonly LinkDescriptor[]
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
export interface LoadGoogleFontIO {
|
|
@@ -340,7 +341,7 @@ export async function loadGoogleFont(
|
|
|
340
341
|
|
|
341
342
|
const assets: FontAsset[] = []
|
|
342
343
|
const rules: string[] = []
|
|
343
|
-
const preloads:
|
|
344
|
+
const preloads: LinkDescriptor[] = []
|
|
344
345
|
|
|
345
346
|
for (const face of faces) {
|
|
346
347
|
// Prefer woff2; the CSS2 endpoint with a Chrome UA only ever returns woff2, but be defensive.
|
package/src/fonts.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* // a root layout's meta — becomes a <link rel="preload" as="font"> in <head>
|
|
15
15
|
* export const meta = { link: [fontPreload({ href: "/fonts/inter.woff2" })] }
|
|
16
16
|
*/
|
|
17
|
+
import type { LinkDescriptor } from "./manifest.ts"
|
|
17
18
|
|
|
18
19
|
/** `font-display` strategy. `swap` (the default here) paints fallback text immediately, then swaps. */
|
|
19
20
|
export type FontDisplay = "auto" | "block" | "swap" | "fallback" | "optional"
|
|
@@ -135,7 +136,7 @@ export interface FontPreloadInput {
|
|
|
135
136
|
* escaped at injection by the head renderer. Preloading the font file removes a render-blocking round
|
|
136
137
|
* trip (the browser would otherwise discover the font only after parsing the CSS).
|
|
137
138
|
*/
|
|
138
|
-
export function fontPreload(input: FontPreloadInput):
|
|
139
|
+
export function fontPreload(input: FontPreloadInput): LinkDescriptor {
|
|
139
140
|
const type = input.type ?? MIME_BY_EXT[extOf(input.href)]
|
|
140
141
|
return {
|
|
141
142
|
rel: "preload",
|