@nifrajs/web 3.1.0 → 3.3.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.
Files changed (114) hide show
  1. package/dist/build-plan.d.ts +228 -0
  2. package/dist/build-plan.d.ts.map +1 -0
  3. package/dist/build-plan.js +514 -0
  4. package/dist/build-plan.js.map +1 -0
  5. package/dist/build-vite.d.ts +2 -1
  6. package/dist/build-vite.d.ts.map +1 -1
  7. package/dist/build-vite.js.map +1 -1
  8. package/dist/build.d.ts +2 -197
  9. package/dist/build.d.ts.map +1 -1
  10. package/dist/build.js +61 -502
  11. package/dist/build.js.map +1 -1
  12. package/dist/chunk-names.d.ts.map +1 -1
  13. package/dist/chunk-names.js +3 -2
  14. package/dist/chunk-names.js.map +1 -1
  15. package/dist/dev-ssr-graph.d.ts.map +1 -1
  16. package/dist/dev-ssr-graph.js +63 -28
  17. package/dist/dev-ssr-graph.js.map +1 -1
  18. package/dist/diagnostic.d.ts.map +1 -1
  19. package/dist/diagnostic.js +9 -2
  20. package/dist/diagnostic.js.map +1 -1
  21. package/dist/fonts-google.d.ts.map +1 -1
  22. package/dist/fonts-google.js +3 -1
  23. package/dist/fonts-google.js.map +1 -1
  24. package/dist/fs.d.ts.map +1 -1
  25. package/dist/fs.js +2 -1
  26. package/dist/fs.js.map +1 -1
  27. package/dist/index.d.ts +15 -609
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +22 -1935
  30. package/dist/index.js.map +1 -1
  31. package/dist/internal/codegen.d.ts +68 -0
  32. package/dist/internal/codegen.d.ts.map +1 -0
  33. package/dist/internal/codegen.js +317 -0
  34. package/dist/internal/codegen.js.map +1 -0
  35. package/dist/internal/head-merge.d.ts +2 -0
  36. package/dist/internal/head-merge.d.ts.map +1 -1
  37. package/dist/internal/head-merge.js +17 -1
  38. package/dist/internal/head-merge.js.map +1 -1
  39. package/dist/internal/parity.d.ts +7 -0
  40. package/dist/internal/parity.d.ts.map +1 -1
  41. package/dist/internal/parity.js +212 -30
  42. package/dist/internal/parity.js.map +1 -1
  43. package/dist/internal/render-document.d.ts +372 -0
  44. package/dist/internal/render-document.d.ts.map +1 -0
  45. package/dist/internal/render-document.js +744 -0
  46. package/dist/internal/render-document.js.map +1 -0
  47. package/dist/internal/runtime-contract.d.ts +48 -0
  48. package/dist/internal/runtime-contract.d.ts.map +1 -0
  49. package/dist/internal/runtime-contract.js +27 -0
  50. package/dist/internal/runtime-contract.js.map +1 -0
  51. package/dist/internal/web-app.d.ts +171 -0
  52. package/dist/internal/web-app.d.ts.map +1 -0
  53. package/dist/internal/web-app.js +844 -0
  54. package/dist/internal/web-app.js.map +1 -0
  55. package/dist/islands.d.ts +48 -0
  56. package/dist/islands.d.ts.map +1 -1
  57. package/dist/islands.js +79 -0
  58. package/dist/islands.js.map +1 -1
  59. package/dist/module-graph.d.ts.map +1 -1
  60. package/dist/module-graph.js +22 -9
  61. package/dist/module-graph.js.map +1 -1
  62. package/dist/nano.d.ts +125 -0
  63. package/dist/nano.d.ts.map +1 -0
  64. package/dist/nano.js +190 -0
  65. package/dist/nano.js.map +1 -0
  66. package/dist/plugins/css-modules.js +2 -2
  67. package/dist/plugins/css-modules.js.map +1 -1
  68. package/dist/plugins/kit.d.ts +12 -0
  69. package/dist/plugins/kit.d.ts.map +1 -1
  70. package/dist/plugins/kit.js +62 -12
  71. package/dist/plugins/kit.js.map +1 -1
  72. package/dist/plugins/postcss.js +2 -2
  73. package/dist/plugins/postcss.js.map +1 -1
  74. package/dist/plugins/scss.js +2 -2
  75. package/dist/plugins/scss.js.map +1 -1
  76. package/dist/plugins/stylex.d.ts +70 -0
  77. package/dist/plugins/stylex.d.ts.map +1 -0
  78. package/dist/plugins/stylex.js +166 -0
  79. package/dist/plugins/stylex.js.map +1 -0
  80. package/dist/plugins/svg.js +2 -2
  81. package/dist/plugins/svg.js.map +1 -1
  82. package/dist/router.d.ts +1 -1
  83. package/dist/router.js +23 -23
  84. package/dist/router.js.map +1 -1
  85. package/dist/vite.d.ts.map +1 -1
  86. package/dist/vite.js +26 -10
  87. package/dist/vite.js.map +1 -1
  88. package/package.json +39 -3
  89. package/src/build-plan.ts +741 -0
  90. package/src/build-vite.ts +1 -4
  91. package/src/build.ts +84 -716
  92. package/src/chunk-names.ts +3 -2
  93. package/src/dev-ssr-graph.ts +63 -28
  94. package/src/diagnostic.ts +14 -2
  95. package/src/fonts-google.ts +3 -1
  96. package/src/fs.ts +2 -1
  97. package/src/index.ts +73 -2705
  98. package/src/internal/codegen.ts +400 -0
  99. package/src/internal/head-merge.ts +19 -1
  100. package/src/internal/parity.ts +215 -35
  101. package/src/internal/render-document.ts +1116 -0
  102. package/src/internal/runtime-contract.ts +54 -0
  103. package/src/internal/web-app.ts +1114 -0
  104. package/src/islands.ts +85 -0
  105. package/src/module-graph.ts +23 -9
  106. package/src/nano.ts +246 -0
  107. package/src/plugins/css-modules.ts +2 -2
  108. package/src/plugins/kit.ts +65 -12
  109. package/src/plugins/postcss.ts +2 -2
  110. package/src/plugins/scss.ts +2 -2
  111. package/src/plugins/stylex.ts +298 -0
  112. package/src/plugins/svg.ts +2 -2
  113. package/src/router.ts +24 -24
  114. package/src/vite.ts +33 -9
