@nifrajs/web 5.0.0 → 5.2.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/CHANGELOG.md +2970 -0
- package/README.md +8 -0
- package/dist/build-plan.d.ts +7 -0
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js +19 -0
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts +1 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +51 -3
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +12 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +43 -6
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +7 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +39 -22
- package/dist/client.js.map +1 -1
- package/dist/dev-port.d.ts +3 -2
- package/dist/dev-port.d.ts.map +1 -1
- package/dist/dev-port.js +2 -6
- package/dist/dev-port.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/codegen.d.ts +6 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +23 -5
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/copy-split.d.ts +27 -0
- package/dist/internal/copy-split.d.ts.map +1 -0
- package/dist/internal/copy-split.js +173 -0
- package/dist/internal/copy-split.js.map +1 -0
- package/dist/internal/package-entries.d.ts +34 -0
- package/dist/internal/package-entries.d.ts.map +1 -0
- package/dist/internal/package-entries.js +324 -0
- package/dist/internal/package-entries.js.map +1 -0
- package/dist/internal/page-execution.d.ts +19 -1
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +123 -14
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +15 -2
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/private-env.d.ts +8 -0
- package/dist/internal/private-env.d.ts.map +1 -1
- package/dist/internal/private-env.js +25 -10
- package/dist/internal/private-env.js.map +1 -1
- package/dist/internal/render-document.d.ts +15 -46
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +59 -62
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/route-path.d.ts +6 -0
- package/dist/internal/route-path.d.ts.map +1 -1
- package/dist/internal/route-path.js +11 -0
- package/dist/internal/route-path.js.map +1 -1
- package/dist/internal/secret-scan.d.ts.map +1 -1
- package/dist/internal/secret-scan.js +103 -1
- package/dist/internal/secret-scan.js.map +1 -1
- package/dist/internal/typescript-api.d.ts +4 -0
- package/dist/internal/typescript-api.d.ts.map +1 -0
- package/dist/internal/typescript-api.js +8 -0
- package/dist/internal/typescript-api.js.map +1 -0
- package/dist/internal/vite-plugin-compat.d.ts +28 -0
- package/dist/internal/vite-plugin-compat.d.ts.map +1 -0
- package/dist/internal/vite-plugin-compat.js +82 -0
- package/dist/internal/vite-plugin-compat.js.map +1 -0
- package/dist/internal/web-app.d.ts +38 -2
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +30 -3
- package/dist/internal/web-app.js.map +1 -1
- package/dist/internal/without-query.d.ts +4 -0
- package/dist/internal/without-query.d.ts.map +1 -0
- package/dist/internal/without-query.js +9 -0
- package/dist/internal/without-query.js.map +1 -0
- package/dist/internal/zone-graph.d.ts.map +1 -1
- package/dist/internal/zone-graph.js +17 -6
- package/dist/internal/zone-graph.js.map +1 -1
- package/dist/isr.d.ts +1 -1
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +15 -2
- package/dist/isr.js.map +1 -1
- package/dist/manifest.d.ts +9 -0
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +1 -0
- package/dist/manifest.js.map +1 -1
- package/dist/plugins/import-glob.d.ts +20 -0
- package/dist/plugins/import-glob.d.ts.map +1 -0
- package/dist/plugins/import-glob.js +401 -0
- package/dist/plugins/import-glob.js.map +1 -0
- package/dist/plugins/kit.d.ts +11 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +32 -0
- package/dist/plugins/kit.js.map +1 -1
- package/dist/plugins/stylex.d.ts.map +1 -1
- package/dist/plugins/stylex.js +2 -2
- package/dist/plugins/stylex.js.map +1 -1
- package/dist/plugins/vite-leak-guard.d.ts +6 -0
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
- package/dist/plugins/vite-leak-guard.js +80 -7
- package/dist/plugins/vite-leak-guard.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
- package/dist/plugins/vite-zone-guard.js +17 -21
- package/dist/plugins/vite-zone-guard.js.map +1 -1
- package/dist/plugins/zone-guard.d.ts.map +1 -1
- package/dist/plugins/zone-guard.js +44 -3
- package/dist/plugins/zone-guard.js.map +1 -1
- package/dist/prerender.d.ts +5 -1
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +41 -19
- package/dist/prerender.js.map +1 -1
- package/dist/render-seam.d.ts +60 -2
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +33 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/router.d.ts +3 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +13 -9
- package/dist/router.js.map +1 -1
- package/dist/seo.d.ts +49 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +48 -0
- package/dist/seo.js.map +1 -0
- package/dist/server-components/typescript.d.ts.map +1 -1
- package/dist/server-components/typescript.js +2 -7
- package/dist/server-components/typescript.js.map +1 -1
- package/dist/vite.d.ts +1 -28
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +3 -69
- package/dist/vite.js.map +1 -1
- package/dist/zones.d.ts +30 -8
- package/dist/zones.d.ts.map +1 -1
- package/dist/zones.js +185 -46
- package/dist/zones.js.map +1 -1
- package/package.json +10 -4
- package/src/build-plan.ts +21 -0
- package/src/build-vite.ts +60 -3
- package/src/build.ts +57 -4
- package/src/client.ts +42 -22
- package/src/dev-port.ts +4 -10
- package/src/index.ts +8 -4
- package/src/internal/codegen.ts +32 -3
- package/src/internal/copy-split.ts +220 -0
- package/src/internal/package-entries.ts +331 -0
- package/src/internal/page-execution.ts +150 -13
- package/src/internal/parity.ts +15 -2
- package/src/internal/private-env.ts +27 -13
- package/src/internal/render-document.ts +100 -81
- package/src/internal/route-path.ts +16 -0
- package/src/internal/secret-scan.ts +101 -1
- package/src/internal/typescript-api.ts +11 -0
- package/src/internal/vite-plugin-compat.ts +81 -0
- package/src/internal/web-app.ts +71 -6
- package/src/internal/without-query.ts +8 -0
- package/src/internal/zone-graph.ts +19 -6
- package/src/isr.ts +13 -2
- package/src/manifest.ts +10 -0
- package/src/plugins/import-glob.ts +392 -0
- package/src/plugins/kit.ts +36 -0
- package/src/plugins/stylex.ts +2 -1
- package/src/plugins/vite-leak-guard.ts +99 -7
- package/src/plugins/vite-zone-guard.ts +21 -19
- package/src/plugins/zone-guard.ts +41 -2
- package/src/prerender.ts +39 -18
- package/src/render-seam.ts +88 -2
- package/src/router.ts +16 -11
- package/src/seo.ts +68 -0
- package/src/server-components/typescript.ts +2 -10
- package/src/vite.ts +3 -81
- package/src/zones.ts +237 -52
package/src/build-vite.ts
CHANGED
|
@@ -39,15 +39,18 @@ import {
|
|
|
39
39
|
} from "./css-contract.ts"
|
|
40
40
|
import { discoverRoutes } from "./fs.ts"
|
|
41
41
|
import { generateClientEntry, generateServerManifest } from "./index.ts"
|
|
42
|
+
import { loadCopySplit } from "./internal/copy-split.ts"
|
|
42
43
|
import { viteDedupePackages } from "./internal/identity-policy.ts"
|
|
43
44
|
import {
|
|
44
45
|
assertDevelopmentProductionParity,
|
|
45
46
|
assertIdentityParity,
|
|
46
47
|
collectDevelopmentParityInput,
|
|
47
48
|
} from "./internal/parity.ts"
|
|
49
|
+
import { emittedScanFiles, formatSecretFindings, scanForSecrets } from "./internal/secret-scan.ts"
|
|
48
50
|
import { vitePublicEnvPrefix } from "./internal/server-boundary.ts"
|
|
49
51
|
import { unsupportedBuiltin } from "./internal/target-compat.ts"
|
|
50
52
|
import { importVite, isViteUnresolved } from "./internal/vite-import.ts"
|
|
53
|
+
import { normalizeRolldownPlugins } from "./internal/vite-plugin-compat.ts"
|
|
51
54
|
import { scopedName } from "./plugins/css-modules.ts"
|
|
52
55
|
import { reproduciblePath } from "./plugins/kit.ts"
|
|
53
56
|
import {
|
|
@@ -73,6 +76,7 @@ interface ViteManifestEntry {
|
|
|
73
76
|
type ViteBuildManifest = Readonly<Record<string, ViteManifestEntry>>
|
|
74
77
|
|
|
75
78
|
interface ViteModule {
|
|
79
|
+
readonly rolldownVersion?: string
|
|
76
80
|
build(config: Record<string, unknown>): Promise<unknown>
|
|
77
81
|
loadEnv(mode: string, envDir: string, prefixes: string): Record<string, string>
|
|
78
82
|
}
|
|
@@ -166,13 +170,21 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
|
|
|
166
170
|
mkdirSync(outDir, { recursive: true })
|
|
167
171
|
|
|
168
172
|
const routeManifest = discoverRoutes(routesDir)
|
|
173
|
+
const copySplit =
|
|
174
|
+
(options.copySplit ?? options.minify !== false)
|
|
175
|
+
? await loadCopySplit(root, { skip: [outDir] })
|
|
176
|
+
: undefined
|
|
169
177
|
// The generated bootstrap, written beside the project so its imports resolve the app's deps (same
|
|
170
178
|
// reasoning as buildClient). A unique dir avoids clobbering a user file / colliding with parallel builds.
|
|
171
179
|
const entryDir = mkdtempSync(resolvePath(dirname(routesDir), ".nifra-vite-client-"))
|
|
172
180
|
const entryFile = `${entryDir}/_nifra-entry.ts`
|
|
173
181
|
writeFileSync(
|
|
174
182
|
entryFile,
|
|
175
|
-
`globalThis.process ??= { env: {} };\n${generateClientEntry(routeManifest, {
|
|
183
|
+
`globalThis.process ??= { env: {} };\n${generateClientEntry(routeManifest, {
|
|
184
|
+
clientModule,
|
|
185
|
+
resolve,
|
|
186
|
+
copyChunks: copySplit !== undefined,
|
|
187
|
+
})}`,
|
|
176
188
|
)
|
|
177
189
|
|
|
178
190
|
// Every unique route/layout/_404 file - each an additional entry so Vite emits a named chunk per file
|
|
@@ -274,7 +286,28 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
|
|
|
274
286
|
},
|
|
275
287
|
// First, so a bare built-in is named before Vite turns it into an anonymous stub.
|
|
276
288
|
// The asset URL guard last, so it reads each module after any framework plugin made it JavaScript.
|
|
277
|
-
plugins: [
|
|
289
|
+
plugins: [
|
|
290
|
+
viteBareBuiltinExternal(),
|
|
291
|
+
...(copySplit === undefined
|
|
292
|
+
? []
|
|
293
|
+
: [
|
|
294
|
+
{
|
|
295
|
+
name: "nifra-copy-split",
|
|
296
|
+
// Before a framework plugin compiles the module: the tables are read from its source.
|
|
297
|
+
enforce: "pre" as const,
|
|
298
|
+
transform(code: string, id: string) {
|
|
299
|
+
const out = copySplit.rewrite(id, code)
|
|
300
|
+
// Lines do not move, so the module's map stays the source's.
|
|
301
|
+
return out === undefined ? undefined : { code: out, map: null }
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
]),
|
|
305
|
+
...normalizeRolldownPlugins(
|
|
306
|
+
options.vitePlugins ?? [],
|
|
307
|
+
vite.rolldownVersion !== undefined,
|
|
308
|
+
),
|
|
309
|
+
assetUrlGuard,
|
|
310
|
+
],
|
|
278
311
|
worker: {
|
|
279
312
|
plugins: () => {
|
|
280
313
|
const guard = viteLeakGuard(guardOptions)
|
|
@@ -416,6 +449,23 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
|
|
|
416
449
|
for (const asset of entry.assets ?? []) assets.add(url(asset))
|
|
417
450
|
}
|
|
418
451
|
|
|
452
|
+
// The leak guard scanned the bundle; the copy it no longer holds is scanned here.
|
|
453
|
+
const copy = copySplit?.chunks(publicPath)
|
|
454
|
+
if (copy !== undefined && copy.files.length > 0) {
|
|
455
|
+
const secrets = formatSecretFindings(
|
|
456
|
+
scanForSecrets({
|
|
457
|
+
artifacts: copy.files.flatMap((file) => emittedScanFiles(file.name, file.text)),
|
|
458
|
+
env: guardOptions.secrets.env,
|
|
459
|
+
publicEnvPrefix: options.publicEnvPrefix ?? "PUBLIC_",
|
|
460
|
+
exemptions: exemptions ?? [],
|
|
461
|
+
}),
|
|
462
|
+
)
|
|
463
|
+
if (secrets !== undefined) throw new Error(secrets)
|
|
464
|
+
for (const file of copy.files) writeFileSync(join(outDir, file.name), file.text)
|
|
465
|
+
}
|
|
466
|
+
const copyChunks = copy?.urls ?? {}
|
|
467
|
+
for (const chunk of Object.values(copyChunks)) assets.add(chunk)
|
|
468
|
+
|
|
419
469
|
const publicFiles =
|
|
420
470
|
publicDir !== undefined && existsSync(publicDir) ? await copyPublicDir(publicDir, outDir) : []
|
|
421
471
|
|
|
@@ -428,6 +478,7 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
|
|
|
428
478
|
...(publicFiles.length > 0 ? { publicFiles } : {}),
|
|
429
479
|
...(css.length > 0 ? { css } : {}),
|
|
430
480
|
...(Object.keys(routeStyles).length > 0 ? { routeStyles } : {}),
|
|
481
|
+
...(Object.keys(copyChunks).length > 0 ? { copyChunks } : {}),
|
|
431
482
|
}
|
|
432
483
|
assertDevelopmentProductionParity(collectDevelopmentParityInput(routesDir, publicDir), manifest)
|
|
433
484
|
writeFileSync(`${outDir}/manifest.json`, JSON.stringify(manifest, null, 2))
|
|
@@ -442,6 +493,7 @@ export interface BuildServerViteOptions {
|
|
|
442
493
|
readonly clientEntry: string
|
|
443
494
|
readonly styles?: readonly string[] | undefined
|
|
444
495
|
readonly routeStyles?: Readonly<Record<string, readonly string[]>> | undefined
|
|
496
|
+
readonly copyChunks?: Readonly<Record<string, string>> | undefined
|
|
445
497
|
/** How framework-owned stylesheet links should activate before hydration (default `"blocking"`). */
|
|
446
498
|
readonly cssLoading?: CssLoadingMode
|
|
447
499
|
readonly resolve?: (file: string) => string
|
|
@@ -563,6 +615,7 @@ export async function buildServerVite(options: BuildServerViteOptions): Promise<
|
|
|
563
615
|
styles,
|
|
564
616
|
routeStyles,
|
|
565
617
|
...(cssLoading !== undefined ? { cssLoading } : {}),
|
|
618
|
+
...(options.copyChunks !== undefined ? { copyChunks: options.copyChunks } : {}),
|
|
566
619
|
}),
|
|
567
620
|
)
|
|
568
621
|
|
|
@@ -599,7 +652,10 @@ export async function buildServerVite(options: BuildServerViteOptions): Promise<
|
|
|
599
652
|
dedupe: ["react", "react-dom", ...singleCopyPackageNames(root)],
|
|
600
653
|
conditions: [...conditions],
|
|
601
654
|
},
|
|
602
|
-
plugins:
|
|
655
|
+
plugins: normalizeRolldownPlugins(
|
|
656
|
+
options.vitePlugins ?? [],
|
|
657
|
+
vite.rolldownVersion !== undefined,
|
|
658
|
+
),
|
|
603
659
|
build: {
|
|
604
660
|
outDir,
|
|
605
661
|
emptyOutDir: false,
|
|
@@ -675,6 +731,7 @@ export const viteBundler: Bundler = {
|
|
|
675
731
|
target: input.target,
|
|
676
732
|
...(input.styles !== undefined ? { styles: input.styles } : {}),
|
|
677
733
|
...(input.routeStyles !== undefined ? { routeStyles: input.routeStyles } : {}),
|
|
734
|
+
...(input.copyChunks !== undefined ? { copyChunks: input.copyChunks } : {}),
|
|
678
735
|
...(input.plugins ? { vitePlugins: input.plugins } : {}),
|
|
679
736
|
...(input.define ? { define: input.define } : {}),
|
|
680
737
|
...(input.cssLoading !== undefined ? { cssLoading: input.cssLoading } : {}),
|
package/src/build.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
formatServerOnlyLeak,
|
|
40
40
|
parseBuildTarget,
|
|
41
41
|
parseManifestClientEntry,
|
|
42
|
+
parseManifestCopyChunks,
|
|
42
43
|
parseManifestCssLoading,
|
|
43
44
|
parseManifestRouteStyles,
|
|
44
45
|
parseManifestStyles,
|
|
@@ -50,6 +51,7 @@ import { sanitizeOutputNames } from "./chunk-names.ts"
|
|
|
50
51
|
import { type CssLoadingMode, normalizeCssCodeSplit, normalizeCssLoading } from "./css-contract.ts"
|
|
51
52
|
import { discoverRoutes } from "./fs.ts"
|
|
52
53
|
import { generateClientEntry, generateServerManifest } from "./index.ts"
|
|
54
|
+
import { loadCopySplit } from "./internal/copy-split.ts"
|
|
53
55
|
import { assertDisposableDir } from "./internal/disposable-dir.ts"
|
|
54
56
|
import { entryChunks } from "./internal/entry-chunks.ts"
|
|
55
57
|
import { dedupePolicyFor } from "./internal/identity-policy.ts"
|
|
@@ -89,6 +91,7 @@ import {
|
|
|
89
91
|
verifyServerGraph,
|
|
90
92
|
} from "./internal/zone-graph.ts"
|
|
91
93
|
import { type ClientModuleGraph, fromBunMetafile } from "./module-graph.ts"
|
|
94
|
+
import { assertNoImportGlob, importGlobPlugin } from "./plugins/import-glob.ts"
|
|
92
95
|
import { zoneGuardPlugin } from "./plugins/zone-guard.ts"
|
|
93
96
|
// `buildTarget(static)` drives the SSG prerender engine directly (it's also re-exported below).
|
|
94
97
|
import { prerenderRoutes } from "./prerender.ts"
|
|
@@ -186,6 +189,12 @@ export interface BuildClientOptions {
|
|
|
186
189
|
* variable. Each names its rule, its file (or, for an environment value, its variable) and a reason.
|
|
187
190
|
*/
|
|
188
191
|
readonly secretExemptions?: readonly SecretExemption[]
|
|
192
|
+
/**
|
|
193
|
+
* Move copy tables (`@nifrajs/i18n/copy`) into one chunk per locale, so a page's JavaScript carries
|
|
194
|
+
* only the copy of the locale it renders in (default: on when minifying). The server bundle keeps
|
|
195
|
+
* every table whole; the server entry passes the manifest's `copyChunks` to `createWebApp`.
|
|
196
|
+
*/
|
|
197
|
+
readonly copySplit?: boolean
|
|
189
198
|
}
|
|
190
199
|
|
|
191
200
|
/**
|
|
@@ -374,12 +383,14 @@ export function resyncServerManifestSource(
|
|
|
374
383
|
routesPrefix: string,
|
|
375
384
|
): string {
|
|
376
385
|
const cssLoading = parseManifestCssLoading(source)
|
|
386
|
+
const copyChunks = parseManifestCopyChunks(source)
|
|
377
387
|
return generateServerManifest(manifest, {
|
|
378
388
|
resolve: (file) => `${routesPrefix}${file}`,
|
|
379
389
|
clientEntry: parseManifestClientEntry(source) ?? "",
|
|
380
390
|
styles: parseManifestStyles(source),
|
|
381
391
|
routeStyles: parseManifestRouteStyles(source),
|
|
382
392
|
...(cssLoading !== undefined ? { cssLoading } : {}),
|
|
393
|
+
...(copyChunks !== undefined ? { copyChunks } : {}),
|
|
383
394
|
lazy: MANIFEST_IS_LAZY.test(source),
|
|
384
395
|
})
|
|
385
396
|
}
|
|
@@ -426,6 +437,10 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
426
437
|
// The bootstrap's filename is `_nifra`-namespaced (not `entry.ts`) so its `[name]` can't collide with
|
|
427
438
|
// a user route named `entry.tsx` when the CSS mapping below excludes the bootstrap's aggregate CSS.
|
|
428
439
|
const mode = options.minify === false ? "development" : "production"
|
|
440
|
+
const copySplit =
|
|
441
|
+
(options.copySplit ?? mode === "production")
|
|
442
|
+
? await loadCopySplit(root, { skip: [outDir] })
|
|
443
|
+
: undefined
|
|
429
444
|
// PUBLIC_*-prefixed env → client define (Vite/Next convention). Sourced from the build env (`Bun.env`,
|
|
430
445
|
// falling back to `process.env`). Only prefixed vars are baked in; unprefixed secrets stay undefined
|
|
431
446
|
// in the client via the bare `process.env` → `({})` define below. Caller's `options.define` wins (it's
|
|
@@ -442,7 +457,11 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
442
457
|
// an app module doesn't crash hydration (`process.env.*` reads are handled at compile time by `define`).
|
|
443
458
|
writeFileSync(
|
|
444
459
|
entryFile,
|
|
445
|
-
`globalThis.process ??= { env: {} };\n${generateClientEntry(routeManifest, {
|
|
460
|
+
`globalThis.process ??= { env: {} };\n${generateClientEntry(routeManifest, {
|
|
461
|
+
clientModule,
|
|
462
|
+
resolve,
|
|
463
|
+
copyChunks: copySplit !== undefined,
|
|
464
|
+
})}`,
|
|
446
465
|
)
|
|
447
466
|
|
|
448
467
|
// Every unique route/layout/`_404` file (sorted, stable), as ADDITIONAL entrypoints - Bun emits a
|
|
@@ -496,6 +515,10 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
496
515
|
svelteDedupePlugin(routesDir),
|
|
497
516
|
serverFnStubPlugin(),
|
|
498
517
|
...(options.plugins ?? []),
|
|
518
|
+
// Before the copy split: it claims a module that calls the glob and reads it through the split.
|
|
519
|
+
importGlobPlugin({ root }),
|
|
520
|
+
// Last: it rewrites a module no other plugin claims, and the ones that do read through it.
|
|
521
|
+
...(copySplit === undefined ? [] : [copySplit.plugin]),
|
|
499
522
|
],
|
|
500
523
|
...(options.conditions ? { conditions: [...options.conditions] } : {}),
|
|
501
524
|
// Replace `process.env.*` at compile time so an app module reading config off `process.env` doesn't
|
|
@@ -550,6 +573,11 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
550
573
|
return { name: out.path, kind: "code", text: await out.text() }
|
|
551
574
|
}),
|
|
552
575
|
)
|
|
576
|
+
assertNoImportGlob(
|
|
577
|
+
emitted.flatMap((file) =>
|
|
578
|
+
file.kind === "code" ? [[relative(cwd, file.name), file.text ?? ""] as const] : [],
|
|
579
|
+
),
|
|
580
|
+
)
|
|
553
581
|
const clientGraph = withEmittedImports(fromBunMetafile(clientMeta), emitted, publicPath)
|
|
554
582
|
const verdict = verifyClientGraph(clientGraph, {
|
|
555
583
|
classifier,
|
|
@@ -596,6 +624,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
596
624
|
if (unaccounted !== undefined) throw new Error(unaccounted)
|
|
597
625
|
|
|
598
626
|
const publicDir = options.publicDir === false ? undefined : (options.publicDir ?? "public")
|
|
627
|
+
const copy = copySplit?.chunks(publicPath)
|
|
599
628
|
const { sources, originsOf } = graphScanInput(clientGraph, bunModuleSource(cwd), classifier)
|
|
600
629
|
const secrets = formatSecretFindings(
|
|
601
630
|
scanForSecrets({
|
|
@@ -606,6 +635,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
606
635
|
const name = file.name.replace(/^\.\//, "")
|
|
607
636
|
return emittedScanFiles(name, file.text, originsOf(name))
|
|
608
637
|
}),
|
|
638
|
+
...(copy?.files ?? []).flatMap((file) => emittedScanFiles(file.name, file.text)),
|
|
609
639
|
...(publicDir !== undefined && existsSync(publicDir)
|
|
610
640
|
? publicScanFiles(publicDir, originName(root, resolvePath(publicDir)))
|
|
611
641
|
: []),
|
|
@@ -624,6 +654,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
624
654
|
return { kind: out.kind, path }
|
|
625
655
|
}),
|
|
626
656
|
)
|
|
657
|
+
for (const file of copy?.files ?? []) writeFileSync(join(outDir, file.name), file.text)
|
|
627
658
|
|
|
628
659
|
// Rename any chunk whose basename isn't URL-safe (dynamic-route files become `[slug]-hash.js`) and
|
|
629
660
|
// rewrite the references - otherwise the lazy import 404s and the route silently never hydrates.
|
|
@@ -750,13 +781,15 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
750
781
|
const publicFiles =
|
|
751
782
|
publicDir !== undefined && existsSync(publicDir) ? await copyPublicDir(publicDir, outDir) : []
|
|
752
783
|
|
|
784
|
+
const copyChunks = copy?.urls ?? {}
|
|
753
785
|
const manifest: BuildManifest = {
|
|
754
786
|
entry: entryUrl,
|
|
755
|
-
assets: outputs.map((o) => toUrl(o.path)),
|
|
787
|
+
assets: [...outputs.map((o) => toUrl(o.path)), ...Object.values(copyChunks)],
|
|
756
788
|
routes,
|
|
757
789
|
...(publicFiles.length > 0 ? { publicFiles } : {}),
|
|
758
790
|
...(css.length > 0 ? { css } : {}),
|
|
759
791
|
...(Object.keys(routeStyles).length > 0 ? { routeStyles } : {}),
|
|
792
|
+
...(Object.keys(copyChunks).length > 0 ? { copyChunks } : {}),
|
|
760
793
|
}
|
|
761
794
|
assertDevelopmentProductionParity(collectDevelopmentParityInput(routesDir, publicDir), manifest)
|
|
762
795
|
writeFileSync(`${outDir}/manifest.json`, JSON.stringify(manifest, null, 2))
|
|
@@ -780,6 +813,8 @@ export interface BuildServerOptions {
|
|
|
780
813
|
readonly styles?: readonly string[] | undefined
|
|
781
814
|
/** Per-route stylesheet URLs (`buildClient`'s `BuildManifest.routeStyles`) - baked alongside `styles`. */
|
|
782
815
|
readonly routeStyles?: Readonly<Record<string, readonly string[]>> | undefined
|
|
816
|
+
/** Copy chunk URLs by locale (`buildClient`'s `BuildManifest.copyChunks`) - baked as `copyChunks`. */
|
|
817
|
+
readonly copyChunks?: Readonly<Record<string, string>> | undefined
|
|
783
818
|
/** How framework-owned stylesheet links should activate before hydration (default `"blocking"`). */
|
|
784
819
|
readonly cssLoading?: CssLoadingMode
|
|
785
820
|
/** Route/layout file → import specifier in the generated manifest (default: a relative path from the
|
|
@@ -1051,6 +1086,7 @@ function formatRefusedFiles(refused: ReadonlyMap<string, string>, root: string):
|
|
|
1051
1086
|
return `[nifra/web] the browser build reached code that may not ship to a browser:\n${lines.join("\n")}`
|
|
1052
1087
|
}
|
|
1053
1088
|
|
|
1089
|
+
export { importGlobPlugin } from "./plugins/import-glob.ts"
|
|
1054
1090
|
export { zoneGuardPlugin } from "./plugins/zone-guard.ts"
|
|
1055
1091
|
|
|
1056
1092
|
/**
|
|
@@ -1130,6 +1166,7 @@ export async function buildServer(options: BuildServerOptions): Promise<ServerBu
|
|
|
1130
1166
|
styles,
|
|
1131
1167
|
routeStyles,
|
|
1132
1168
|
...(cssLoading !== undefined ? { cssLoading } : {}),
|
|
1169
|
+
...(options.copyChunks !== undefined ? { copyChunks: options.copyChunks } : {}),
|
|
1133
1170
|
lazy,
|
|
1134
1171
|
}),
|
|
1135
1172
|
)
|
|
@@ -1161,6 +1198,7 @@ export async function buildServer(options: BuildServerOptions): Promise<ServerBu
|
|
|
1161
1198
|
reactDedupePlugin(entryDir),
|
|
1162
1199
|
preactDedupePlugin(entryDir),
|
|
1163
1200
|
...(options.plugins ?? []),
|
|
1201
|
+
importGlobPlugin({ root: resolvePath(options.root ?? dirname(routesDir)) }),
|
|
1164
1202
|
],
|
|
1165
1203
|
})
|
|
1166
1204
|
if (!result.success) {
|
|
@@ -1227,7 +1265,14 @@ export async function buildServer(options: BuildServerOptions): Promise<ServerBu
|
|
|
1227
1265
|
// ===================================================================================================
|
|
1228
1266
|
|
|
1229
1267
|
/** The `createWebApp` options a generated server entry can import from the app's framework module. */
|
|
1230
|
-
export const SERVER_ENTRY_OPTIONS = [
|
|
1268
|
+
export const SERVER_ENTRY_OPTIONS = [
|
|
1269
|
+
"apiPrefix",
|
|
1270
|
+
"apiStrip",
|
|
1271
|
+
"mounts",
|
|
1272
|
+
"csp",
|
|
1273
|
+
"nonce",
|
|
1274
|
+
"i18n",
|
|
1275
|
+
] as const
|
|
1231
1276
|
export type ServerEntryOption = (typeof SERVER_ENTRY_OPTIONS)[number]
|
|
1232
1277
|
/** Each importable option mapped to the specifier of the module that exports it. */
|
|
1233
1278
|
export type ServerEntryOptionImports = Readonly<Partial<Record<ServerEntryOption, string>>>
|
|
@@ -1285,6 +1330,8 @@ export function generateServerEntry(options: {
|
|
|
1285
1330
|
readonly hashedFiles?: readonly string[]
|
|
1286
1331
|
/** Trust the edge platform's client-address header for `c.clientIp`. See {@link ServerEntryClientIp}. */
|
|
1287
1332
|
readonly clientIp?: ServerEntryClientIp
|
|
1333
|
+
/** The server manifest exports `copyChunks` (the client build split copy tables by locale). */
|
|
1334
|
+
readonly copyChunks?: boolean
|
|
1288
1335
|
}): string {
|
|
1289
1336
|
const {
|
|
1290
1337
|
target,
|
|
@@ -1327,7 +1374,9 @@ export function generateServerEntry(options: {
|
|
|
1327
1374
|
lines.push(`import { backend } from ${JSON.stringify(backendImport)}`)
|
|
1328
1375
|
}
|
|
1329
1376
|
lines.push(
|
|
1330
|
-
|
|
1377
|
+
options.copyChunks === true
|
|
1378
|
+
? 'import { clientEntry, copyChunks, cssLoading, manifest, styles, routeStyles } from "./server-manifest"'
|
|
1379
|
+
: 'import { clientEntry, cssLoading, manifest, styles, routeStyles } from "./server-manifest"',
|
|
1331
1380
|
)
|
|
1332
1381
|
// Cloudflare needs the fetch-handler shape; the others call app.fetch directly.
|
|
1333
1382
|
if (target === "cloudflare") lines.push('import { toFetchHandler } from "@nifrajs/core/server"')
|
|
@@ -1343,6 +1392,7 @@ export function generateServerEntry(options: {
|
|
|
1343
1392
|
" styles,",
|
|
1344
1393
|
" routeStyles,",
|
|
1345
1394
|
" cssLoading,",
|
|
1395
|
+
...(options.copyChunks === true ? [" copyChunks,"] : []),
|
|
1346
1396
|
...(backendImport !== undefined ? [" api: inProcessClient(backend),"] : []),
|
|
1347
1397
|
` title: ${JSON.stringify(title)},`,
|
|
1348
1398
|
// Core's own `{ header }` trust, so the mounted backend and every loader see the derived caller.
|
|
@@ -1630,6 +1680,7 @@ export const bunBundler: Bundler = {
|
|
|
1630
1680
|
target: input.target,
|
|
1631
1681
|
...(input.styles !== undefined ? { styles: input.styles } : {}),
|
|
1632
1682
|
...(input.routeStyles !== undefined ? { routeStyles: input.routeStyles } : {}),
|
|
1683
|
+
...(input.copyChunks !== undefined ? { copyChunks: input.copyChunks } : {}),
|
|
1633
1684
|
...(input.plugins ? { plugins: input.plugins as BunPlugin[] } : {}),
|
|
1634
1685
|
...(input.define ? { define: input.define } : {}),
|
|
1635
1686
|
...(input.cssLoading !== undefined ? { cssLoading: input.cssLoading } : {}),
|
|
@@ -1813,6 +1864,7 @@ export async function buildTargetWith(
|
|
|
1813
1864
|
...(publicFiles.length > 0 ? { publicFiles } : {}),
|
|
1814
1865
|
hashedFiles: client.assets,
|
|
1815
1866
|
...(options.clientIp !== undefined ? { clientIp: options.clientIp } : {}),
|
|
1867
|
+
...(client.copyChunks !== undefined ? { copyChunks: true } : {}),
|
|
1816
1868
|
}),
|
|
1817
1869
|
)
|
|
1818
1870
|
const { worker } = await bundler.buildServer({
|
|
@@ -1823,6 +1875,7 @@ export async function buildTargetWith(
|
|
|
1823
1875
|
target: targetPlan.serverTarget,
|
|
1824
1876
|
...(client.css !== undefined ? { styles: client.css } : {}),
|
|
1825
1877
|
...(client.routeStyles !== undefined ? { routeStyles: client.routeStyles } : {}),
|
|
1878
|
+
...(client.copyChunks !== undefined ? { copyChunks: client.copyChunks } : {}),
|
|
1826
1879
|
...(options.serverPlugins ? { plugins: options.serverPlugins } : {}),
|
|
1827
1880
|
define: { "process.env.NODE_ENV": '"production"', ...(options.define ?? {}) },
|
|
1828
1881
|
...(client.cssLoading !== undefined
|
package/src/client.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* a full page load). DOM-only - never imported on the server, so the store stays SSR-safe.
|
|
6
6
|
*/
|
|
7
7
|
import { trustedHeadAttributes } from "./internal/head-attributes.ts"
|
|
8
|
+
import { appUrl } from "./internal/route-path.ts"
|
|
8
9
|
import { EXECUTABLE_SCRIPT_TYPES, INERT_SCRIPT_TYPES } from "./internal/script-types.ts"
|
|
9
10
|
import type { Meta } from "./manifest.ts"
|
|
10
11
|
import {
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
setBlockerController,
|
|
19
20
|
setBrowserNavigate,
|
|
20
21
|
} from "./navigation.ts"
|
|
22
|
+
import { handoverLocale } from "./render-seam.ts"
|
|
21
23
|
import { type ClientRouter, redirectOf } from "./router.ts"
|
|
22
24
|
|
|
23
25
|
/** Read the nonce carried by the current server-rendered document, if any. */
|
|
@@ -48,6 +50,7 @@ export {
|
|
|
48
50
|
resolveNavigate,
|
|
49
51
|
} from "./navigation.ts"
|
|
50
52
|
export { createMutation, createQueryClient } from "./query.ts"
|
|
53
|
+
export { localeOfProps } from "./render-seam.ts"
|
|
51
54
|
export { createClientRouter, createMatcher } from "./router.ts"
|
|
52
55
|
// Same reason, for the ADAPTER packages (`@nifrajs/web-react` and friends). Every one of them reaches
|
|
53
56
|
// for these from the root - `searchOfChain` in its `/client` entry, the navigation and query values in
|
|
@@ -232,7 +235,7 @@ export function installHistory(
|
|
|
232
235
|
router: ClientRouter,
|
|
233
236
|
options: InstallHistoryOptions = {},
|
|
234
237
|
): () => void {
|
|
235
|
-
const fallback = options.fallback ?? ((path: string) => location.assign(path))
|
|
238
|
+
const fallback = options.fallback ?? ((path: string) => location.assign(appUrl(path)))
|
|
236
239
|
|
|
237
240
|
// We manage scroll ourselves (save per history entry, restore after the navigated content
|
|
238
241
|
// renders), so disable the browser's native restoration which would otherwise fight us.
|
|
@@ -364,7 +367,7 @@ export function installHistory(
|
|
|
364
367
|
if (mode === "replace") {
|
|
365
368
|
// Replace the current entry (same index) with the new URL. The entry we're leaving is discarded, so
|
|
366
369
|
// its scroll isn't worth saving; the new route scrolls to its fragment target, else the top.
|
|
367
|
-
history.replaceState(entryState, "", path)
|
|
370
|
+
history.replaceState(entryState, "", appUrl(path))
|
|
368
371
|
pendingScroll = url.hash !== "" ? { hash: hashId(url.hash) } : { pos: [0, 0] }
|
|
369
372
|
} else {
|
|
370
373
|
// Save the leaving entry's scroll (spread keeps its nifraIndex), push a fresh higher-indexed entry
|
|
@@ -372,7 +375,7 @@ export function installHistory(
|
|
|
372
375
|
history.replaceState({ ...(history.state ?? {}), nifraScroll: [scrollX, scrollY] }, "")
|
|
373
376
|
index += 1
|
|
374
377
|
entryState.nifraIndex = index
|
|
375
|
-
history.pushState(entryState, "", path)
|
|
378
|
+
history.pushState(entryState, "", appUrl(path))
|
|
376
379
|
pendingScroll = url.hash !== "" ? { hash: hashId(url.hash) } : { pos: [0, 0] }
|
|
377
380
|
}
|
|
378
381
|
here = url.pathname + url.search + url.hash
|
|
@@ -555,9 +558,9 @@ export function installHistory(
|
|
|
555
558
|
if (expected === undefined) {
|
|
556
559
|
history.replaceState({ ...(history.state ?? {}), nifraScroll: [scrollX, scrollY] }, "")
|
|
557
560
|
index += 1
|
|
558
|
-
history.pushState({ nifraIndex: index }, "", path)
|
|
561
|
+
history.pushState({ nifraIndex: index }, "", appUrl(path))
|
|
559
562
|
} else {
|
|
560
|
-
history.replaceState(history.state, "", path + location.hash)
|
|
563
|
+
history.replaceState(history.state, "", appUrl(path) + location.hash)
|
|
561
564
|
}
|
|
562
565
|
here = location.pathname + location.search + location.hash
|
|
563
566
|
pendingScroll = location.hash !== "" ? { hash: hashId(location.hash) } : { pos: [0, 0] }
|
|
@@ -610,28 +613,29 @@ export function installHistory(
|
|
|
610
613
|
}
|
|
611
614
|
}
|
|
612
615
|
|
|
613
|
-
/**
|
|
614
|
-
* Intercept submissions of same-origin `<form method="post">` whose action targets an app route:
|
|
615
|
-
* submit via the router (no full reload - POST the action, then revalidate the active loader). On
|
|
616
|
-
* failure it falls back to a native submit, so the form still works. Returns a teardown function.
|
|
617
|
-
*/
|
|
618
616
|
/**
|
|
619
617
|
* Sync the document head to a route's resolved {@link Meta} on client navigation. Sets the title
|
|
620
618
|
* (when provided) and replaces the **managed** (`data-nifra`) `<meta>`/`<link>` tags - static head
|
|
621
619
|
* content (charset, hand-written tags) is never touched. SSR injects the same `data-nifra` tags, so
|
|
622
620
|
* the first navigation cleanly takes over from the server-rendered head.
|
|
623
621
|
*/
|
|
624
|
-
export function applyHead(head: Meta): void {
|
|
622
|
+
export function applyHead(head: Meta, path?: string): void {
|
|
625
623
|
const documentNonce = currentDocumentNonce()
|
|
626
624
|
if (head.title !== undefined) document.title = head.title
|
|
627
625
|
// `<html lang>`/`<html dir>` - applied AUTHORITATIVELY (unlike `title`, which is left alone when the
|
|
628
|
-
// route omits it), mirroring the SSR shell's defaulting exactly: `lang`
|
|
629
|
-
// removed when unset. Anything laxer drifts on a
|
|
630
|
-
// `dir="rtl"` behind and lay the English page
|
|
626
|
+
// route omits it), mirroring the SSR shell's defaulting exactly: a meta `lang` wins, then the page's
|
|
627
|
+
// registered locale, then `"en"`; `dir` is removed when unset. Anything laxer drifts on a
|
|
628
|
+
// multilingual site - navigating /ur → /en would leave `dir="rtl"` behind and lay the English page
|
|
629
|
+
// out right-to-left, a bug a hard reload would not show.
|
|
630
|
+
const locale =
|
|
631
|
+
head.lang === undefined
|
|
632
|
+
? handoverLocale(path ?? (typeof location === "undefined" ? "" : location.pathname))
|
|
633
|
+
: undefined
|
|
634
|
+
const dir = head.dir ?? locale?.dir
|
|
631
635
|
const root = document.documentElement
|
|
632
|
-
root.setAttribute("lang", head.lang ?? "en")
|
|
633
|
-
if (
|
|
634
|
-
else root.setAttribute("dir",
|
|
636
|
+
root.setAttribute("lang", head.lang ?? locale?.lang ?? "en")
|
|
637
|
+
if (dir === undefined) root.removeAttribute("dir")
|
|
638
|
+
else root.setAttribute("dir", dir)
|
|
635
639
|
for (const el of document.head.querySelectorAll("[data-nifra]")) el.remove()
|
|
636
640
|
// Values follow the same HTML attribute conventions as the SSR `tagAttrs`: a string sets the value,
|
|
637
641
|
// `true` sets the bare boolean attribute, `false`/`undefined` skip it - so a soft-nav head matches
|
|
@@ -689,6 +693,17 @@ export function applyHead(head: Meta): void {
|
|
|
689
693
|
}
|
|
690
694
|
}
|
|
691
695
|
|
|
696
|
+
// A control named like a form property shadows it (`<input name="target">` makes `form.target` that
|
|
697
|
+
// input), so the form's own state is read through the prototype accessors.
|
|
698
|
+
function formProperty(form: HTMLFormElement, name: "action" | "method" | "target"): string {
|
|
699
|
+
return String(Reflect.get(HTMLFormElement.prototype, name, form))
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Intercept submissions of same-origin `<form method="post">` whose action targets an app route:
|
|
704
|
+
* submit via the router (no full reload - POST the action, then revalidate the active loader). On
|
|
705
|
+
* failure it falls back to a native submit, so the form still works. Returns a teardown function.
|
|
706
|
+
*/
|
|
692
707
|
export function installForms(router: ClientRouter): () => void {
|
|
693
708
|
// Forms whose data submit failed before the server answered: their next submit event is the native
|
|
694
709
|
// post that replaces it.
|
|
@@ -702,20 +717,25 @@ export function installForms(router: ClientRouter): () => void {
|
|
|
702
717
|
// name=value is part of the submission (`<button name="intent" value="delete">`).
|
|
703
718
|
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: a submit event's submitter is the submit button or input that fired it, or null
|
|
704
719
|
const submitter = event.submitter as HTMLButtonElement | null
|
|
705
|
-
const method = submitter?.hasAttribute("formmethod")
|
|
720
|
+
const method = submitter?.hasAttribute("formmethod")
|
|
721
|
+
? submitter.formMethod
|
|
722
|
+
: formProperty(form, "method")
|
|
706
723
|
if (method.toLowerCase() !== "post") return // GET forms submit natively
|
|
707
|
-
const target = submitter?.hasAttribute("formtarget")
|
|
724
|
+
const target = submitter?.hasAttribute("formtarget")
|
|
725
|
+
? submitter.formTarget
|
|
726
|
+
: formProperty(form, "target")
|
|
708
727
|
if (target !== "" && target !== "_self") return // it loads in another browsing context
|
|
709
728
|
// Resolved (or the current document URL if unset).
|
|
710
729
|
const url = sameDocumentUrl(
|
|
711
|
-
submitter?.hasAttribute("formaction") ? submitter.formAction : form
|
|
730
|
+
submitter?.hasAttribute("formaction") ? submitter.formAction : formProperty(form, "action"),
|
|
712
731
|
)
|
|
713
732
|
if (url === null) return
|
|
714
733
|
if (router.match(url.pathname) === null) return // not an app route → native submit
|
|
715
734
|
event.preventDefault()
|
|
716
735
|
// `data-nifra-revalidate="false"` opts out of the post-action loader revalidation (the action's
|
|
717
736
|
// actionData drives the update); absent or any other value keeps the default revalidation.
|
|
718
|
-
const revalidate =
|
|
737
|
+
const revalidate =
|
|
738
|
+
Element.prototype.getAttribute.call(form, "data-nifra-revalidate") !== "false"
|
|
719
739
|
const body = new FormData(form, submitter)
|
|
720
740
|
router.submit(url.pathname + url.search, body, { revalidate }).catch((error) => {
|
|
721
741
|
const to = redirectOf(error)
|
|
@@ -724,7 +744,7 @@ export function installForms(router: ClientRouter): () => void {
|
|
|
724
744
|
else {
|
|
725
745
|
// Nothing reached the server - post natively, through the same button.
|
|
726
746
|
native.add(form)
|
|
727
|
-
|
|
747
|
+
HTMLFormElement.prototype.requestSubmit.call(form, submitter)
|
|
728
748
|
}
|
|
729
749
|
})
|
|
730
750
|
}
|
package/src/dev-port.ts
CHANGED
|
@@ -38,13 +38,14 @@ export const explainBindFailure = (err: unknown, port: number): Error =>
|
|
|
38
38
|
|
|
39
39
|
/** The bits of a Node server {@link listenOrExplain} touches - structural, so a test can fake it. */
|
|
40
40
|
export interface ListenTarget {
|
|
41
|
-
listen(port: number, cb: () => void): void
|
|
41
|
+
listen(port: number, hostname: string, cb: () => void): void
|
|
42
42
|
once(event: "error", cb: (err: unknown) => void): void
|
|
43
43
|
removeListener(event: "error", cb: (err: unknown) => void): void
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* `listen`, but a bind failure becomes a readable nifra error instead of Node's raw internal throw.
|
|
48
|
+
* The hostname is required: omitting it binds every interface, and a dev server is local-only.
|
|
48
49
|
*
|
|
49
50
|
* Without an `error` listener attached, Node throws from deep inside `node:events`, the new process dies
|
|
50
51
|
* in the background, and nothing connects that death to the stale page in front of you.
|
|
@@ -52,7 +53,7 @@ export interface ListenTarget {
|
|
|
52
53
|
export function listenOrExplain(
|
|
53
54
|
server: ListenTarget,
|
|
54
55
|
port: number,
|
|
55
|
-
hostname
|
|
56
|
+
hostname: string,
|
|
56
57
|
): Promise<void> {
|
|
57
58
|
return new Promise<void>((resolve, reject) => {
|
|
58
59
|
const onError = (err: unknown): void => reject(explainBindFailure(err, port))
|
|
@@ -63,13 +64,6 @@ export function listenOrExplain(
|
|
|
63
64
|
server.removeListener("error", onError)
|
|
64
65
|
resolve()
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
-
else {
|
|
68
|
-
;(
|
|
69
|
-
server as ListenTarget & {
|
|
70
|
-
listen(port: number, hostname: string, cb: () => void): void
|
|
71
|
-
}
|
|
72
|
-
).listen(port, hostname, onListening)
|
|
73
|
-
}
|
|
67
|
+
server.listen(port, hostname, onListening)
|
|
74
68
|
})
|
|
75
69
|
}
|
package/src/index.ts
CHANGED
|
@@ -88,12 +88,8 @@ export {
|
|
|
88
88
|
// `import { createClientRouter, createMatcher, mergeHeads, resolveChainMeta } from "@nifrajs/web/client"`.
|
|
89
89
|
export { mergeHeads, resolveChainMeta, resolveMeta } from "./internal/head-merge.ts"
|
|
90
90
|
export {
|
|
91
|
-
canonical,
|
|
92
91
|
gone,
|
|
93
|
-
jsonLd,
|
|
94
92
|
notFound,
|
|
95
|
-
type OpenGraphInput,
|
|
96
|
-
openGraph,
|
|
97
93
|
type RedirectOptions,
|
|
98
94
|
type RenderAssemblyCache,
|
|
99
95
|
type RenderedPage,
|
|
@@ -125,6 +121,7 @@ export {
|
|
|
125
121
|
type CreateWebAppOptions,
|
|
126
122
|
createWebApp,
|
|
127
123
|
type NonceResolver,
|
|
124
|
+
type WebAppI18n,
|
|
128
125
|
webProjectEvidence,
|
|
129
126
|
} from "./internal/web-app.ts"
|
|
130
127
|
// ISR (incremental static regeneration): a pluggable cache store + the `withISR` stale-while-revalidate
|
|
@@ -257,9 +254,15 @@ export {
|
|
|
257
254
|
ACTION_GLOBAL,
|
|
258
255
|
BOUNDARY_GLOBAL,
|
|
259
256
|
DATA_GLOBAL,
|
|
257
|
+
type DocumentParts,
|
|
260
258
|
HANDOVER_ID,
|
|
261
259
|
LAYOUT_DATA_GLOBAL,
|
|
260
|
+
LOCALES_GLOBAL,
|
|
261
|
+
localeOfProps,
|
|
262
262
|
type MatchChain,
|
|
263
|
+
type PageLocale,
|
|
264
|
+
type PageLocales,
|
|
265
|
+
pageLocaleIn,
|
|
263
266
|
type RenderAdapter,
|
|
264
267
|
type RenderProps,
|
|
265
268
|
type RenderStreamOptions,
|
|
@@ -298,6 +301,7 @@ export {
|
|
|
298
301
|
// ctx + `renderPage`) and a client adapter mount call this with the same URL + schema, so the two sides
|
|
299
302
|
// produce the identical value by construction. An adapter's `useSearch` binding reads its result.
|
|
300
303
|
export { type SearchOf, searchOf, searchOfChain, serializeSearch } from "./search.ts"
|
|
304
|
+
export { canonical, jsonLd, type OpenGraphInput, openGraph } from "./seo.ts"
|
|
301
305
|
export {
|
|
302
306
|
type HtmlSanitizer,
|
|
303
307
|
type SanitizedHtml,
|