@nifrajs/web 2.13.0 → 3.1.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/boundary.d.ts +158 -0
- package/dist/boundary.d.ts.map +1 -0
- package/dist/boundary.js +237 -0
- package/dist/boundary.js.map +1 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +5 -3
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +9 -6
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +57 -31
- package/dist/build.js.map +1 -1
- package/dist/fn.d.ts +2 -2
- package/dist/fn.d.ts.map +1 -1
- package/dist/fn.js +2 -2
- package/dist/fn.js.map +1 -1
- package/dist/index.d.ts +30 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +179 -56
- package/dist/index.js.map +1 -1
- package/dist/internal/fetcher-runtime.d.ts +19 -0
- package/dist/internal/fetcher-runtime.d.ts.map +1 -0
- package/dist/internal/fetcher-runtime.js +26 -0
- package/dist/internal/fetcher-runtime.js.map +1 -0
- package/dist/internal/identity-policy.d.ts +51 -0
- package/dist/internal/identity-policy.d.ts.map +1 -0
- package/dist/internal/identity-policy.js +67 -0
- package/dist/internal/identity-policy.js.map +1 -0
- package/dist/internal/parity.d.ts +105 -7
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +303 -56
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/query-runtime.d.ts +8 -0
- package/dist/internal/query-runtime.d.ts.map +1 -0
- package/dist/internal/query-runtime.js +29 -0
- package/dist/internal/query-runtime.js.map +1 -0
- package/dist/islands.d.ts +2 -2
- package/dist/islands.d.ts.map +1 -1
- package/dist/islands.js +17 -36
- package/dist/islands.js.map +1 -1
- package/dist/manifest.d.ts +41 -6
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js.map +1 -1
- package/dist/render-seam.d.ts +5 -0
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +10 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/router.d.ts +10 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +233 -26
- package/dist/router.js.map +1 -1
- package/dist/vite.d.ts +8 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +51 -13
- package/dist/vite.js.map +1 -1
- package/package.json +13 -2
- package/src/boundary.ts +407 -0
- package/src/build-vite.ts +5 -3
- package/src/build.ts +61 -31
- package/src/fn.ts +6 -6
- package/src/index.ts +255 -54
- package/src/internal/fetcher-runtime.ts +36 -0
- package/src/internal/identity-policy.ts +94 -0
- package/src/internal/parity.ts +447 -70
- package/src/internal/query-runtime.ts +31 -0
- package/src/islands.ts +22 -41
- package/src/manifest.ts +50 -3
- package/src/render-seam.ts +6 -0
- package/src/router.ts +271 -29
- package/src/vite.ts +75 -13
package/src/build.ts
CHANGED
|
@@ -8,10 +8,15 @@
|
|
|
8
8
|
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
|
9
9
|
import { cp, lstat, mkdir, realpath } from "node:fs/promises"
|
|
10
10
|
import { dirname, join, relative, resolve as resolvePath, sep } from "node:path"
|
|
11
|
+
import {
|
|
12
|
+
singleCopyPlugin as declaredSingleCopyPlugin,
|
|
13
|
+
readSingleCopyDeclaration,
|
|
14
|
+
} from "@nifrajs/core/single-copy"
|
|
11
15
|
import { type BunPlugin, Glob } from "bun"
|
|
12
16
|
import { sanitizeOutputNames } from "./chunk-names.ts"
|
|
13
17
|
import { discoverRoutes } from "./fs.ts"
|
|
14
18
|
import { generateClientEntry, generateServerManifest } from "./index.ts"
|
|
19
|
+
import { dedupePolicyFor } from "./internal/identity-policy.ts"
|
|
15
20
|
import {
|
|
16
21
|
assertDevelopmentProductionParity,
|
|
17
22
|
assertIdentityParity,
|
|
@@ -789,28 +794,36 @@ export interface ManifestDrift {
|
|
|
789
794
|
readonly extra: readonly string[]
|
|
790
795
|
}
|
|
791
796
|
|
|
792
|
-
// The route-relative
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
|
|
797
|
+
// The route-relative file keys the generated manifest's route map declares. Both shapes are matched by
|
|
798
|
+
// their VALUE: eager ` "routes/x.tsx": m0,` and lazy ` "routes/x.tsx": () => import("./routes/x"),`.
|
|
799
|
+
// The KEY (not the import specifier) is the route identity `discoverRoutes` produces, extension and all;
|
|
800
|
+
// the import specifier is emitted extensionless (so a bare `tsc` compiles the manifest) and could not be
|
|
801
|
+
// mapped back to the exact `.tsx`/`.ts` key. The baked `routeStyles` JSON is a single line, so its keys
|
|
802
|
+
// are never at an indented line start and never match. `[ \t]+` (not `\s+`) so the `m` anchor stays on
|
|
803
|
+
// the same line and one route-map entry cannot span two lines of source.
|
|
804
|
+
const MANIFEST_ROUTE_ENTRY = /^[ \t]+(["'])([^"'\n]+)\1\s*:\s*(?:m\d+\b|\(\s*\)\s*=>\s*import\b)/gm
|
|
805
|
+
// The lazy shape declares `const loaders`, the eager shape `const modules`. Anchored at a line start,
|
|
806
|
+
// NOT `.includes("const loaders =")`: the real emit is `const loaders: Record<...> = {`, whose `:` type
|
|
807
|
+
// annotation sits between the name and the `=`, so the ` =` substring never matched and every lazy
|
|
808
|
+
// manifest was re-synced as eager (silently converting `import()` splitting into a single eager bundle).
|
|
809
|
+
const MANIFEST_IS_LAZY = /^const loaders\b/m
|
|
798
810
|
// The baked client-entry line: `export const clientEntry = "…"`.
|
|
799
811
|
const MANIFEST_CLIENT_ENTRY = /export\s+const\s+clientEntry\s*=\s*["']([^"']+)["']/
|
|
800
812
|
|
|
801
813
|
/**
|
|
802
|
-
* Extract the route-relative file list
|
|
803
|
-
* `routes/`-relative keys `discoverRoutes` produces (e.g. `docs/index.tsx`).
|
|
804
|
-
*
|
|
805
|
-
*
|
|
806
|
-
*
|
|
814
|
+
* Extract the route-relative file list a committed server-manifest declares, as the same
|
|
815
|
+
* `routes/`-relative keys `discoverRoutes` produces (e.g. `docs/index.tsx`). Reads the route map's KEYS,
|
|
816
|
+
* which carry the file extension and no directory prefix - exactly discovery's shape - so the result is
|
|
817
|
+
* independent of the specifier prefix the manifest happened to import with. The `@nifrajs/web` import and
|
|
818
|
+
* the baked `clientEntry`/`styles`/`routeStyles` lines carry no route-map entry and are ignored.
|
|
819
|
+
*
|
|
820
|
+
* `_routesPrefix` is accepted for call-site compatibility but no longer needed: the keys are already
|
|
821
|
+
* prefix-free. Pure - operates on source text.
|
|
807
822
|
*/
|
|
808
|
-
export function parseManifestRouteFiles(source: string,
|
|
823
|
+
export function parseManifestRouteFiles(source: string, _routesPrefix?: string): string[] {
|
|
809
824
|
const files = new Set<string>()
|
|
810
|
-
for (const match of source.matchAll(
|
|
811
|
-
|
|
812
|
-
if (spec === undefined || !spec.startsWith(routesPrefix)) continue
|
|
813
|
-
files.add(spec.slice(routesPrefix.length))
|
|
825
|
+
for (const match of source.matchAll(MANIFEST_ROUTE_ENTRY)) {
|
|
826
|
+
if (match[2] !== undefined) files.add(match[2])
|
|
814
827
|
}
|
|
815
828
|
return [...files].sort()
|
|
816
829
|
}
|
|
@@ -885,7 +898,7 @@ export function resyncServerManifestSource(
|
|
|
885
898
|
clientEntry: parseManifestClientEntry(source) ?? "",
|
|
886
899
|
styles: parseManifestStyles(source),
|
|
887
900
|
routeStyles: parseManifestRouteStyles(source),
|
|
888
|
-
lazy:
|
|
901
|
+
lazy: MANIFEST_IS_LAZY.test(source),
|
|
889
902
|
})
|
|
890
903
|
}
|
|
891
904
|
|
|
@@ -1007,6 +1020,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
1007
1020
|
...buildExtras,
|
|
1008
1021
|
minify: options.minify ?? true,
|
|
1009
1022
|
plugins: [
|
|
1023
|
+
...declaredSingleCopyPlugins(root),
|
|
1010
1024
|
reactDedupePlugin(routesDir),
|
|
1011
1025
|
preactDedupePlugin(routesDir),
|
|
1012
1026
|
svelteDedupePlugin(routesDir),
|
|
@@ -1097,12 +1111,17 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
1097
1111
|
const base = basename(path)
|
|
1098
1112
|
return base.slice(0, base.lastIndexOf("-")) // strip `-${hash}.css`
|
|
1099
1113
|
}
|
|
1114
|
+
// `css` is the union of every emitted stylesheet, matching the Vite pipeline (build-vite.ts) so the
|
|
1115
|
+
// field means the same thing on both bundlers. The bootstrap aggregate lists first, preserving the
|
|
1116
|
+
// fallback link order `createWebApp` uses (whole-app stylesheet before any route-scoped one); the
|
|
1117
|
+
// per-route `cssBundle` outputs follow. Dropping those per-route outputs (the old `aggregate`-only
|
|
1118
|
+
// shape) left them in `manifest.assets` but not `css`, so a route-scoped stylesheet normalized to
|
|
1119
|
+
// `asset:css` and tripped the module-graph parity contract - a bundler-dependent divergence.
|
|
1100
1120
|
const cssAssets = result.outputs.filter((o) => o.kind === "asset" && o.path.endsWith(".css"))
|
|
1101
|
-
const
|
|
1102
|
-
.filter((o) => cssNameOf(o.path) === bootstrapName)
|
|
1103
|
-
.
|
|
1104
|
-
|
|
1105
|
-
aggregate.length > 0 ? aggregate : cssAssets.map((o) => toUrl(o.path))
|
|
1121
|
+
const css: readonly string[] = [
|
|
1122
|
+
...cssAssets.filter((o) => cssNameOf(o.path) === bootstrapName),
|
|
1123
|
+
...cssAssets.filter((o) => cssNameOf(o.path) !== bootstrapName),
|
|
1124
|
+
].map((o) => toUrl(o.path))
|
|
1106
1125
|
|
|
1107
1126
|
// CSS - per-route: each route/layout file is its own entrypoint, so the build metafile records its
|
|
1108
1127
|
// `cssBundle` - exactly the CSS that file's subtree uses (shared-component CSS is inlined into each
|
|
@@ -1236,7 +1255,7 @@ const reactDomEdgePlugin = (from: string): BunPlugin => ({
|
|
|
1236
1255
|
* condition-agnostic, so pinning it doesn't disturb the edge/browser/server conditions that select
|
|
1237
1256
|
* react-dom's build.
|
|
1238
1257
|
*/
|
|
1239
|
-
const REACT_DEDUPE_SPECS =
|
|
1258
|
+
const REACT_DEDUPE_SPECS = dedupePolicyFor("react").bunSpecs ?? []
|
|
1240
1259
|
export const reactDedupePlugin = (from: string): BunPlugin => ({
|
|
1241
1260
|
name: "nifra-react-dedupe",
|
|
1242
1261
|
setup(build) {
|
|
@@ -1264,13 +1283,7 @@ export const reactDedupePlugin = (from: string): BunPlugin => ({
|
|
|
1264
1283
|
* skipped). Preact core is condition-agnostic, so pinning it doesn't disturb any condition selection - and
|
|
1265
1284
|
* unlike react-dom there is no edge-vs-server build to preserve, so pinning the subpaths is safe.
|
|
1266
1285
|
*/
|
|
1267
|
-
const PREACT_DEDUPE_SPECS = [
|
|
1268
|
-
"preact",
|
|
1269
|
-
"preact/hooks",
|
|
1270
|
-
"preact/compat",
|
|
1271
|
-
"preact/jsx-runtime",
|
|
1272
|
-
"preact/jsx-dev-runtime",
|
|
1273
|
-
] as const
|
|
1286
|
+
const PREACT_DEDUPE_SPECS = dedupePolicyFor("preact").bunSpecs ?? []
|
|
1274
1287
|
export const preactDedupePlugin = (from: string): BunPlugin => ({
|
|
1275
1288
|
name: "nifra-preact-dedupe",
|
|
1276
1289
|
setup(build) {
|
|
@@ -1287,6 +1300,7 @@ export const preactDedupePlugin = (from: string): BunPlugin => ({
|
|
|
1287
1300
|
},
|
|
1288
1301
|
})
|
|
1289
1302
|
|
|
1303
|
+
const SVELTE_DEDUPE_PATTERN = dedupePolicyFor("svelte").bunPattern ?? /^svelte($|\/internal\/)/
|
|
1290
1304
|
/**
|
|
1291
1305
|
* Dedupe Svelte to a single copy - the Svelte analogue of `reactDedupePlugin`/`preactDedupePlugin`, closing
|
|
1292
1306
|
* the same class of bug for Svelte (which had NO build-time dedup before). A workspace- or file-linked
|
|
@@ -1305,7 +1319,7 @@ export const preactDedupePlugin = (from: string): BunPlugin => ({
|
|
|
1305
1319
|
export const svelteDedupePlugin = (from: string): BunPlugin => ({
|
|
1306
1320
|
name: "nifra-svelte-dedupe",
|
|
1307
1321
|
setup(build) {
|
|
1308
|
-
build.onResolve({ filter:
|
|
1322
|
+
build.onResolve({ filter: SVELTE_DEDUPE_PATTERN }, (args) => {
|
|
1309
1323
|
try {
|
|
1310
1324
|
return { path: Bun.resolveSync(args.path, from) }
|
|
1311
1325
|
} catch {
|
|
@@ -1315,6 +1329,21 @@ export const svelteDedupePlugin = (from: string): BunPlugin => ({
|
|
|
1315
1329
|
},
|
|
1316
1330
|
})
|
|
1317
1331
|
|
|
1332
|
+
/**
|
|
1333
|
+
* The app's declared single-copy rule, applied to the bundle.
|
|
1334
|
+
*
|
|
1335
|
+
* The three plugins above cover the framework runtimes by name, which is the case that bites hardest
|
|
1336
|
+
* and needs no configuration. This one covers what a name list cannot know: `@nifrajs/*` (two copies
|
|
1337
|
+
* of `@nifrajs/core` are two `Server` classes, and `Server` has private members, so `.merge()` stops
|
|
1338
|
+
* accepting the other's app) and whatever else the app named in
|
|
1339
|
+
* `"nifra": { "singleCopy": [...] }`.
|
|
1340
|
+
*
|
|
1341
|
+
* Nothing declared means no plugin at all - the plan does filesystem work, so it is not built for the
|
|
1342
|
+
* majority of apps that have a single copy of everything and never think about this.
|
|
1343
|
+
*/
|
|
1344
|
+
const declaredSingleCopyPlugins = (root: string): readonly BunPlugin[] =>
|
|
1345
|
+
readSingleCopyDeclaration(root) === undefined ? [] : [declaredSingleCopyPlugin({ cwd: root })]
|
|
1346
|
+
|
|
1318
1347
|
/**
|
|
1319
1348
|
* Remix-style `.server` convention for the CLIENT build. A module named `*.server.ts(x)` (`db.server.ts`,
|
|
1320
1349
|
* `auth.server.ts`, …) is server-only - empty it in the browser bundle so its (possibly `node:` / native /
|
|
@@ -1431,6 +1460,7 @@ export async function buildServer(options: BuildServerOptions): Promise<ServerBu
|
|
|
1431
1460
|
// Lazy → one chunk per route (loaded on first request); eager → a single self-contained file.
|
|
1432
1461
|
splitting: lazy,
|
|
1433
1462
|
plugins: [
|
|
1463
|
+
...declaredSingleCopyPlugins(resolvePath(dirname(routesDir))),
|
|
1434
1464
|
...(edge ? [reactDomEdgePlugin(entryDir), solidWebServerPlugin(entryDir)] : []),
|
|
1435
1465
|
reactDedupePlugin(entryDir),
|
|
1436
1466
|
preactDedupePlugin(entryDir),
|
package/src/fn.ts
CHANGED
|
@@ -45,7 +45,10 @@
|
|
|
45
45
|
* bespoke dispatcher.
|
|
46
46
|
*/
|
|
47
47
|
import {
|
|
48
|
+
type AnyServer,
|
|
48
49
|
type Context,
|
|
50
|
+
defineIdentityPlugin,
|
|
51
|
+
type IdentityPlugin,
|
|
49
52
|
isSameOriginRequest,
|
|
50
53
|
type RouteSchema,
|
|
51
54
|
type StandardSchemaV1,
|
|
@@ -151,10 +154,7 @@ export type ServerFnModule = Readonly<Record<string, unknown>>
|
|
|
151
154
|
*
|
|
152
155
|
* Exports that are not server functions are ignored, so `export type` and helpers can live alongside.
|
|
153
156
|
*/
|
|
154
|
-
export function serverFunctions
|
|
155
|
-
namespace: string,
|
|
156
|
-
module: ServerFnModule,
|
|
157
|
-
): (app: S) => S {
|
|
157
|
+
export function serverFunctions(namespace: string, module: ServerFnModule): IdentityPlugin {
|
|
158
158
|
if (!NAMESPACE.test(namespace)) {
|
|
159
159
|
throw new Error(
|
|
160
160
|
`[nifra/fn] invalid server-function namespace ${JSON.stringify(namespace)} - use lowercase dot/dash segments (it becomes a URL path).`,
|
|
@@ -164,7 +164,7 @@ export function serverFunctions<S extends ServerFnHost>(
|
|
|
164
164
|
(entry): entry is [string, ServerFn<unknown, unknown>] => isServerFn(entry[1]),
|
|
165
165
|
)
|
|
166
166
|
|
|
167
|
-
const apply = (app: S): S => {
|
|
167
|
+
const apply = <S extends AnyServer>(app: S): S => {
|
|
168
168
|
for (const [name, fn] of mounted) {
|
|
169
169
|
if (!EXPORT_NAME.test(name)) {
|
|
170
170
|
throw new Error(
|
|
@@ -197,5 +197,5 @@ export function serverFunctions<S extends ServerFnHost>(
|
|
|
197
197
|
}
|
|
198
198
|
return app
|
|
199
199
|
}
|
|
200
|
-
return
|
|
200
|
+
return defineIdentityPlugin(`nifra:server-fn:${namespace}`, apply)
|
|
201
201
|
}
|