@@ -6,9 +6,9 @@
6
6
  * manifest normalization so those callers cannot quietly grow separate rules.
7
7
  */
8
8
 
9
- import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs"
9
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs"
10
10
  import { lstat, realpath, stat } from "node:fs/promises"
11
- import { dirname, join, relative, resolve, sep } from "node:path"
11
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"
12
12
  import {
13
13
  matchesSingleCopyDeclaration,
14
14
  readSingleCopyDeclaration,
@@ -32,6 +32,12 @@ const MAX_LINK_PROBES = 4_096
32
32
  export interface IdentityParityCopy {
33
33
  readonly version: string
34
34
  readonly path: string
35
+ /**
36
+ * The resolved realpath of the copy. `path` is display-relative to the invoked directory, which
37
+ * reads well in a report but cannot be pasted into a resolver or an editor from anywhere else.
38
+ * Absent on a finding built by hand (a test fixture, an older cached result).
39
+ */
40
+ readonly absolutePath?: string
35
41
  readonly importers: readonly string[]
36
42
  }
37
43
 
@@ -156,14 +162,121 @@ export interface DevelopmentParityInput {
156
162
  }
157
163
 
158
164
  const SOURCE_EXTENSIONS = /\.(?:c|m)?(?:j|t)sx?$|\.(?:mdx|svelte|vue)$/
159
- /** A first-party reference to a stylesheet by an explicit path specifier. Covers static `import`,
160
- * re-export `from`, dynamic `import(...)`, and `require(...)`. It stays deliberately *sound*: it only
161
- * matches when a stylesheet specifier genuinely exists, never on an unreferenced `.css` file in the
162
- * tree. With the css parity comparison now directional (a scanner miss passes, a false positive fails),
163
- * a loose pattern would fail correct apps. Specifiers that cannot end in a stylesheet extension (a bare
164
- * package `exports` subpath) stay unreachable without a resolver; those fall in the passing direction. */
165
- const CSS_IMPORT =
166
- /(?:import\s+(?:[^"']+\s+from\s+)?|from\s+|(?:import|require)\s*\(\s*)["'][^"']+\.(?:css|scss|sass|less|styl)(?:\?[^"']*)?["']/i
165
+ /** A stylesheet path that the source scanner can prove is a real explicit import. */
166
+ const CSS_SPECIFIER = /\.(?:css|scss|sass|less|styl)(?:\?[^"'`]*)?$/i
167
+
168
+ type CssToken =
169
+ | { readonly kind: "identifier"; readonly value: string; readonly lineStart: boolean }
170
+ | { readonly kind: "string"; readonly value: string }
171
+ | { readonly kind: "punctuator"; readonly value: string }
172
+
173
+ /**
174
+ * Tokenize only the small part of TypeScript/JS needed for the CSS contract.
175
+ *
176
+ * A regex over source text is not sound here: documentation routes contain code examples such as
177
+ * `import "./app.css"` in template literals, and comments can contain the same text. Those are not
178
+ * imports and must not make a style-free production build fail. This intentionally conservative lexer
179
+ * drops comments and complete template literals (including `${...}` expressions); missing a stylesheet
180
+ * is the passing direction of this parity check, while claiming one that is not imported is not.
181
+ */
182
+ const cssTokens = (source: string): readonly CssToken[] => {
183
+ const tokens: CssToken[] = []
184
+ let lineStart = true
185
+ for (let index = 0; index < source.length; ) {
186
+ const char = source[index]
187
+ if (char === undefined) break
188
+ if (/\s/.test(char)) {
189
+ if (char === "\n" || char === "\r") lineStart = true
190
+ index++
191
+ continue
192
+ }
193
+ if (char === "/" && source[index + 1] === "/") {
194
+ index += 2
195
+ while (index < source.length && source[index] !== "\n") index++
196
+ continue
197
+ }
198
+ if (char === "/" && source[index + 1] === "*") {
199
+ const commentStart = index
200
+ index += 2
201
+ while (index < source.length && !(source[index] === "*" && source[index + 1] === "/")) index++
202
+ index = Math.min(source.length, index + 2)
203
+ if (/[\r\n]/.test(source.slice(commentStart, index))) lineStart = true
204
+ continue
205
+ }
206
+ if (char === "`") {
207
+ const templateStart = index
208
+ index++
209
+ while (index < source.length) {
210
+ const templateChar = source[index]
211
+ if (templateChar === "\\") {
212
+ index += 2
213
+ continue
214
+ }
215
+ index++
216
+ if (templateChar === "`") break
217
+ }
218
+ lineStart = /[\r\n]/.test(source.slice(templateStart, index))
219
+ continue
220
+ }
221
+ if (char === '"' || char === "'") {
222
+ const quote = char
223
+ const start = ++index
224
+ while (index < source.length) {
225
+ const stringChar = source[index]
226
+ if (stringChar === "\\") {
227
+ index += 2
228
+ continue
229
+ }
230
+ index++
231
+ if (stringChar === quote) break
232
+ }
233
+ tokens.push({ kind: "string", value: source.slice(start, Math.max(start, index - 1)) })
234
+ lineStart = false
235
+ continue
236
+ }
237
+ if (/[A-Za-z_$]/.test(char)) {
238
+ const start = index++
239
+ while (index < source.length && /[A-Za-z0-9_$]/.test(source[index] ?? "")) index++
240
+ tokens.push({ kind: "identifier", value: source.slice(start, index), lineStart })
241
+ lineStart = false
242
+ continue
243
+ }
244
+ tokens.push({ kind: "punctuator", value: char })
245
+ lineStart = false
246
+ index++
247
+ }
248
+ return tokens
249
+ }
250
+
251
+ const isStylesheetToken = (token: CssToken | undefined): boolean =>
252
+ token?.kind === "string" && CSS_SPECIFIER.test(token.value)
253
+
254
+ /** Detect static imports, re-exports, literal dynamic imports, and literal require calls. */
255
+ const hasStylesheetImport = (source: string): boolean => {
256
+ const tokens = cssTokens(source)
257
+ for (let index = 0; index < tokens.length; index++) {
258
+ const token = tokens[index]
259
+ if (token?.kind !== "identifier") continue
260
+ const next = tokens[index + 1]
261
+ if (token.value === "import") {
262
+ if (token.lineStart && isStylesheetToken(next)) return true
263
+ if (next?.value === "(" && isStylesheetToken(tokens[index + 2])) return true
264
+ }
265
+ if (token.value === "require" && next?.value === "(" && isStylesheetToken(tokens[index + 2]))
266
+ return true
267
+ if (token.value === "from" && isStylesheetToken(next)) {
268
+ for (let previous = index - 1; previous >= 0; previous--) {
269
+ const candidate = tokens[previous]
270
+ if (candidate?.kind === "identifier" && candidate.lineStart) {
271
+ if (candidate.value === "import" || candidate.value === "export") return true
272
+ break
273
+ }
274
+ if (candidate?.value === ";") break
275
+ }
276
+ }
277
+ }
278
+ return false
279
+ }
167
280
  /** A single-file-component `<style>` block (Svelte/Vue). The bundler extracts these into the app
168
281
  * stylesheet even though no `import "...css"` statement exists, so the dev contract must count them
169
282
  * or a scoped-style component would look style-free next to a production manifest that carries css. */
@@ -219,8 +332,85 @@ const pathExists = (path: string): Promise<boolean> =>
219
332
  () => false,
220
333
  )
221
334
 
222
- const pathInside = (root: string, path: string): boolean =>
223
- path === root || path.startsWith(root.endsWith(sep) ? root : `${root}${sep}`)
335
+ /** The real path, or the input when it cannot be resolved. Roots are compared against each other and
336
+ * printed side by side, so they have to be normalized the same way (`/var` vs `/private/var`). */
337
+ const realpathOrSelf = (path: string): string => {
338
+ try {
339
+ // The native Windows resolver is the one that knows how to expand 8.3 aliases such as
340
+ // `RUNNER~1`. The portable resolver can preserve the spelling it was given, which makes two
341
+ // handles to the same checkout look like different install roots.
342
+ return process.platform === "win32" ? realpathSync.native(path) : realpathSync(path)
343
+ } catch {
344
+ return path
345
+ }
346
+ }
347
+
348
+ /** Canonical comparison form for containment. Windows paths are case-insensitive and CI can expose
349
+ * the same directory once through a short name (`RUNNER~1`) and once through its long name. */
350
+ const comparisonPath = (path: string): string => {
351
+ const resolved = realpathOrSelf(resolve(path))
352
+ const native = resolved.replaceAll("/", sep)
353
+ return process.platform === "win32" ? native.toLowerCase() : native
354
+ }
355
+
356
+ /**
357
+ * Windows may expose the same directory through a long path and an 8.3 short path even after
358
+ * `realpathSync` has normalized both inputs. When the lexical comparison above says "outside", walk
359
+ * the target's existing ancestors and compare the filesystem identity of each one. `statSync` with
360
+ * bigint fields avoids truncating Windows file IDs, and a missing identity fails closed.
361
+ */
362
+ const pathInsideByFilesystemIdentity = (root: string, path: string): boolean => {
363
+ if (process.platform !== "win32") return false
364
+ const rootIdentity = (() => {
365
+ try {
366
+ const info = statSync(realpathOrSelf(resolve(root)), { bigint: true })
367
+ if (info.ino === 0n) return undefined
368
+ return { dev: info.dev, ino: info.ino }
369
+ } catch {
370
+ try {
371
+ const info = statSync(realpathOrSelf(resolve(root)))
372
+ if (info.ino === 0) return undefined
373
+ return { dev: BigInt(info.dev), ino: BigInt(info.ino) }
374
+ } catch {
375
+ return undefined
376
+ }
377
+ }
378
+ })()
379
+ if (rootIdentity === undefined) return false
380
+
381
+ let current = realpathOrSelf(resolve(path))
382
+ for (;;) {
383
+ try {
384
+ const info = statSync(current, { bigint: true })
385
+ if (info.ino !== 0n && info.dev === rootIdentity.dev && info.ino === rootIdentity.ino)
386
+ return true
387
+ } catch {
388
+ try {
389
+ const info = statSync(current)
390
+ if (
391
+ info.ino !== 0 &&
392
+ BigInt(info.dev) === rootIdentity.dev &&
393
+ BigInt(info.ino) === rootIdentity.ino
394
+ )
395
+ return true
396
+ } catch {
397
+ // A non-existent leaf can still have an existing parent. Keep walking; if no ancestor can
398
+ // prove containment, the answer remains false rather than trusting a spelling alone.
399
+ }
400
+ }
401
+ const parent = dirname(current)
402
+ if (parent === current) return false
403
+ current = parent
404
+ }
405
+ }
406
+
407
+ export const pathInside = (root: string, path: string): boolean => {
408
+ const distance = relative(comparisonPath(root), comparisonPath(path))
409
+ const lexicallyInside =
410
+ distance === "" ||
411
+ (distance !== ".." && !distance.startsWith(`..${sep}`) && !isAbsolute(distance))
412
+ return lexicallyInside || pathInsideByFilesystemIdentity(root, path)
413
+ }
224
414
 
225
415
  const workspaceContains = async (
226
416
  root: string,
@@ -233,27 +423,12 @@ const workspaceContains = async (
233
423
  for await (const match of new Bun.Glob(packagePattern).scan({ cwd: root, dot: false })) {
234
424
  if (match.split(/[\\/]/).includes("node_modules")) continue
235
425
  const packageRoot = resolve(root, dirname(match))
236
- const targetRelative = relative(packageRoot, target)
237
- if (
238
- targetRelative === "" ||
239
- (!targetRelative.startsWith(`..${sep}`) && targetRelative !== "..")
240
- )
241
- return true
426
+ if (pathInside(packageRoot, target)) return true
242
427
  }
243
428
  }
244
429
  return false
245
430
  }
246
431
 
247
- /** The real path, or the input when it cannot be resolved. Roots are compared against each other and
248
- * printed side by side, so they have to be normalized the same way (`/var` vs `/private/var`). */
249
- const realpathOrSelf = (path: string): string => {
250
- try {
251
- return realpathSync(path)
252
- } catch {
253
- return path
254
- }
255
- }
256
-
257
432
  /** Resolve the workspace root once, even when the caller starts in a workspace package. */
258
433
  export async function resolveParityWorkspaceRoot(
259
434
  start: string,
@@ -392,7 +567,7 @@ const linkedPackageRoots = async (
392
567
 
393
568
  export const displayPath = (cwd: string, path: string): string => {
394
569
  const rel = relative(cwd, path)
395
- return rel === "" ? "." : rel
570
+ return rel === "" ? "." : rel.replaceAll("\\", "/")
396
571
  }
397
572
 
398
573
  /**
@@ -465,7 +640,7 @@ const describeScope = (
465
640
  if (requestedRoot === scanRoot) return undefined
466
641
  const outside = paths.filter((path) => !pathInside(requestedRoot, path))
467
642
  if (outside.length === 0) return undefined
468
- const here = relative(scanRoot, requestedRoot) || "."
643
+ const here = displayPath(scanRoot, requestedRoot) || "."
469
644
  return `${outside.length} of these copies ${outside.length === 1 ? "is" : "are"} outside ${here}, so this fails every build in the workspace, including apps that never import the package. The scan is anchored on the workspace on purpose: a copy reached through a workspace-linked dependency cannot be seen from ${here} alone, and scoping to it would miss the case this check exists for. Fix the copies where they live, or declare the package single-copy.`
470
645
  }
471
646
 
@@ -577,6 +752,7 @@ export async function collectIdentityParity(
577
752
  .map(([path, copy]) => ({
578
753
  version: copy.version,
579
754
  path: displayPath(requestedRoot, path),
755
+ absolutePath: path,
580
756
  importers: [...copy.importers].sort(),
581
757
  }))
582
758
  const topology = describeTopology(requestedRoot, scanRoot, absolutePaths)
@@ -775,7 +951,11 @@ const sourceFilesUnder = (root: string): readonly string[] => {
775
951
  const files: string[] = []
776
952
  const walk = (current: string): void => {
777
953
  for (const entry of readdirSync(current, { withFileTypes: true })) {
778
- if (["node_modules", "dist", "build", ".git", ".nifra", "coverage"].includes(entry.name))
954
+ if (
955
+ ["node_modules", "dist", "dist-node", "build", ".git", ".nifra", "coverage"].includes(
956
+ entry.name,
957
+ )
958
+ )
779
959
  continue
780
960
  const path = join(current, entry.name)
781
961
  if (entry.isDirectory()) walk(path)
@@ -814,7 +994,7 @@ export function collectDevelopmentParityInput(
814
994
  const sourceRoot = dirname(resolve(routesDir))
815
995
  const css = sourceFilesUnder(sourceRoot).some((file) => {
816
996
  const content = readFileSync(file, "utf8")
817
- return CSS_IMPORT.test(content) || (isSingleFileComponent(file) && SFC_STYLE.test(content))
997
+ return hasStylesheetImport(content) || (isSingleFileComponent(file) && SFC_STYLE.test(content))
818
998
  })
819
999
  ? ["css:present"]
820
1000
  : []
@@ -855,9 +1035,9 @@ export function compareManifestParity(
855
1035
  return differences
856
1036
  }
857
1037
 
858
- /** The forms `CSS_IMPORT` cannot see, quoted in a css parity failure so the reader knows where to look. */
1038
+ /** The forms the source scanner cannot prove, quoted in a css parity failure so the reader knows where to look. */
859
1039
  const CSS_SCANNER_BLIND_SPOTS =
860
- "a dynamic import(), a bare package subpath (exports), require(), an @import inside a stylesheet, or plugin-injected css"
1040
+ "a bare package subpath (exports), an @import inside a stylesheet, template interpolation, or plugin-injected css"
861
1041
 
862
1042
  const symmetricDifference = (
863
1043
  a: readonly string[],
@@ -881,7 +1061,7 @@ function explainParityDifference(
881
1061
  const shipped = (production.css ?? []).join(", ")
882
1062
  const where = development.sourceRoot === undefined ? "" : ` under ${development.sourceRoot}`
883
1063
  return (
884
- `css: production ships [${shipped || "(none)"}] but the development scanner found no static ` +
1064
+ `css: production ships [${shipped || "(none)"}] but the development scanner found a static ` +
885
1065
  `stylesheet import${where} - it is blind to ${CSS_SCANNER_BLIND_SPOTS}`
886
1066
  )
887
1067
  }