@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/zones.ts
CHANGED
|
@@ -11,7 +11,15 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { closeSync, existsSync, openSync, readFileSync, readSync, realpathSync } from "node:fs"
|
|
13
13
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"
|
|
14
|
+
import {
|
|
15
|
+
type EntryOwnership,
|
|
16
|
+
type EnvironmentDeclaration,
|
|
17
|
+
entryKey,
|
|
18
|
+
entryOwnership,
|
|
19
|
+
parseEnvironment,
|
|
20
|
+
} from "./internal/package-entries.ts"
|
|
14
21
|
import { SERVER_FN_MODULE } from "./internal/server-boundary.ts"
|
|
22
|
+
import { withoutQuery } from "./internal/without-query.ts"
|
|
15
23
|
|
|
16
24
|
export type Zone =
|
|
17
25
|
/** A page or special file under `routes/` - the frontend half of a route. */
|
|
@@ -39,12 +47,16 @@ export type Classification =
|
|
|
39
47
|
readonly declared?: PackageEnvironment
|
|
40
48
|
/** A third-party file's path inside `node_modules`: `drizzle-orm/node-postgres/index.js`. */
|
|
41
49
|
readonly modulePath?: string
|
|
50
|
+
/** The export subpath (`"./react"`) whose own declared side decided this import. */
|
|
51
|
+
readonly entry?: string
|
|
42
52
|
}
|
|
43
53
|
| {
|
|
44
54
|
readonly zone: "error"
|
|
45
55
|
readonly reason: string
|
|
46
56
|
/** The undeclared package this error is about: every file in it shares the one fix. */
|
|
47
57
|
readonly package?: string
|
|
58
|
+
/** The export subpath the import named, when the package's map gives it no side. */
|
|
59
|
+
readonly entry?: string
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
export type PackageEnvironment = "frontend" | "backend" | "shared" | "library"
|
|
@@ -151,11 +163,14 @@ export interface ZoneConfig {
|
|
|
151
163
|
* longest prefix over a shorter one, and a package's own declaration over all of them.
|
|
152
164
|
*/
|
|
153
165
|
readonly packages?: Readonly<Record<string, PackageEnvironment>>
|
|
154
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* App-relative folders or files holding backend code, beside `backend/` (`"migrations"`). A path
|
|
168
|
+
* listed inside another one's folder overrides it: the most specific entry wins.
|
|
169
|
+
*/
|
|
155
170
|
readonly backend?: readonly string[]
|
|
156
|
-
/** App-relative folders holding frontend code, beside `frontend/`. */
|
|
171
|
+
/** App-relative folders or files holding frontend code, beside `frontend/`. */
|
|
157
172
|
readonly frontend?: readonly string[]
|
|
158
|
-
/** App-relative folders holding code both sides import, beside `shared/`. */
|
|
173
|
+
/** App-relative folders or files holding code both sides import, beside `shared/`. */
|
|
159
174
|
readonly shared?: readonly string[]
|
|
160
175
|
}
|
|
161
176
|
|
|
@@ -189,9 +204,10 @@ const recordOf = (value: unknown): Readonly<Record<string, unknown>> | undefined
|
|
|
189
204
|
: undefined
|
|
190
205
|
|
|
191
206
|
/**
|
|
192
|
-
* Validate a `zones` setting. `source` names where it came from in the error. Folders are
|
|
193
|
-
* app-relative paths (a trailing `/**` is accepted and dropped); any other glob is refused, so
|
|
194
|
-
* pattern can never match less than its author meant without saying so.
|
|
207
|
+
* Validate a `zones` setting. `source` names where it came from in the error. Folders and files are
|
|
208
|
+
* plain app-relative paths (a trailing `/**` is accepted and dropped); any other glob is refused, so
|
|
209
|
+
* a pattern can never match less than its author meant without saying so. One path under two sides
|
|
210
|
+
* is refused; a path nested in another's folder is not, since the most specific one decides.
|
|
195
211
|
*/
|
|
196
212
|
export function parseZoneConfig(value: unknown, source: string): ZoneConfig {
|
|
197
213
|
const fail = (message: string): never => {
|
|
@@ -228,7 +244,7 @@ export function parseZoneConfig(value: unknown, source: string): ZoneConfig {
|
|
|
228
244
|
if (raw === undefined) continue
|
|
229
245
|
const entries: readonly unknown[] = Array.isArray(raw)
|
|
230
246
|
? raw
|
|
231
|
-
: fail(`"${side}" must be an array of app-relative folders`)
|
|
247
|
+
: fail(`"${side}" must be an array of app-relative folders or files`)
|
|
232
248
|
const folders: string[] = []
|
|
233
249
|
for (const entry of entries) {
|
|
234
250
|
const folder = (typeof entry === "string" ? entry : fail(`"${side}" must hold strings`))
|
|
@@ -238,18 +254,15 @@ export function parseZoneConfig(value: unknown, source: string): ZoneConfig {
|
|
|
238
254
|
.replace(/\/+$/, "")
|
|
239
255
|
const top = folder.split("/")[0] ?? ""
|
|
240
256
|
if (folder === "" || folder.startsWith("/") || /^[a-z]:/i.test(folder))
|
|
241
|
-
fail(`"${side}" entry "${entry}" must be a folder relative to the app`)
|
|
257
|
+
fail(`"${side}" entry "${entry}" must be a folder or file relative to the app`)
|
|
242
258
|
if (folder.split("/").includes("..")) fail(`"${side}" entry "${entry}" may not contain ".."`)
|
|
243
259
|
if (/[*?[\]{}!]/.test(folder))
|
|
244
|
-
fail(`"${side}" entry "${entry}" must be a plain
|
|
260
|
+
fail(`"${side}" entry "${entry}" must be a plain path (a trailing "/**" is allowed)`)
|
|
245
261
|
if (RESERVED_FOLDERS.has(top) || top.startsWith(".nifra"))
|
|
246
262
|
fail(`"${side}" entry "${entry}" is inside ${top}/, which already has its own rules`)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (nested && otherSide !== side)
|
|
251
|
-
fail(`"${folder}" (${side}) overlaps "${other}" (${otherSide}); a folder has one side`)
|
|
252
|
-
}
|
|
263
|
+
const other = owner.get(folder)
|
|
264
|
+
if (other !== undefined && other !== side)
|
|
265
|
+
fail(`"${folder}" is listed as both ${other} and ${side}; a path has one side`)
|
|
253
266
|
owner.set(folder, side)
|
|
254
267
|
folders.push(folder)
|
|
255
268
|
}
|
|
@@ -301,8 +314,16 @@ const suggestedPattern = (name: string): string =>
|
|
|
301
314
|
|
|
302
315
|
export interface ZoneClassifier {
|
|
303
316
|
readonly appRoot: string
|
|
304
|
-
/**
|
|
305
|
-
|
|
317
|
+
/**
|
|
318
|
+
* Classify a file by its absolute (or appRoot-relative) path. Results are cached per path.
|
|
319
|
+
*
|
|
320
|
+
* With `specifier`, the import as written that resolved to `file`: when it names a package that
|
|
321
|
+
* gives each export entry its own side (`"nifra": { "environment": { ".": "backend", "./react":
|
|
322
|
+
* "frontend" } }`), the entry it names decides, since two entries may share one file. A specifier
|
|
323
|
+
* naming an entry the map leaves out is an error. The resolved file must belong to that entry, as
|
|
324
|
+
* the package's own imports reach it; one only other entries reach is judged by its path.
|
|
325
|
+
*/
|
|
326
|
+
classify(file: string, specifier?: string): Classification
|
|
306
327
|
}
|
|
307
328
|
|
|
308
329
|
const toPosix = (path: string): string => path.replaceAll("\\", "/")
|
|
@@ -356,7 +377,19 @@ function nodeModulesPackage(path: string): string | undefined {
|
|
|
356
377
|
interface PackageInfo {
|
|
357
378
|
readonly dir: string
|
|
358
379
|
readonly name: string | undefined
|
|
359
|
-
readonly
|
|
380
|
+
readonly declaration: EnvironmentDeclaration
|
|
381
|
+
readonly manifest: Readonly<Record<string, unknown>>
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const DECLARATION_FILE = /\.d\.[cm]?ts$/
|
|
385
|
+
|
|
386
|
+
/** The one side a file reached by several mapped entries takes: what each entry's importers may import. */
|
|
387
|
+
function ownersSide(owners: ReadonlySet<PackageEnvironment>): PackageEnvironment {
|
|
388
|
+
const [only] = owners
|
|
389
|
+
if (owners.size === 1 && only !== undefined) return only
|
|
390
|
+
return owners.has("library") || (owners.has("frontend") && owners.has("backend"))
|
|
391
|
+
? "library"
|
|
392
|
+
: "shared"
|
|
360
393
|
}
|
|
361
394
|
|
|
362
395
|
/**
|
|
@@ -374,11 +407,12 @@ export function createZoneClassifier(options: ZoneClassifierOptions): ZoneClassi
|
|
|
374
407
|
const configuredFolders: Array<readonly [string, "backend" | "frontend" | "shared"]> = []
|
|
375
408
|
for (const side of FOLDER_SIDES)
|
|
376
409
|
for (const folder of zones[side] ?? []) configuredFolders.push([folder, side])
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
410
|
+
// Longest first, so the most specific entry covering a file decides its side.
|
|
411
|
+
configuredFolders.sort(([a], [b]) => b.length - a.length)
|
|
412
|
+
const configuredPath = (
|
|
413
|
+
rel: string,
|
|
414
|
+
): readonly [string, "backend" | "frontend" | "shared"] | undefined =>
|
|
415
|
+
configuredFolders.find(([folder]) => rel === folder || rel.startsWith(`${folder}/`))
|
|
382
416
|
const cache = new Map<string, Classification>()
|
|
383
417
|
const packages = new Map<string, PackageInfo | null>()
|
|
384
418
|
|
|
@@ -395,17 +429,17 @@ export function createZoneClassifier(options: ZoneClassifierOptions): ZoneClassi
|
|
|
395
429
|
seen.push(current)
|
|
396
430
|
const manifest = join(current, "package.json")
|
|
397
431
|
if (existsSync(manifest)) {
|
|
398
|
-
let parsed:
|
|
432
|
+
let parsed: Readonly<Record<string, unknown>> = {}
|
|
399
433
|
try {
|
|
400
|
-
parsed = JSON.parse(readFileSync(manifest, "utf8"))
|
|
434
|
+
parsed = recordOf(JSON.parse(readFileSync(manifest, "utf8"))) ?? {}
|
|
401
435
|
} catch {
|
|
402
436
|
// An unreadable manifest declares nothing; the caller reports the missing declaration.
|
|
403
437
|
}
|
|
404
438
|
const info: PackageInfo = {
|
|
405
439
|
dir: current,
|
|
406
440
|
name: typeof parsed.name === "string" ? parsed.name : undefined,
|
|
407
|
-
environment
|
|
408
|
-
|
|
441
|
+
declaration: parseEnvironment(recordOf(parsed.nifra)?.environment),
|
|
442
|
+
manifest: parsed,
|
|
409
443
|
}
|
|
410
444
|
for (const visited of seen) packages.set(visited, info)
|
|
411
445
|
return info
|
|
@@ -419,15 +453,96 @@ export function createZoneClassifier(options: ZoneClassifierOptions): ZoneClassi
|
|
|
419
453
|
}
|
|
420
454
|
}
|
|
421
455
|
|
|
456
|
+
const ownerships = new Map<string, EntryOwnership>()
|
|
457
|
+
const ownershipOf = (
|
|
458
|
+
info: PackageInfo,
|
|
459
|
+
entries: Readonly<Record<string, PackageEnvironment>>,
|
|
460
|
+
): EntryOwnership => {
|
|
461
|
+
let found = ownerships.get(info.dir)
|
|
462
|
+
if (found === undefined) {
|
|
463
|
+
found = entryOwnership(info.dir, info.manifest, entries)
|
|
464
|
+
ownerships.set(info.dir, found)
|
|
465
|
+
}
|
|
466
|
+
return found
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** The package `file` is part of, when that package is not the app: installed, or a workspace sibling. */
|
|
470
|
+
const packageOf = (file: string): PackageInfo | undefined => {
|
|
471
|
+
const thirdParty = nodeModulesPackage(file)
|
|
472
|
+
if (thirdParty !== undefined) {
|
|
473
|
+
const at = file.lastIndexOf("/node_modules/") + "/node_modules/".length
|
|
474
|
+
return packageFor(file.slice(0, at + thirdParty.length))
|
|
475
|
+
}
|
|
476
|
+
return isInside(file, appRoot) ? undefined : packageFor(toPosix(dirname(file)))
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** What a side declared for `file` (by its package, an entry of it, or the app) makes it. */
|
|
480
|
+
const declaredAs = (
|
|
481
|
+
file: string,
|
|
482
|
+
info: PackageInfo | undefined,
|
|
483
|
+
side: PackageEnvironment,
|
|
484
|
+
entry?: string,
|
|
485
|
+
): Classification => {
|
|
486
|
+
const thirdParty = nodeModulesPackage(file)
|
|
487
|
+
const named = entry === undefined || info?.name === undefined ? {} : { packageName: info.name }
|
|
488
|
+
const decided = entry === undefined ? {} : { entry }
|
|
489
|
+
if (thirdParty !== undefined) {
|
|
490
|
+
return {
|
|
491
|
+
zone: "library",
|
|
492
|
+
packageName: thirdParty,
|
|
493
|
+
modulePath: file.slice(file.lastIndexOf("/node_modules/") + "/node_modules/".length),
|
|
494
|
+
declared: side,
|
|
495
|
+
...decided,
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (side !== "library") return { zone: side, ...named, ...decided }
|
|
499
|
+
return {
|
|
500
|
+
zone: "library",
|
|
501
|
+
...(info?.name ? { packageName: info.name } : {}),
|
|
502
|
+
declared: "library",
|
|
503
|
+
...decided,
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** A file of a package that declares a side per entry, judged by the entries that reach it. */
|
|
508
|
+
const byOwners = (
|
|
509
|
+
file: string,
|
|
510
|
+
info: PackageInfo,
|
|
511
|
+
entries: Readonly<Record<string, PackageEnvironment>>,
|
|
512
|
+
): Classification => {
|
|
513
|
+
const ownership = ownershipOf(info, entries)
|
|
514
|
+
const owners = ownership.owners.get(file)
|
|
515
|
+
if (owners !== undefined) return declaredAs(file, info, ownersSide(owners))
|
|
516
|
+
const shown = nodeModulesPackage(file) === undefined ? file : file.slice(info.dir.length + 1)
|
|
517
|
+
const left =
|
|
518
|
+
ownership.unmapped.length === 0
|
|
519
|
+
? ""
|
|
520
|
+
: ` (the map leaves out ${ownership.unmapped.map((key) => `"${key}"`).join(", ")})`
|
|
521
|
+
return {
|
|
522
|
+
zone: "error",
|
|
523
|
+
reason: `"${shown}" is in package "${info.name ?? info.dir}", whose "nifra.environment" gives each entry its own side, and no mapped entry reaches this file${left}. Import the package through a mapped entry`,
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const invalidDeclaration = (info: PackageInfo, problem: string): Classification => {
|
|
528
|
+
const reason = `${info.dir}/package.json "nifra.environment": ${problem}`
|
|
529
|
+
return info.name === undefined
|
|
530
|
+
? { zone: "error", reason }
|
|
531
|
+
: { zone: "error", reason, package: info.name }
|
|
532
|
+
}
|
|
533
|
+
|
|
422
534
|
const classifyUncached = (file: string): Classification => {
|
|
423
535
|
if (generated.has(file)) return { zone: "generated" }
|
|
424
536
|
const thirdParty = nodeModulesPackage(file)
|
|
425
537
|
if (thirdParty !== undefined) {
|
|
426
|
-
const info =
|
|
538
|
+
const info = packageOf(file)
|
|
539
|
+
const declaration = info?.declaration ?? { kind: "none" }
|
|
540
|
+
if (info !== undefined && declaration.kind === "invalid")
|
|
541
|
+
return invalidDeclaration(info, declaration.problem)
|
|
542
|
+
if (info !== undefined && declaration.kind === "entries")
|
|
543
|
+
return byOwners(file, info, declaration.entries)
|
|
427
544
|
const declared =
|
|
428
|
-
|
|
429
|
-
? (info.environment as PackageEnvironment)
|
|
430
|
-
: mappedSide(zones.packages, thirdParty)
|
|
545
|
+
declaration.kind === "package" ? declaration.side : mappedSide(zones.packages, thirdParty)
|
|
431
546
|
return {
|
|
432
547
|
zone: "library",
|
|
433
548
|
packageName: thirdParty,
|
|
@@ -472,13 +587,20 @@ export function createZoneClassifier(options: ZoneClassifierOptions): ZoneClassi
|
|
|
472
587
|
if (suffix === "shared") return { zone: "shared" }
|
|
473
588
|
return { zone: "route-frontend" }
|
|
474
589
|
}
|
|
475
|
-
const
|
|
590
|
+
const configured = ZONE_FOLDERS[top] === undefined ? configuredPath(rel) : undefined
|
|
591
|
+
const folder = ZONE_FOLDERS[top] ?? configured?.[1]
|
|
476
592
|
if (folder !== undefined) {
|
|
477
593
|
if (suffix === undefined || suffix === folder) return { zone: folder }
|
|
478
594
|
if (suffix === "fn" && folder === "backend") return { zone: "fn" }
|
|
595
|
+
const where =
|
|
596
|
+
configured === undefined
|
|
597
|
+
? `sits in ${top}/`
|
|
598
|
+
: configured[0] === rel
|
|
599
|
+
? `is listed as ${folder} in "nifra.zones"`
|
|
600
|
+
: `sits in ${configured[0]}/, which "nifra.zones" lists as ${folder},`
|
|
479
601
|
return {
|
|
480
602
|
zone: "error",
|
|
481
|
-
reason: `"${rel}"
|
|
603
|
+
reason: `"${rel}" ${where} but its ".${suffix}" suffix says otherwise. A file belongs to one side: move it, or drop the suffix`,
|
|
482
604
|
}
|
|
483
605
|
}
|
|
484
606
|
if (suffix !== undefined) return { zone: suffix === "fn" ? "fn" : suffix }
|
|
@@ -486,19 +608,14 @@ export function createZoneClassifier(options: ZoneClassifierOptions): ZoneClassi
|
|
|
486
608
|
}
|
|
487
609
|
// First-party code outside the app: a workspace package, or a relative import out of the app.
|
|
488
610
|
const info = packageFor(toPosix(dirname(file)))
|
|
611
|
+
const declaration = info?.declaration ?? { kind: "none" }
|
|
612
|
+
if (info !== undefined && declaration.kind === "invalid")
|
|
613
|
+
return invalidDeclaration(info, declaration.problem)
|
|
614
|
+
if (info !== undefined && declaration.kind === "entries")
|
|
615
|
+
return byOwners(file, info, declaration.entries)
|
|
489
616
|
const environment =
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
: mappedSide(zones.packages, info?.name)
|
|
493
|
-
if (environment !== undefined) {
|
|
494
|
-
return environment === "library"
|
|
495
|
-
? {
|
|
496
|
-
zone: "library",
|
|
497
|
-
...(info?.name ? { packageName: info.name } : {}),
|
|
498
|
-
declared: "library",
|
|
499
|
-
}
|
|
500
|
-
: { zone: environment as Zone }
|
|
501
|
-
}
|
|
617
|
+
declaration.kind === "package" ? declaration.side : mappedSide(zones.packages, info?.name)
|
|
618
|
+
if (environment !== undefined) return declaredAs(file, info, environment)
|
|
502
619
|
const declare = `"nifra": { "environment": "frontend" | "backend" | "shared" | "library" }`
|
|
503
620
|
if (info?.name === undefined) {
|
|
504
621
|
return {
|
|
@@ -514,16 +631,57 @@ export function createZoneClassifier(options: ZoneClassifierOptions): ZoneClassi
|
|
|
514
631
|
}
|
|
515
632
|
}
|
|
516
633
|
|
|
634
|
+
const byPath = (file: string): Classification => {
|
|
635
|
+
const cached = cache.get(file)
|
|
636
|
+
if (cached !== undefined) return cached
|
|
637
|
+
const result = classifyUncached(file)
|
|
638
|
+
cache.set(file, result)
|
|
639
|
+
return result
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Keyed by file and subpath: two entries may share one file and declare different sides.
|
|
643
|
+
const imports = new Map<string, Classification>()
|
|
644
|
+
const byImport = (file: string, specifier: string): Classification => {
|
|
645
|
+
const name = specifierPackage(specifier)
|
|
646
|
+
const info = name === undefined ? undefined : packageOf(file)
|
|
647
|
+
if (name === undefined || info?.name !== name || info.declaration.kind !== "entries")
|
|
648
|
+
return byPath(file)
|
|
649
|
+
const subpath = entrySubpath(specifier, name)
|
|
650
|
+
const cacheKey = `${file}\0${subpath}`
|
|
651
|
+
const cached = imports.get(cacheKey)
|
|
652
|
+
if (cached !== undefined) return cached
|
|
653
|
+
const { entries } = info.declaration
|
|
654
|
+
const key = entryKey(entries, subpath)
|
|
655
|
+
const side = key === undefined ? undefined : entries[key]
|
|
656
|
+
let result: Classification
|
|
657
|
+
if (side === undefined) {
|
|
658
|
+
const mapped = Object.keys(entries)
|
|
659
|
+
.map((entry) => `"${entry}"`)
|
|
660
|
+
.join(", ")
|
|
661
|
+
result = {
|
|
662
|
+
zone: "error",
|
|
663
|
+
reason: `"${specifier}" names the "${subpath}" entry of package "${name}", whose "nifra.environment" map gives it no side (it maps ${mapped}). Map the entry, or import one that is mapped`,
|
|
664
|
+
entry: subpath,
|
|
665
|
+
}
|
|
666
|
+
} else {
|
|
667
|
+
// The resolver's pick must belong to the named entry; a file only other entries reach (an alias
|
|
668
|
+
// pointed at it) is judged by its path. A declaration file is types only and erased.
|
|
669
|
+
const owners = ownershipOf(info, entries).owners.get(file)
|
|
670
|
+
result =
|
|
671
|
+
owners !== undefined && !owners.has(side) && !DECLARATION_FILE.test(file)
|
|
672
|
+
? byPath(file)
|
|
673
|
+
: declaredAs(file, info, side, subpath)
|
|
674
|
+
}
|
|
675
|
+
imports.set(cacheKey, result)
|
|
676
|
+
return result
|
|
677
|
+
}
|
|
678
|
+
|
|
517
679
|
return {
|
|
518
680
|
appRoot,
|
|
519
|
-
classify(input: string): Classification {
|
|
681
|
+
classify(input: string, specifier?: string): Classification {
|
|
520
682
|
const absolute = isAbsolute(input) ? input : resolve(appRoot, input)
|
|
521
683
|
const file = realFile(absolute)
|
|
522
|
-
|
|
523
|
-
if (cached !== undefined) return cached
|
|
524
|
-
const result = classifyUncached(file)
|
|
525
|
-
cache.set(file, result)
|
|
526
|
-
return result
|
|
684
|
+
return specifier === undefined ? byPath(file) : byImport(file, specifier)
|
|
527
685
|
},
|
|
528
686
|
}
|
|
529
687
|
}
|
|
@@ -554,6 +712,27 @@ export function specifierPackage(specifier: string): string | undefined {
|
|
|
554
712
|
return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0]
|
|
555
713
|
}
|
|
556
714
|
|
|
715
|
+
/** The export subpath (`"."`, `"./react"`) a bare specifier of package `name` names. */
|
|
716
|
+
const entrySubpath = (specifier: string, name: string): string =>
|
|
717
|
+
`.${withoutQuery(specifier.slice(name.length))}`
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* The side a package's `"nifra": { "environment": ... }` value gives the bare import `specifier` of it:
|
|
721
|
+
* the package-wide string, or the side of the entry it names - the map's exact subpath, else its most
|
|
722
|
+
* specific `*` pattern. `undefined` when it gives none, as a map that leaves the entry out does.
|
|
723
|
+
*/
|
|
724
|
+
export function declaredSide(
|
|
725
|
+
environment: unknown,
|
|
726
|
+
specifier: string,
|
|
727
|
+
): PackageEnvironment | undefined {
|
|
728
|
+
const declaration = parseEnvironment(environment)
|
|
729
|
+
if (declaration.kind === "package") return declaration.side
|
|
730
|
+
const name = specifierPackage(specifier)
|
|
731
|
+
if (declaration.kind !== "entries" || name === undefined) return undefined
|
|
732
|
+
const key = entryKey(declaration.entries, entrySubpath(specifier, name))
|
|
733
|
+
return key === undefined ? undefined : declaration.entries[key]
|
|
734
|
+
}
|
|
735
|
+
|
|
557
736
|
/**
|
|
558
737
|
* Why a classified file may not ship to a browser, or `undefined` when it may. A `drizzle-orm` driver
|
|
559
738
|
* entry is recognized by the import as written (`specifier`) or by the file's path in the package.
|
|
@@ -564,13 +743,19 @@ export function browserDenial(
|
|
|
564
743
|
): string | undefined {
|
|
565
744
|
if (classification.zone === "error") return classification.reason
|
|
566
745
|
if (!BROWSER_ZONES.has(classification.zone)) {
|
|
746
|
+
if (classification.entry !== undefined)
|
|
747
|
+
return `package "${classification.packageName}" declares its "${classification.entry}" entry ${classification.zone}-only`
|
|
567
748
|
return classification.zone === "route-backend"
|
|
568
749
|
? "it is a route's backend half, which runs on the server only"
|
|
569
750
|
: "it is backend code"
|
|
570
751
|
}
|
|
571
752
|
if (classification.zone !== "library") return undefined
|
|
572
753
|
const name = classification.packageName
|
|
573
|
-
if (classification.declared === "backend")
|
|
754
|
+
if (classification.declared === "backend") {
|
|
755
|
+
return classification.entry === undefined
|
|
756
|
+
? `package "${name}" declares itself backend-only`
|
|
757
|
+
: `package "${name}" declares its "${classification.entry}" entry backend-only`
|
|
758
|
+
}
|
|
574
759
|
if (name !== undefined && SERVER_PACKAGES.includes(name)) {
|
|
575
760
|
return `package "${name}" is server code (a driver, server SDK or credential helper)`
|
|
576
761
|
}
|