@nifrajs/web 2.12.1 → 2.14.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/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +3 -0
- 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 +30 -18
- package/dist/build.js.map +1 -1
- package/dist/dev-ssr-graph.js +1 -1
- package/dist/dev-ssr-graph.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 +0 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -5
- package/dist/index.js.map +1 -1
- package/dist/internal/parity.d.ts +86 -0
- package/dist/internal/parity.d.ts.map +1 -0
- package/dist/internal/parity.js +441 -0
- package/dist/internal/parity.js.map +1 -0
- package/dist/vite.d.ts +8 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +43 -5
- package/dist/vite.js.map +1 -1
- package/package.json +7 -2
- package/src/build-vite.ts +7 -0
- package/src/build.ts +33 -17
- package/src/dev-ssr-graph.ts +1 -1
- package/src/fn.ts +6 -6
- package/src/index.ts +20 -5
- package/src/internal/parity.ts +579 -0
- package/src/vite.ts +67 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/web",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.1",
|
|
4
4
|
"description": "Framework-agnostic SSR core for nifra - the render seam + HTML document orchestration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -154,6 +154,11 @@
|
|
|
154
154
|
"bun": "./src/fn-state.ts",
|
|
155
155
|
"types": "./dist/fn-state.d.ts",
|
|
156
156
|
"default": "./dist/fn-state.js"
|
|
157
|
+
},
|
|
158
|
+
"./internal/parity": {
|
|
159
|
+
"bun": "./src/internal/parity.ts",
|
|
160
|
+
"types": "./dist/internal/parity.d.ts",
|
|
161
|
+
"default": "./dist/internal/parity.js"
|
|
157
162
|
}
|
|
158
163
|
},
|
|
159
164
|
"files": [
|
|
@@ -171,7 +176,7 @@
|
|
|
171
176
|
"access": "public"
|
|
172
177
|
},
|
|
173
178
|
"dependencies": {
|
|
174
|
-
"@nifrajs/core": "2.
|
|
179
|
+
"@nifrajs/core": "2.14.1"
|
|
175
180
|
},
|
|
176
181
|
"peerDependencies": {
|
|
177
182
|
"sass": "^1.0.0",
|
package/src/build-vite.ts
CHANGED
|
@@ -33,6 +33,11 @@ import {
|
|
|
33
33
|
} from "./build.ts"
|
|
34
34
|
import { discoverRoutes } from "./fs.ts"
|
|
35
35
|
import { generateClientEntry, generateServerManifest } from "./index.ts"
|
|
36
|
+
import {
|
|
37
|
+
assertDevelopmentProductionParity,
|
|
38
|
+
assertIdentityParity,
|
|
39
|
+
collectDevelopmentParityInput,
|
|
40
|
+
} from "./internal/parity.ts"
|
|
36
41
|
import { vitePublicEnvPrefix } from "./internal/server-boundary.ts"
|
|
37
42
|
import { importVite, isViteUnresolved } from "./internal/vite-import.ts"
|
|
38
43
|
import { scopedName } from "./plugins/css-modules.ts"
|
|
@@ -128,6 +133,7 @@ export interface BuildClientViteOptions extends Omit<BuildClientOptions, "plugin
|
|
|
128
133
|
export async function buildClientVite(options: BuildClientViteOptions): Promise<BuildManifest> {
|
|
129
134
|
const { routesDir, outDir, clientModule } = options
|
|
130
135
|
const root = resolvePath(options.root ?? dirname(routesDir))
|
|
136
|
+
await assertIdentityParity(root)
|
|
131
137
|
const resolve = options.resolve ?? ((file: string) => `${routesDir}/${file}`)
|
|
132
138
|
const publicPath = options.publicPath ?? "/assets/"
|
|
133
139
|
mkdirSync(outDir, { recursive: true })
|
|
@@ -325,6 +331,7 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
|
|
|
325
331
|
...(css.length > 0 ? { css } : {}),
|
|
326
332
|
...(Object.keys(routeStyles).length > 0 ? { routeStyles } : {}),
|
|
327
333
|
}
|
|
334
|
+
assertDevelopmentProductionParity(collectDevelopmentParityInput(routesDir, publicDir), manifest)
|
|
328
335
|
writeFileSync(`${outDir}/manifest.json`, JSON.stringify(manifest, null, 2))
|
|
329
336
|
return manifest
|
|
330
337
|
}
|
package/src/build.ts
CHANGED
|
@@ -12,6 +12,11 @@ import { type BunPlugin, Glob } from "bun"
|
|
|
12
12
|
import { sanitizeOutputNames } from "./chunk-names.ts"
|
|
13
13
|
import { discoverRoutes } from "./fs.ts"
|
|
14
14
|
import { generateClientEntry, generateServerManifest } from "./index.ts"
|
|
15
|
+
import {
|
|
16
|
+
assertDevelopmentProductionParity,
|
|
17
|
+
assertIdentityParity,
|
|
18
|
+
collectDevelopmentParityInput,
|
|
19
|
+
} from "./internal/parity.ts"
|
|
15
20
|
import {
|
|
16
21
|
generateServerFnStub,
|
|
17
22
|
SERVER_FN_MODULE,
|
|
@@ -784,28 +789,36 @@ export interface ManifestDrift {
|
|
|
784
789
|
readonly extra: readonly string[]
|
|
785
790
|
}
|
|
786
791
|
|
|
787
|
-
// The route-relative
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
//
|
|
792
|
-
|
|
792
|
+
// The route-relative file keys the generated manifest's route map declares. Both shapes are matched by
|
|
793
|
+
// their VALUE: eager ` "routes/x.tsx": m0,` and lazy ` "routes/x.tsx": () => import("./routes/x"),`.
|
|
794
|
+
// The KEY (not the import specifier) is the route identity `discoverRoutes` produces, extension and all;
|
|
795
|
+
// the import specifier is emitted extensionless (so a bare `tsc` compiles the manifest) and could not be
|
|
796
|
+
// mapped back to the exact `.tsx`/`.ts` key. The baked `routeStyles` JSON is a single line, so its keys
|
|
797
|
+
// are never at an indented line start and never match. `[ \t]+` (not `\s+`) so the `m` anchor stays on
|
|
798
|
+
// the same line and one route-map entry cannot span two lines of source.
|
|
799
|
+
const MANIFEST_ROUTE_ENTRY = /^[ \t]+(["'])([^"'\n]+)\1\s*:\s*(?:m\d+\b|\(\s*\)\s*=>\s*import\b)/gm
|
|
800
|
+
// The lazy shape declares `const loaders`, the eager shape `const modules`. Anchored at a line start,
|
|
801
|
+
// NOT `.includes("const loaders =")`: the real emit is `const loaders: Record<...> = {`, whose `:` type
|
|
802
|
+
// annotation sits between the name and the `=`, so the ` =` substring never matched and every lazy
|
|
803
|
+
// manifest was re-synced as eager (silently converting `import()` splitting into a single eager bundle).
|
|
804
|
+
const MANIFEST_IS_LAZY = /^const loaders\b/m
|
|
793
805
|
// The baked client-entry line: `export const clientEntry = "…"`.
|
|
794
806
|
const MANIFEST_CLIENT_ENTRY = /export\s+const\s+clientEntry\s*=\s*["']([^"']+)["']/
|
|
795
807
|
|
|
796
808
|
/**
|
|
797
|
-
* Extract the route-relative file list
|
|
798
|
-
* `routes/`-relative keys `discoverRoutes` produces (e.g. `docs/index.tsx`).
|
|
799
|
-
*
|
|
800
|
-
*
|
|
801
|
-
*
|
|
809
|
+
* Extract the route-relative file list a committed server-manifest declares, as the same
|
|
810
|
+
* `routes/`-relative keys `discoverRoutes` produces (e.g. `docs/index.tsx`). Reads the route map's KEYS,
|
|
811
|
+
* which carry the file extension and no directory prefix - exactly discovery's shape - so the result is
|
|
812
|
+
* independent of the specifier prefix the manifest happened to import with. The `@nifrajs/web` import and
|
|
813
|
+
* the baked `clientEntry`/`styles`/`routeStyles` lines carry no route-map entry and are ignored.
|
|
814
|
+
*
|
|
815
|
+
* `_routesPrefix` is accepted for call-site compatibility but no longer needed: the keys are already
|
|
816
|
+
* prefix-free. Pure - operates on source text.
|
|
802
817
|
*/
|
|
803
|
-
export function parseManifestRouteFiles(source: string,
|
|
818
|
+
export function parseManifestRouteFiles(source: string, _routesPrefix?: string): string[] {
|
|
804
819
|
const files = new Set<string>()
|
|
805
|
-
for (const match of source.matchAll(
|
|
806
|
-
|
|
807
|
-
if (spec === undefined || !spec.startsWith(routesPrefix)) continue
|
|
808
|
-
files.add(spec.slice(routesPrefix.length))
|
|
820
|
+
for (const match of source.matchAll(MANIFEST_ROUTE_ENTRY)) {
|
|
821
|
+
if (match[2] !== undefined) files.add(match[2])
|
|
809
822
|
}
|
|
810
823
|
return [...files].sort()
|
|
811
824
|
}
|
|
@@ -880,7 +893,7 @@ export function resyncServerManifestSource(
|
|
|
880
893
|
clientEntry: parseManifestClientEntry(source) ?? "",
|
|
881
894
|
styles: parseManifestStyles(source),
|
|
882
895
|
routeStyles: parseManifestRouteStyles(source),
|
|
883
|
-
lazy:
|
|
896
|
+
lazy: MANIFEST_IS_LAZY.test(source),
|
|
884
897
|
})
|
|
885
898
|
}
|
|
886
899
|
|
|
@@ -941,6 +954,8 @@ export function formatManifestDrift(
|
|
|
941
954
|
*/
|
|
942
955
|
export async function buildClient(options: BuildClientOptions): Promise<BuildManifest> {
|
|
943
956
|
const { routesDir, outDir, clientModule } = options
|
|
957
|
+
const root = resolvePath(dirname(routesDir))
|
|
958
|
+
await assertIdentityParity(root)
|
|
944
959
|
const resolve = options.resolve ?? ((file: string) => `${routesDir}/${file}`)
|
|
945
960
|
const publicPath = options.publicPath ?? "/assets/"
|
|
946
961
|
mkdirSync(outDir, { recursive: true })
|
|
@@ -1141,6 +1156,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
1141
1156
|
...(css.length > 0 ? { css } : {}),
|
|
1142
1157
|
...(Object.keys(routeStyles).length > 0 ? { routeStyles } : {}),
|
|
1143
1158
|
}
|
|
1159
|
+
assertDevelopmentProductionParity(collectDevelopmentParityInput(routesDir, publicDir), manifest)
|
|
1144
1160
|
writeFileSync(`${outDir}/manifest.json`, JSON.stringify(manifest, null, 2))
|
|
1145
1161
|
return manifest
|
|
1146
1162
|
}
|
package/src/dev-ssr-graph.ts
CHANGED
|
@@ -87,7 +87,7 @@ const appSourceFilter = (root: string): RegExp =>
|
|
|
87
87
|
* including `export ... from`. What it matches is only a candidate: a hit counts as an import only if the
|
|
88
88
|
* transpiler's own scan reported the same specifier, which is what keeps a lookalike string literal out.
|
|
89
89
|
*/
|
|
90
|
-
const SPECIFIER = /\b(from|import|require)(
|
|
90
|
+
const SPECIFIER = /\b(from|import|require)((?:\s*\()?\s*)(["'])([^"'\n]+)\3/g
|
|
91
91
|
|
|
92
92
|
export interface SsrGraphOptions {
|
|
93
93
|
/** The app root. Only files under it (and outside `node_modules`) are tracked. */
|
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
|
}
|
package/src/index.ts
CHANGED
|
@@ -2617,6 +2617,18 @@ export interface GenerateServerManifestOptions {
|
|
|
2617
2617
|
* `createWebApp`, unchanged) + `clientEntry` (baked). Write it to a file and bundle it into the worker
|
|
2618
2618
|
* entry (see `buildServer` in `@nifrajs/web/build`).
|
|
2619
2619
|
*/
|
|
2620
|
+
// Route source files are emitted as EXTENSIONLESS import specifiers. An explicit `.tsx`/`.ts` in an
|
|
2621
|
+
// import is a hard error under a plain `tsc` (TS5097) unless `allowImportingTsExtensions` is set - which
|
|
2622
|
+
// only the nifra template tsconfig does - so the generated manifest failed a bare `tsc` with one error
|
|
2623
|
+
// per route. Every bundler and `moduleResolution` still resolves the extensionless form to the source
|
|
2624
|
+
// file, so dropping the extension costs nothing at build time and makes the output portable. Only a
|
|
2625
|
+
// known TS/JS source extension is stripped; the map KEY keeps its extension (it is the route identifier
|
|
2626
|
+
// `buildManifest` matches, not an import path).
|
|
2627
|
+
const SOURCE_EXTENSION = /\.(?:[mc]?tsx?|[mc]?jsx?)$/
|
|
2628
|
+
function moduleSpecifier(resolved: string): string {
|
|
2629
|
+
return resolved.replace(SOURCE_EXTENSION, "")
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2620
2632
|
export function generateServerManifest(
|
|
2621
2633
|
manifest: Manifest,
|
|
2622
2634
|
options: GenerateServerManifestOptions,
|
|
@@ -2636,7 +2648,7 @@ export function generateServerManifest(
|
|
|
2636
2648
|
"// GENERATED by @nifrajs/web generateServerManifest - route manifest for the disk-less edge",
|
|
2637
2649
|
"// (no filesystem; route imports are static specifiers the bundler resolves). buildManifest is",
|
|
2638
2650
|
"// the same pure logic discoverRoutes feeds, so patterns + layout chains match exactly.",
|
|
2639
|
-
'import { buildManifest } from "@nifrajs/web"',
|
|
2651
|
+
'import { buildManifest, type RouteModule } from "@nifrajs/web"',
|
|
2640
2652
|
]
|
|
2641
2653
|
const clientEntryLine = `export const clientEntry = ${JSON.stringify(clientEntry)}`
|
|
2642
2654
|
// Baked stylesheet URLs so the server entry can pass them to createWebApp → <link rel="stylesheet">.
|
|
@@ -2648,11 +2660,12 @@ export function generateServerManifest(
|
|
|
2648
2660
|
// code-splitting bundler, loaded on first request). The map keys are the route-relative paths
|
|
2649
2661
|
// `buildManifest` expects; the importer it builds calls the per-file loader.
|
|
2650
2662
|
const loaders = files.map(
|
|
2651
|
-
(file) =>
|
|
2663
|
+
(file) =>
|
|
2664
|
+
` ${jsStringLiteral(file)}: () => import(${jsStringLiteral(moduleSpecifier(resolve(file)))}),`,
|
|
2652
2665
|
)
|
|
2653
2666
|
return `${[
|
|
2654
2667
|
...header,
|
|
2655
|
-
"const loaders = {",
|
|
2668
|
+
"const loaders: Record<string, () => Promise<RouteModule>> = {",
|
|
2656
2669
|
...loaders,
|
|
2657
2670
|
"}",
|
|
2658
2671
|
clientEntryLine,
|
|
@@ -2663,12 +2676,14 @@ export function generateServerManifest(
|
|
|
2663
2676
|
}
|
|
2664
2677
|
// Eager: `import * as` per route (all bundled into the entry, parsed at boot). Index-based
|
|
2665
2678
|
// identifiers are collision-proof regardless of filename.
|
|
2666
|
-
const imports = files.map(
|
|
2679
|
+
const imports = files.map(
|
|
2680
|
+
(file, i) => `import * as m${i} from ${JSON.stringify(moduleSpecifier(resolve(file)))}`,
|
|
2681
|
+
)
|
|
2667
2682
|
const entries = files.map((file, i) => ` ${JSON.stringify(file)}: m${i},`)
|
|
2668
2683
|
return `${[
|
|
2669
2684
|
...header,
|
|
2670
2685
|
...imports,
|
|
2671
|
-
"const modules = {",
|
|
2686
|
+
"const modules: Record<string, RouteModule> = {",
|
|
2672
2687
|
...entries,
|
|
2673
2688
|
"}",
|
|
2674
2689
|
clientEntryLine,
|