zyzz 0.0.27 → 0.0.29

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 (320) hide show
  1. package/binding/wasi/wasi-worker.mjs +186 -0
  2. package/binding/wasi/zyzz.wasi.cjs +2302 -0
  3. package/binding/wasi/zyzz.wasm32-wasi.wasm +0 -0
  4. package/binding/zyzz.linux-x64-gnu.node +0 -0
  5. package/dist/babel/index.d.ts.map +1 -1
  6. package/dist/babel/index.js +2 -4
  7. package/dist/babel/index.js.map +1 -1
  8. package/dist/babel/internal/Compilation.d.ts +0 -1
  9. package/dist/babel/internal/Compilation.d.ts.map +1 -1
  10. package/dist/compiler/Graph.d.ts +14 -11
  11. package/dist/compiler/Graph.d.ts.map +1 -1
  12. package/dist/compiler/Graph.js +57 -1442
  13. package/dist/compiler/Graph.js.map +1 -1
  14. package/dist/compiler/Native.d.ts +7 -9
  15. package/dist/compiler/Native.d.ts.map +1 -1
  16. package/dist/compiler/Native.js +12 -716
  17. package/dist/compiler/Native.js.map +1 -1
  18. package/dist/compiler/Source.d.ts +122 -24
  19. package/dist/compiler/Source.d.ts.map +1 -1
  20. package/dist/compiler/Source.js +10 -1113
  21. package/dist/compiler/Source.js.map +1 -1
  22. package/dist/compiler/Transform.d.ts +4 -3
  23. package/dist/compiler/Transform.d.ts.map +1 -1
  24. package/dist/compiler/Transform.js +13 -958
  25. package/dist/compiler/Transform.js.map +1 -1
  26. package/dist/compiler/internal/AtRules.d.ts +5 -1
  27. package/dist/compiler/internal/AtRules.d.ts.map +1 -1
  28. package/dist/compiler/internal/AtRules.js +12 -106
  29. package/dist/compiler/internal/AtRules.js.map +1 -1
  30. package/dist/compiler/internal/Context.d.ts +11 -0
  31. package/dist/compiler/internal/Context.d.ts.map +1 -0
  32. package/dist/compiler/internal/Context.js +11 -0
  33. package/dist/compiler/internal/Context.js.map +1 -0
  34. package/dist/compiler/internal/Results.d.ts +148 -0
  35. package/dist/compiler/internal/Results.d.ts.map +1 -0
  36. package/dist/compiler/internal/Results.js +259 -0
  37. package/dist/compiler/internal/Results.js.map +1 -0
  38. package/dist/compiler/internal/Syntax.d.ts +0 -2
  39. package/dist/compiler/internal/Syntax.d.ts.map +1 -1
  40. package/dist/compiler/internal/Syntax.js +0 -2
  41. package/dist/compiler/internal/Syntax.js.map +1 -1
  42. package/dist/compiler/internal/Wire.d.ts +17 -0
  43. package/dist/compiler/internal/Wire.d.ts.map +1 -0
  44. package/dist/compiler/internal/Wire.js +795 -0
  45. package/dist/compiler/internal/Wire.js.map +1 -0
  46. package/dist/internal/Addon.d.ts +28 -0
  47. package/dist/internal/Addon.d.ts.map +1 -0
  48. package/dist/internal/Addon.js +87 -0
  49. package/dist/internal/Addon.js.map +1 -0
  50. package/dist/internal/Authoring.d.ts.map +1 -1
  51. package/dist/internal/Authoring.js +37 -7
  52. package/dist/internal/Authoring.js.map +1 -1
  53. package/dist/internal/Configuration.d.ts +6 -1
  54. package/dist/internal/Configuration.d.ts.map +1 -1
  55. package/dist/internal/Configuration.js.map +1 -1
  56. package/dist/internal/Literal.d.ts +5 -2
  57. package/dist/internal/Literal.d.ts.map +1 -1
  58. package/dist/internal/Literal.js +7 -2
  59. package/dist/internal/Literal.js.map +1 -1
  60. package/dist/internal/Theme.d.ts +4 -0
  61. package/dist/internal/Theme.d.ts.map +1 -1
  62. package/dist/internal/Theme.js.map +1 -1
  63. package/dist/internal/Token.d.ts +2 -1
  64. package/dist/internal/Token.d.ts.map +1 -1
  65. package/dist/internal/Token.js +2 -2
  66. package/dist/internal/Token.js.map +1 -1
  67. package/dist/internal/Vlq.d.ts +14 -0
  68. package/dist/internal/Vlq.d.ts.map +1 -0
  69. package/dist/internal/Vlq.js +47 -0
  70. package/dist/internal/Vlq.js.map +1 -0
  71. package/dist/internal/Wasm.d.ts +22 -0
  72. package/dist/internal/Wasm.d.ts.map +1 -0
  73. package/dist/internal/Wasm.js +83 -0
  74. package/dist/internal/Wasm.js.map +1 -0
  75. package/dist/metro/Graph.d.ts +0 -1
  76. package/dist/metro/Graph.d.ts.map +1 -1
  77. package/dist/metro/transformer.d.ts.map +1 -1
  78. package/dist/metro/transformer.js +0 -1
  79. package/dist/metro/transformer.js.map +1 -1
  80. package/dist/next/internal/Project.d.ts.map +1 -1
  81. package/dist/next/internal/Project.js +2 -5
  82. package/dist/next/internal/Project.js.map +1 -1
  83. package/dist/node/Host.d.ts.map +1 -1
  84. package/dist/node/Host.js +0 -2
  85. package/dist/node/Host.js.map +1 -1
  86. package/dist/node/internal/Contracts.d.ts.map +1 -1
  87. package/dist/node/internal/Contracts.js +3 -7
  88. package/dist/node/internal/Contracts.js.map +1 -1
  89. package/dist/node/internal/Snapshot.d.ts +0 -1
  90. package/dist/node/internal/Snapshot.d.ts.map +1 -1
  91. package/dist/node/internal/Snapshot.js +0 -6
  92. package/dist/node/internal/Snapshot.js.map +1 -1
  93. package/dist/runtime/Composition.d.ts +2 -3
  94. package/dist/runtime/Composition.d.ts.map +1 -1
  95. package/dist/runtime/Composition.js +51 -30
  96. package/dist/runtime/Composition.js.map +1 -1
  97. package/dist/runtime/Recipe.d.ts.map +1 -1
  98. package/dist/runtime/Recipe.js +16 -12
  99. package/dist/runtime/Recipe.js.map +1 -1
  100. package/dist/styleFunction.d.ts +11 -2
  101. package/dist/styleFunction.d.ts.map +1 -1
  102. package/dist/styleFunction.js +3 -1
  103. package/dist/styleFunction.js.map +1 -1
  104. package/dist/unplugin/index.d.ts.map +1 -1
  105. package/dist/unplugin/index.js +3 -6
  106. package/dist/unplugin/index.js.map +1 -1
  107. package/dist/vite/index.d.ts +7 -0
  108. package/dist/vite/index.d.ts.map +1 -1
  109. package/dist/vite/index.js +862 -216
  110. package/dist/vite/index.js.map +1 -1
  111. package/dist/web/Css.d.ts +8 -3
  112. package/dist/web/Css.d.ts.map +1 -1
  113. package/dist/web/Css.js +5 -721
  114. package/dist/web/Css.js.map +1 -1
  115. package/dist/web/internal/Block.d.ts +0 -2
  116. package/dist/web/internal/Block.d.ts.map +1 -1
  117. package/dist/web/internal/Block.js +1 -12
  118. package/dist/web/internal/Block.js.map +1 -1
  119. package/dist/web/internal/ClassName.d.ts +23 -18
  120. package/dist/web/internal/ClassName.d.ts.map +1 -1
  121. package/dist/web/internal/ClassName.js +26 -55
  122. package/dist/web/internal/ClassName.js.map +1 -1
  123. package/dist/web/internal/Contributions.d.ts +3 -5
  124. package/dist/web/internal/Contributions.d.ts.map +1 -1
  125. package/dist/web/internal/Contributions.js +0 -45
  126. package/dist/web/internal/Contributions.js.map +1 -1
  127. package/dist/web/internal/Engine.browser.d.ts +8 -0
  128. package/dist/web/internal/Engine.browser.d.ts.map +1 -0
  129. package/dist/web/internal/Engine.browser.js +42 -0
  130. package/dist/web/internal/Engine.browser.js.map +1 -0
  131. package/dist/web/internal/Engine.d.ts +9 -0
  132. package/dist/web/internal/Engine.d.ts.map +1 -0
  133. package/dist/web/internal/Engine.js +14 -0
  134. package/dist/web/internal/Engine.js.map +1 -0
  135. package/dist/web/internal/Themes.d.ts +1 -9
  136. package/dist/web/internal/Themes.d.ts.map +1 -1
  137. package/dist/web/internal/Themes.js +3 -221
  138. package/dist/web/internal/Themes.js.map +1 -1
  139. package/dist/web/internal/Wasm.generated.d.ts +4 -0
  140. package/dist/web/internal/Wasm.generated.d.ts.map +1 -0
  141. package/dist/web/internal/Wasm.generated.js +4 -0
  142. package/dist/web/internal/Wasm.generated.js.map +1 -0
  143. package/dist/web/internal/Wire.d.ts +19 -0
  144. package/dist/web/internal/Wire.d.ts.map +1 -0
  145. package/dist/web/internal/Wire.js +387 -0
  146. package/dist/web/internal/Wire.js.map +1 -0
  147. package/package.json +20 -1
  148. package/src/babel/index.ts +2 -5
  149. package/src/babel/internal/Compilation.ts +0 -1
  150. package/src/compiler/Graph.ts +89 -1930
  151. package/src/compiler/Native.ts +18 -871
  152. package/src/compiler/Source.ts +144 -1579
  153. package/src/compiler/Transform.ts +16 -1312
  154. package/src/compiler/internal/AtRules.ts +27 -115
  155. package/src/compiler/internal/Context.ts +11 -0
  156. package/src/compiler/internal/Results.ts +406 -0
  157. package/src/compiler/internal/Syntax.ts +0 -3
  158. package/src/compiler/internal/Wire.ts +1045 -0
  159. package/src/internal/Addon.ts +118 -0
  160. package/src/internal/Authoring.ts +53 -18
  161. package/src/internal/Configuration.ts +38 -1
  162. package/src/internal/Literal.ts +7 -2
  163. package/src/internal/Theme.ts +15 -0
  164. package/src/internal/Token.ts +2 -2
  165. package/src/internal/Vlq.ts +59 -0
  166. package/src/internal/Wasm.ts +111 -0
  167. package/src/metro/transformer.ts +0 -1
  168. package/src/next/internal/Project.ts +2 -9
  169. package/src/node/Host.ts +0 -2
  170. package/src/node/internal/Contracts.ts +3 -7
  171. package/src/node/internal/Snapshot.ts +0 -8
  172. package/src/runtime/Composition.ts +48 -25
  173. package/src/runtime/Recipe.ts +21 -14
  174. package/src/styleFunction.ts +31 -2
  175. package/src/unplugin/index.ts +6 -6
  176. package/src/vite/index.ts +1157 -241
  177. package/src/web/Css.ts +12 -908
  178. package/src/web/internal/Block.ts +0 -14
  179. package/src/web/internal/ClassName.ts +52 -85
  180. package/src/web/internal/Contributions.ts +3 -59
  181. package/src/web/internal/Engine.browser.ts +51 -0
  182. package/src/web/internal/Engine.ts +17 -0
  183. package/src/web/internal/Themes.ts +3 -279
  184. package/src/web/internal/Wasm.generated.ts +3 -0
  185. package/src/web/internal/Wire.ts +466 -0
  186. package/src/web/internal/wasm.d.ts +9 -0
  187. package/dist/compiler/internal/Applications.d.ts +0 -55
  188. package/dist/compiler/internal/Applications.d.ts.map +0 -1
  189. package/dist/compiler/internal/Applications.js +0 -352
  190. package/dist/compiler/internal/Applications.js.map +0 -1
  191. package/dist/compiler/internal/ColorProfiles.d.ts +0 -3
  192. package/dist/compiler/internal/ColorProfiles.d.ts.map +0 -1
  193. package/dist/compiler/internal/ColorProfiles.js +0 -84
  194. package/dist/compiler/internal/ColorProfiles.js.map +0 -1
  195. package/dist/compiler/internal/CompositionBindings.d.ts +0 -16
  196. package/dist/compiler/internal/CompositionBindings.d.ts.map +0 -1
  197. package/dist/compiler/internal/CompositionBindings.js +0 -89
  198. package/dist/compiler/internal/CompositionBindings.js.map +0 -1
  199. package/dist/compiler/internal/Compositions.d.ts +0 -37
  200. package/dist/compiler/internal/Compositions.d.ts.map +0 -1
  201. package/dist/compiler/internal/Compositions.js +0 -494
  202. package/dist/compiler/internal/Compositions.js.map +0 -1
  203. package/dist/compiler/internal/Configurations.d.ts +0 -26
  204. package/dist/compiler/internal/Configurations.d.ts.map +0 -1
  205. package/dist/compiler/internal/Configurations.js +0 -259
  206. package/dist/compiler/internal/Configurations.js.map +0 -1
  207. package/dist/compiler/internal/Contract.d.ts +0 -30
  208. package/dist/compiler/internal/Contract.d.ts.map +0 -1
  209. package/dist/compiler/internal/Contract.js +0 -753
  210. package/dist/compiler/internal/Contract.js.map +0 -1
  211. package/dist/compiler/internal/Contributions.d.ts +0 -54
  212. package/dist/compiler/internal/Contributions.d.ts.map +0 -1
  213. package/dist/compiler/internal/Contributions.js +0 -1151
  214. package/dist/compiler/internal/Contributions.js.map +0 -1
  215. package/dist/compiler/internal/Descriptors.d.ts +0 -15
  216. package/dist/compiler/internal/Descriptors.d.ts.map +0 -1
  217. package/dist/compiler/internal/Descriptors.js +0 -192
  218. package/dist/compiler/internal/Descriptors.js.map +0 -1
  219. package/dist/compiler/internal/Dynamic.d.ts +0 -14
  220. package/dist/compiler/internal/Dynamic.d.ts.map +0 -1
  221. package/dist/compiler/internal/Dynamic.js +0 -175
  222. package/dist/compiler/internal/Dynamic.js.map +0 -1
  223. package/dist/compiler/internal/Expression.d.ts +0 -19
  224. package/dist/compiler/internal/Expression.d.ts.map +0 -1
  225. package/dist/compiler/internal/Expression.js +0 -164
  226. package/dist/compiler/internal/Expression.js.map +0 -1
  227. package/dist/compiler/internal/Functions.d.ts +0 -5
  228. package/dist/compiler/internal/Functions.d.ts.map +0 -1
  229. package/dist/compiler/internal/Functions.js +0 -191
  230. package/dist/compiler/internal/Functions.js.map +0 -1
  231. package/dist/compiler/internal/Keyframes.d.ts +0 -5
  232. package/dist/compiler/internal/Keyframes.d.ts.map +0 -1
  233. package/dist/compiler/internal/Keyframes.js +0 -51
  234. package/dist/compiler/internal/Keyframes.js.map +0 -1
  235. package/dist/compiler/internal/Names.d.ts +0 -5
  236. package/dist/compiler/internal/Names.d.ts.map +0 -1
  237. package/dist/compiler/internal/Names.js +0 -37
  238. package/dist/compiler/internal/Names.js.map +0 -1
  239. package/dist/compiler/internal/NativeBindings.d.ts +0 -72
  240. package/dist/compiler/internal/NativeBindings.d.ts.map +0 -1
  241. package/dist/compiler/internal/NativeBindings.js +0 -171
  242. package/dist/compiler/internal/NativeBindings.js.map +0 -1
  243. package/dist/compiler/internal/NativeMedia.d.ts +0 -14
  244. package/dist/compiler/internal/NativeMedia.d.ts.map +0 -1
  245. package/dist/compiler/internal/NativeMedia.js +0 -287
  246. package/dist/compiler/internal/NativeMedia.js.map +0 -1
  247. package/dist/compiler/internal/NativeVars.d.ts +0 -28
  248. package/dist/compiler/internal/NativeVars.d.ts.map +0 -1
  249. package/dist/compiler/internal/NativeVars.js +0 -160
  250. package/dist/compiler/internal/NativeVars.js.map +0 -1
  251. package/dist/compiler/internal/PackedStyles.d.ts +0 -37
  252. package/dist/compiler/internal/PackedStyles.d.ts.map +0 -1
  253. package/dist/compiler/internal/PackedStyles.js +0 -344
  254. package/dist/compiler/internal/PackedStyles.js.map +0 -1
  255. package/dist/compiler/internal/Properties.d.ts +0 -5
  256. package/dist/compiler/internal/Properties.d.ts.map +0 -1
  257. package/dist/compiler/internal/Properties.js +0 -82
  258. package/dist/compiler/internal/Properties.js.map +0 -1
  259. package/dist/compiler/internal/RecipeConditions.d.ts +0 -41
  260. package/dist/compiler/internal/RecipeConditions.d.ts.map +0 -1
  261. package/dist/compiler/internal/RecipeConditions.js +0 -82
  262. package/dist/compiler/internal/RecipeConditions.js.map +0 -1
  263. package/dist/compiler/internal/RecipePayloads.d.ts +0 -31
  264. package/dist/compiler/internal/RecipePayloads.d.ts.map +0 -1
  265. package/dist/compiler/internal/RecipePayloads.js +0 -99
  266. package/dist/compiler/internal/RecipePayloads.js.map +0 -1
  267. package/dist/compiler/internal/Recipes.d.ts +0 -32
  268. package/dist/compiler/internal/Recipes.d.ts.map +0 -1
  269. package/dist/compiler/internal/Recipes.js +0 -339
  270. package/dist/compiler/internal/Recipes.js.map +0 -1
  271. package/dist/compiler/internal/Selectors.d.ts +0 -12
  272. package/dist/compiler/internal/Selectors.d.ts.map +0 -1
  273. package/dist/compiler/internal/Selectors.js +0 -268
  274. package/dist/compiler/internal/Selectors.js.map +0 -1
  275. package/dist/compiler/internal/Static.d.ts +0 -39
  276. package/dist/compiler/internal/Static.d.ts.map +0 -1
  277. package/dist/compiler/internal/Static.js +0 -534
  278. package/dist/compiler/internal/Static.js.map +0 -1
  279. package/dist/compiler/internal/Themes.d.ts +0 -190
  280. package/dist/compiler/internal/Themes.d.ts.map +0 -1
  281. package/dist/compiler/internal/Themes.js +0 -1577
  282. package/dist/compiler/internal/Themes.js.map +0 -1
  283. package/dist/compiler/internal/Vars.d.ts +0 -33
  284. package/dist/compiler/internal/Vars.d.ts.map +0 -1
  285. package/dist/compiler/internal/Vars.js +0 -387
  286. package/dist/compiler/internal/Vars.js.map +0 -1
  287. package/dist/compiler/internal/ViewTransitions.d.ts +0 -3
  288. package/dist/compiler/internal/ViewTransitions.d.ts.map +0 -1
  289. package/dist/compiler/internal/ViewTransitions.js +0 -68
  290. package/dist/compiler/internal/ViewTransitions.js.map +0 -1
  291. package/dist/internal/Cascade.d.ts +0 -74
  292. package/dist/internal/Cascade.d.ts.map +0 -1
  293. package/dist/internal/Cascade.js +0 -210
  294. package/dist/internal/Cascade.js.map +0 -1
  295. package/src/compiler/internal/Applications.ts +0 -554
  296. package/src/compiler/internal/ColorProfiles.ts +0 -93
  297. package/src/compiler/internal/CompositionBindings.ts +0 -120
  298. package/src/compiler/internal/Compositions.ts +0 -678
  299. package/src/compiler/internal/Configurations.ts +0 -349
  300. package/src/compiler/internal/Contract.ts +0 -1028
  301. package/src/compiler/internal/Contributions.ts +0 -1561
  302. package/src/compiler/internal/Descriptors.ts +0 -242
  303. package/src/compiler/internal/Dynamic.ts +0 -275
  304. package/src/compiler/internal/Expression.ts +0 -218
  305. package/src/compiler/internal/Functions.ts +0 -206
  306. package/src/compiler/internal/Keyframes.ts +0 -62
  307. package/src/compiler/internal/Names.ts +0 -48
  308. package/src/compiler/internal/NativeBindings.ts +0 -235
  309. package/src/compiler/internal/NativeMedia.ts +0 -342
  310. package/src/compiler/internal/NativeVars.ts +0 -231
  311. package/src/compiler/internal/PackedStyles.ts +0 -473
  312. package/src/compiler/internal/Properties.ts +0 -121
  313. package/src/compiler/internal/RecipeConditions.ts +0 -139
  314. package/src/compiler/internal/RecipePayloads.ts +0 -132
  315. package/src/compiler/internal/Recipes.ts +0 -531
  316. package/src/compiler/internal/Selectors.ts +0 -339
  317. package/src/compiler/internal/Static.ts +0 -784
  318. package/src/compiler/internal/Themes.ts +0 -2398
  319. package/src/compiler/internal/Vars.ts +0 -516
  320. package/src/compiler/internal/ViewTransitions.ts +0 -82
package/src/vite/index.ts CHANGED
@@ -5,20 +5,30 @@
5
5
  import * as Namespaces from '../compiler/internal/Namespaces.js'
6
6
  import * as AtRules from '../compiler/internal/AtRules.js'
7
7
  import * as Catalogs from '../compiler/internal/Catalogs.js'
8
+ import * as Results from '../compiler/internal/Results.js'
8
9
  import * as Contracts from '../node/internal/Contracts.js'
9
10
  import * as Mapping from '@jridgewell/gen-mapping'
10
11
  import * as Lightning from 'lightningcss'
11
12
  import * as Crypto from 'node:crypto'
13
+ import { existsSync } from 'node:fs'
12
14
  import * as Fs from 'node:fs/promises'
13
15
  import * as Path from 'node:path'
14
16
  import * as Reset from '../node/Reset.js'
15
- import { isBuiltin } from 'node:module'
17
+ import * as Vlq from '../internal/Vlq.js'
18
+ import { createRequire, isBuiltin } from 'node:module'
16
19
  import * as Parser from 'oxc-parser'
17
20
  import * as Walker from 'oxc-walker'
18
21
  import * as Snapshot from '../node/internal/Snapshot.js'
19
- import * as Syntax from '../compiler/internal/Syntax.js'
20
22
  import * as Scope from '../compiler/internal/Scope.js'
21
- import type { Environment, Plugin, Rollup, ViteDevServer } from 'vite'
23
+ import type {
24
+ DevEnvironment,
25
+ Environment,
26
+ EnvironmentModuleNode,
27
+ Plugin,
28
+ Rollup,
29
+ UserConfig,
30
+ ViteDevServer,
31
+ } from 'vite'
22
32
  import * as Graph from '../compiler/Graph.js'
23
33
  import * as Source from '../compiler/Source.js'
24
34
  import * as ThemeRules from '../web/internal/Themes.js'
@@ -51,7 +61,35 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
51
61
  const states = new WeakMap<Environment, Map<string, Entry>>()
52
62
  const removed = new WeakMap<Environment, Set<string>>()
53
63
  const reads = new WeakMap<Environment, Map<string, string>>()
64
+ const lookups = new WeakMap<Environment, Lookups>()
54
65
  const compilers = new WeakMap<Environment, Graph.create.ReturnType>()
66
+ const projects = new WeakMap<Environment, Project>()
67
+ // The shared stylesheet each environment last loaded, which an edit compares against.
68
+ const servedShared = new WeakMap<Environment, string>()
69
+ // Watcher events advance an environment's generation, which scopes memoized whole-project work.
70
+ const generations = new WeakMap<Environment, number>()
71
+ let devServer: ViteDevServer | undefined
72
+ const watching = new Set<string>()
73
+ // Chokidar matches its ignore patterns on every add, and each compile watches its whole closure.
74
+ const watched = new Set<string>()
75
+ // Each compile reads the catalogs of every contract it saw, which rarely changes text.
76
+ const catalogReads = new Map<string, readonly Catalogs.Catalog[]>()
77
+ const dependencyChains = new Map<string, readonly (readonly unknown[])[]>()
78
+ const edgeMemos = new WeakMap<
79
+ Environment,
80
+ { files: Map<string, Edges>; key: string }
81
+ >()
82
+ // A development entry's last output, reused until a watcher event or a recompiled graph.
83
+ const outputs = new WeakMap<
84
+ Entry,
85
+ {
86
+ allSources: boolean
87
+ code: string | undefined
88
+ generation: number
89
+ output: Awaited<ReturnType<typeof compileEntry>>
90
+ result: Graph.compile.ReturnType
91
+ }
92
+ >()
55
93
  const packages = Contracts.create()
56
94
 
57
95
  function compiler(environment: Environment) {
@@ -62,6 +100,168 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
62
100
  }
63
101
  return value
64
102
  }
103
+
104
+ /**
105
+ * Watches a compile input from a hook. Vite checks the filesystem each time a file outside its
106
+ * root is watched, and every compile reads the same package files, so development watches
107
+ * each of those once.
108
+ */
109
+ function watcher(context: { addWatchFile: (id: string) => void }) {
110
+ return (file: string) => {
111
+ if (devServer && !file.startsWith(`${devServer.config.root}/`)) {
112
+ if (watching.has(file)) return
113
+
114
+ watching.add(file)
115
+ }
116
+
117
+ context.addWatchFile(file)
118
+ }
119
+ }
120
+
121
+ /**
122
+ * The packed stylesheet dependency chains of a library contract. Every compile reads each
123
+ * contract it reaches, and design system contracts are large, so each text parses once.
124
+ */
125
+ function stylesheetDependencies(id: string, contract: string) {
126
+ let value = dependencyChains.get(contract)
127
+ if (value) return value
128
+
129
+ const metadata = (() => {
130
+ try {
131
+ return JSON.parse(contract)
132
+ } catch (error) {
133
+ Graph.compile({ modules: {}, contracts: { [id]: contract } })
134
+ throw error
135
+ }
136
+ })()
137
+
138
+ value = Array.isArray(metadata?.stylesheets)
139
+ ? (metadata.stylesheets as readonly { dependency?: unknown }[])
140
+ .map((section) => section?.dependency)
141
+ .filter((dependency): dependency is readonly unknown[] =>
142
+ Array.isArray(dependency),
143
+ )
144
+ : []
145
+ // Rebuilt libraries leave their earlier texts behind, so the cache stays bounded.
146
+ if (dependencyChains.size > 256) dependencyChains.clear()
147
+ dependencyChains.set(contract, value)
148
+ return value
149
+ }
150
+
151
+ /**
152
+ * Each module's import edges in an environment, shared by every entry compile that walks the
153
+ * module. Added and removed files and dependency optimization change resolutions, so either
154
+ * starts over.
155
+ */
156
+ function importEdges(environment: Environment) {
157
+ const metadata =
158
+ environment.mode === 'dev'
159
+ ? (environment as DevEnvironment).depsOptimizer?.metadata
160
+ : undefined
161
+ const key = `${metadata?.browserHash}:${Object.keys(metadata?.discovered ?? {}).length}`
162
+ let value = edgeMemos.get(environment)
163
+
164
+ if (value?.key !== key) {
165
+ value = { files: new Map(), key }
166
+ edgeMemos.set(environment, value)
167
+ }
168
+
169
+ return value.files
170
+ }
171
+
172
+ /** Adds a compile input to the development watcher once. */
173
+ function watch(server: ViteDevServer, file: string) {
174
+ if (watched.has(file)) return
175
+
176
+ watched.add(file)
177
+ server.watcher.add(file)
178
+ }
179
+
180
+ function read(contract: string) {
181
+ let value = catalogReads.get(contract)
182
+
183
+ if (!value) {
184
+ if (catalogReads.size > 256) catalogReads.clear()
185
+
186
+ value = Catalogs.read(contract)
187
+ catalogReads.set(contract, value)
188
+ }
189
+
190
+ return value
191
+ }
192
+
193
+ function project(environment: Environment) {
194
+ let value = projects.get(environment)
195
+ if (!value) {
196
+ value = {
197
+ compiler: Graph.create(),
198
+ contracts: Object.create(null),
199
+ fallback: Graph.create(),
200
+ imports: Object.create(null),
201
+ modules: Object.create(null),
202
+ outcome: undefined,
203
+ }
204
+ projects.set(environment, value)
205
+ }
206
+ return value
207
+ }
208
+
209
+ /**
210
+ * Development merges every module a transform reaches into one graph per environment, so
211
+ * modules compile once per change instead of once per importer. A module's output depends only
212
+ * on its own import closure, so the merged graph returns the output a closure compile would.
213
+ * While some other module fails, the requested closure compiles alone and reports only its own
214
+ * errors, as before.
215
+ */
216
+ function compileProject(
217
+ environment: Environment,
218
+ input: Graph.compile.Options,
219
+ id: string,
220
+ ) {
221
+ const value = project(environment)
222
+ let changed = value.outcome === undefined
223
+
224
+ for (const [module, source] of Object.entries(input.modules))
225
+ if (value.modules[module] !== source) {
226
+ value.modules[module] = source
227
+ changed = true
228
+ }
229
+
230
+ for (const [module, resolutions] of Object.entries(input.imports ?? {}))
231
+ if (!same(value.imports[module], resolutions)) {
232
+ value.imports[module] = resolutions
233
+ changed = true
234
+ }
235
+
236
+ for (const [module, contract] of Object.entries(input.contracts ?? {}))
237
+ if (value.contracts[module] !== contract) {
238
+ value.contracts[module] = contract
239
+ changed = true
240
+ }
241
+
242
+ if (changed)
243
+ value.outcome = (() => {
244
+ try {
245
+ return {
246
+ result: value.compiler.compile({
247
+ ...input,
248
+ contracts: value.contracts,
249
+ imports: value.imports,
250
+ modules: value.modules,
251
+ }),
252
+ }
253
+ } catch (error) {
254
+ return { error }
255
+ }
256
+ })()
257
+
258
+ const outcome = value.outcome!
259
+
260
+ if ('result' in outcome && Object.hasOwn(outcome.result.modules, id))
261
+ return outcome.result
262
+
263
+ return value.fallback.compile(input)
264
+ }
65
265
  const discoveries = new WeakMap<Environment, Promise<Map<string, string>>>()
66
266
  const contributionFiles = new WeakMap<Environment, Set<string>>()
67
267
  const assets = new Map<string, string>()
@@ -71,8 +271,13 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
71
271
  const initializers = new WeakMap<Environment, Entry>()
72
272
  const sourceEntrypoints = new Set<string>()
73
273
  const identities = new Map<string, string>()
274
+ const exclusions = new Map<string, boolean>()
275
+ // Every compile checks each import against the source directories.
276
+ const roots = new Map<string, string | undefined>()
74
277
  const lazyImports = new WeakMap<Parser.ParseResult, string[]>()
75
278
  let directories: readonly string[]
279
+ let filter: (id: unknown) => boolean
280
+ let optimizeRuntime = false
76
281
  let root: string
77
282
 
78
283
  function snapshot(environment: Environment) {
@@ -149,12 +354,30 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
149
354
  Path.isAbsolute(id) &&
150
355
  sourceRoot(id) !== undefined &&
151
356
  /\.[cm]?[jt]sx?$/.test(id) &&
152
- !/\.(?:d|test|test-d|bench|bench-d)\.[cm]?[jt]sx?$/.test(id)
357
+ !/\.(?:d|test|test-d|bench|bench-d)\.[cm]?[jt]sx?$/.test(id) &&
358
+ !excluded(id)
153
359
  )
154
360
  }
155
361
 
362
+ /** Matches excluded files inside source directories. Package files never match, so packed contracts stay readable. */
363
+ function excluded(id: string) {
364
+ const file = id.split(/[?#]/)[0]!
365
+ let value = exclusions.get(file)
366
+
367
+ // Vite's filter recompiles each glob per call, and every compile checks each import.
368
+ if (value === undefined) {
369
+ value =
370
+ Path.isAbsolute(file) && sourceRoot(file) !== undefined && !filter(file)
371
+ exclusions.set(file, value)
372
+ }
373
+
374
+ return value
375
+ }
376
+
156
377
  function sourceRoot(file: string) {
157
- return directories.find((directory) => {
378
+ if (roots.has(file)) return roots.get(file)
379
+
380
+ const root = directories.find((directory) => {
158
381
  const relative = Path.relative(directory, file)
159
382
 
160
383
  return (
@@ -164,6 +387,9 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
164
387
  !relative.split(Path.sep).includes('node_modules')
165
388
  )
166
389
  })
390
+
391
+ roots.set(file, root)
392
+ return root
167
393
  }
168
394
 
169
395
  function discover(environment: Environment, host: Host) {
@@ -355,6 +581,46 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
355
581
  event: string,
356
582
  ) {
357
583
  reads.delete(environment)
584
+ generations.set(environment, (generations.get(environment) ?? 0) + 1)
585
+
586
+ const changed = entries(environment).get(file)
587
+
588
+ if (changed) changed.source = undefined
589
+
590
+ // An edit keeps every resolution. Adding or removing a file can change where a
591
+ // specifier resolves, and a contract derives again only when a file it read changes.
592
+ // Editors save through temporary files, which no specifier resolves to.
593
+ const resolvable =
594
+ event !== 'update' &&
595
+ /\.(?:[cm]?[jt]sx?|json|css|svelte|vue)$|(?:^|[/\\])package\.json$/.test(
596
+ file,
597
+ )
598
+ const cache = lookups.get(environment)
599
+
600
+ // Import edges derive from resolutions and module text, which each memo compares.
601
+ if (resolvable) edgeMemos.delete(environment)
602
+
603
+ if (cache) {
604
+ if (resolvable) cache.resolutions.clear()
605
+
606
+ for (const [key, files] of cache.read)
607
+ if (files.has(file)) {
608
+ cache.contracts.delete(key)
609
+ cache.read.delete(key)
610
+ }
611
+ }
612
+
613
+ if (event === 'delete') {
614
+ const value = projects.get(environment)
615
+ const id = sourceId(file)
616
+
617
+ if (value && Object.hasOwn(value.modules, id)) {
618
+ delete value.modules[id]
619
+ delete value.imports[id]
620
+ value.outcome = undefined
621
+ }
622
+ }
623
+
358
624
  if (event === 'delete') {
359
625
  if (entries(environment).has(file)) {
360
626
  let files = removed.get(environment)
@@ -392,22 +658,127 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
392
658
  allSources = false,
393
659
  everything = false,
394
660
  ) {
661
+ const generation = generations.get(entry.environment) ?? 0
662
+ const memo = outputs.get(entry)
663
+ const current = projects.get(entry.environment)?.outcome
664
+
665
+ // Without a watcher event the closure is unchanged, so a stylesheet load after its
666
+ // transform reuses the output while the merged graph has not recompiled.
667
+ if (
668
+ memo &&
669
+ !everything &&
670
+ memo.allSources === allSources &&
671
+ memo.generation === generation &&
672
+ (code === undefined || code === memo.code) &&
673
+ current &&
674
+ 'result' in current &&
675
+ current.result === memo.result
676
+ ) {
677
+ for (const file of entry.files) host.watch(file)
678
+ return memo.output
679
+ }
680
+
681
+ const output = await compileEntry(entry, host, code, allSources, everything)
682
+ const outcome = projects.get(entry.environment)?.outcome
683
+
684
+ // A concurrent compile can replace the merged result while this one renders, so only an
685
+ // output of the current result is kept.
686
+ if (
687
+ entry.environment.mode === 'dev' &&
688
+ !everything &&
689
+ outcome &&
690
+ 'result' in outcome &&
691
+ outcome.result.modules === output.modules &&
692
+ generation === (generations.get(entry.environment) ?? 0)
693
+ )
694
+ outputs.set(entry, {
695
+ allSources,
696
+ code,
697
+ generation,
698
+ output,
699
+ result: outcome.result,
700
+ })
701
+
702
+ return output
703
+ }
704
+
705
+ async function compileEntry(
706
+ entry: Entry,
707
+ host: Host,
708
+ code?: string,
709
+ allSources = false,
710
+ everything = false,
711
+ ) {
712
+ // Every compile visits the discovered project, so modules share lookups. Watcher events
713
+ // drop the resolutions and contracts a change can affect. Full-project compiles look up
714
+ // afresh, since a document must not trail the watcher.
715
+ const cache = (() => {
716
+ const fresh: Lookups = {
717
+ contracts: new Map(),
718
+ read: new Map(),
719
+ resolutions: new Map(),
720
+ }
721
+ if (everything) return fresh
722
+
723
+ let value = lookups.get(entry.environment)
724
+ if (!value) {
725
+ value = fresh
726
+ lookups.set(entry.environment, value)
727
+ }
728
+ return value
729
+ })()
730
+
731
+ // Full-project compiles walk afresh, as they look up afresh.
732
+ const memos = everything ? undefined : importEdges(entry.environment)
733
+
395
734
  async function resolve(source: string, importer: string) {
396
735
  if (isBuiltin(source)) return { id: source, external: true }
397
- const resolved = await host.resolve(source, importer)
736
+
737
+ const key = `${importer}\0${source}`
738
+ let pending = cache.resolutions.get(key)
739
+ if (!pending) {
740
+ pending = host.resolve(source, importer)
741
+ cache.resolutions.set(key, pending)
742
+ pending.catch(() => cache.resolutions.delete(key))
743
+ }
744
+
745
+ const resolved = await pending
398
746
  if (!resolved) return resolved
399
747
 
400
748
  if (entry.environment.mode === 'dev') {
401
- const optimized = Object.values({
402
- ...entry.environment.depsOptimizer?.metadata.optimized,
403
- ...entry.environment.depsOptimizer?.metadata.discovered,
404
- }).find((item) => item.file === resolved.id.split('?')[0])
405
- if (optimized?.src) return { ...resolved, id: optimized.src }
749
+ const source = optimized().get(resolved.id.split('?')[0]!)
750
+ if (source) return { ...resolved, id: source }
406
751
  }
407
752
 
408
753
  return { ...resolved, id: normalize(resolved.id) }
409
754
  }
410
755
 
756
+ // Optimized dependency sources by bundled file, indexed once per compile rather than
757
+ // searched for every resolution.
758
+ let indexed: Map<string, string> | undefined
759
+
760
+ function optimized() {
761
+ if (indexed) return indexed
762
+
763
+ indexed = new Map()
764
+
765
+ const environment = entry.environment
766
+ const metadata =
767
+ environment.mode === 'dev'
768
+ ? environment.depsOptimizer?.metadata
769
+ : undefined
770
+
771
+ // Discovered entries replace optimized ones of the same name, and the first entry
772
+ // bundling a file wins, as a search of the merged record finds it.
773
+ for (const item of Object.values({
774
+ ...metadata?.optimized,
775
+ ...metadata?.discovered,
776
+ }))
777
+ if (!indexed.has(item.file)) indexed.set(item.file, item.src ?? '')
778
+
779
+ return indexed
780
+ }
781
+
411
782
  const imports: Record<
412
783
  string,
413
784
  Record<string, string | null>
@@ -416,15 +787,44 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
416
787
  const lazy: Record<string, ReadonlySet<string>> = Object.create(null)
417
788
  const modules: Record<string, string> = Object.create(null)
418
789
  const files = new Set<string>()
419
- const contractHost: Contracts.read.Options = {
420
- read: async (file) => {
421
- const source = await snapshot(entry.environment).read(file)
422
- host.watch(file)
423
- files.add(file)
424
- return source
425
- },
426
- resolve: async (specifier, importer) =>
427
- (await resolve(specifier, importer))?.id.split(/[?#]/)[0],
790
+
791
+ // A package contract derives from the files it reads and the resolutions
792
+ // above. A cached contract still makes this compile watch those files.
793
+ async function readContract(file: string, required = false) {
794
+ const key = `${required}\0${file}`
795
+ let pending = cache.contracts.get(key)
796
+ if (!pending) {
797
+ const read = new Set<string>()
798
+
799
+ pending = packages
800
+ .read(file, {
801
+ read: async (path) => {
802
+ const source = await snapshot(entry.environment).read(path)
803
+ read.add(path)
804
+ return source
805
+ },
806
+ required,
807
+ resolve: async (specifier, importer) =>
808
+ (await resolve(specifier, importer))?.id.split(/[?#]/)[0],
809
+ })
810
+ .then((contract) => {
811
+ cache.read.set(key, read)
812
+ return { contract, files: read }
813
+ })
814
+ cache.contracts.set(key, pending)
815
+ pending.catch(() => cache.contracts.delete(key))
816
+ }
817
+
818
+ const derived = await pending
819
+
820
+ for (const path of derived.files) {
821
+ if (files.has(path)) continue
822
+
823
+ files.add(path)
824
+ host.watch(path)
825
+ }
826
+
827
+ return derived.contract
428
828
  }
429
829
 
430
830
  async function visit(file: string, source?: string) {
@@ -439,16 +839,43 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
439
839
  cached = new Map()
440
840
  reads.set(entry.environment, cached)
441
841
  }
842
+ // Earlier plugins can rewrite a module before this one sees it, as router plugins drop
843
+ // server routes from client route trees. Its importers read the code Vite transformed, so
844
+ // every graph in the environment holds one version of each module.
442
845
  // Full-project compilation must not reuse reads from concurrent module transforms.
443
846
  const text =
444
847
  source ??
848
+ (everything
849
+ ? undefined
850
+ : entries(entry.environment).get(file)?.source) ??
445
851
  (everything ? undefined : cached.get(file)) ??
446
852
  (await snapshot(entry.environment).read(file))
447
853
  if (!everything) cached.set(file, text)
448
854
 
449
855
  modules[id] = text
450
856
 
857
+ const memo = memos?.get(file)
858
+
859
+ // Replaying the edges keeps the order in which modules and contracts join the compile.
860
+ if (memo?.text === text) {
861
+ imports[id] = { ...memo.resolutions }
862
+
863
+ for (const step of memo.steps) {
864
+ if (typeof step === 'string') {
865
+ await visit(step)
866
+ continue
867
+ }
868
+
869
+ const contract = await readContract(step.physical)
870
+ if (contract !== undefined) contracts[step.id] = contract
871
+ }
872
+
873
+ lazy[id] = memo.lazy
874
+ return
875
+ }
876
+
451
877
  const resolutions: Record<string, string | null> = Object.create(null)
878
+ const steps: Edges['steps'][number][] = []
452
879
 
453
880
  imports[id] = resolutions
454
881
 
@@ -519,15 +946,25 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
519
946
  ) {
520
947
  sourceEntrypoints.add(normalize(resolved.id))
521
948
  resolutions[specifier] = sourceId(resolved.id)
949
+ steps.push(resolved.id)
522
950
  await visit(resolved.id)
523
951
  continue
524
952
  }
953
+
954
+ // An excluded source stays a runtime import instead of deriving a package contract.
955
+ if (excluded(resolved.id)) {
956
+ resolutions[specifier] = null
957
+ continue
958
+ }
959
+
525
960
  if (resolved.external || !eligible(resolved.id)) {
526
961
  const physical = resolved.id.split('?')[0]!.split('#')[0]!
527
- const contract =
962
+ const readable =
528
963
  Path.isAbsolute(physical) && /\.[cm]?[jt]sx?$/.test(physical)
529
- ? await packages.read(physical, contractHost)
530
- : undefined
964
+
965
+ if (readable) steps.push({ id: resolved.id, physical })
966
+
967
+ const contract = readable ? await readContract(physical) : undefined
531
968
 
532
969
  if (contract !== undefined) {
533
970
  contracts[resolved.id] = contract
@@ -540,6 +977,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
540
977
  }
541
978
 
542
979
  resolutions[specifier] = sourceId(resolved.id)
980
+ steps.push(resolved.id)
543
981
  await visit(resolved.id)
544
982
  }
545
983
 
@@ -573,6 +1011,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
573
1011
  }
574
1012
 
575
1013
  lazy[id] = targets
1014
+ memos?.set(file, { lazy: targets, resolutions, steps, text })
576
1015
  }
577
1016
 
578
1017
  await visit(entry.file, code)
@@ -648,7 +1087,11 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
648
1087
  continue
649
1088
 
650
1089
  const resolved = await resolve(specifier, file)
651
- if (!resolved || (!resolved.external && eligible(resolved.id)))
1090
+ if (
1091
+ !resolved ||
1092
+ (!resolved.external && eligible(resolved.id)) ||
1093
+ excluded(resolved.id)
1094
+ )
652
1095
  continue
653
1096
 
654
1097
  if (resource(specifier) || resource(resolved.id)) continue
@@ -657,7 +1100,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
657
1100
  if (!Path.isAbsolute(physical) || !/\.[cm]?[jt]sx?$/.test(physical))
658
1101
  continue
659
1102
 
660
- const contract = await packages.read(physical, contractHost)
1103
+ const contract = await readContract(physical)
661
1104
  if (contract === undefined) continue
662
1105
 
663
1106
  contracts[resolved.id] = contract
@@ -685,22 +1128,10 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
685
1128
 
686
1129
  loaded.add(id)
687
1130
 
688
- const metadata = (() => {
689
- try {
690
- return JSON.parse(contracts[id]!)
691
- } catch (error) {
692
- Graph.compile({ modules: {}, contracts: { [id]: contracts[id]! } })
693
- throw error
694
- }
695
- })()
696
- if (!Array.isArray(metadata?.stylesheets)) return
697
-
698
- for (const section of metadata.stylesheets) {
699
- if (!Array.isArray(section?.dependency)) continue
700
-
1131
+ for (const dependency of stylesheetDependencies(id, contracts[id]!)) {
701
1132
  let owner = id
702
1133
 
703
- for (const specifier of section.dependency) {
1134
+ for (const specifier of dependency) {
704
1135
  if (
705
1136
  typeof specifier !== 'string' ||
706
1137
  !specifier ||
@@ -715,9 +1146,9 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
715
1146
  imports[owner] ??= Object.create(null)
716
1147
  imports[owner]![specifier] = target.id
717
1148
 
718
- contracts[target.id] ??= (await packages.read(
1149
+ contracts[target.id] ??= (await readContract(
719
1150
  target.id.split(/[?#]/)[0]!,
720
- { ...contractHost, required: true },
1151
+ true,
721
1152
  ))!
722
1153
 
723
1154
  await dependencies(target.id)
@@ -748,9 +1179,9 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
748
1179
  ]),
749
1180
  )
750
1181
 
751
- const result = entry.compiler.compile({
1182
+ const input: Graph.compile.Options = {
1183
+ [Results.lean]: true,
752
1184
  [ThemeRules.shared]: native ? undefined : 'all',
753
- [Syntax.cache]: snapshot(entry.environment).programs(modules),
754
1185
  compiler: options.compiler,
755
1186
  reset: options.reset ? Reset.read() : undefined,
756
1187
  native,
@@ -758,7 +1189,11 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
758
1189
  development: entry.environment.mode !== 'build',
759
1190
  imports,
760
1191
  modules,
761
- })
1192
+ }
1193
+ const result =
1194
+ entry.environment.mode === 'dev' && !native && !everything
1195
+ ? compileProject(entry.environment, input, sourceId(entry.file))
1196
+ : entry.compiler.compile(input)
762
1197
 
763
1198
  if (native) {
764
1199
  const output = result.modules[sourceId(entry.file)]!
@@ -772,6 +1207,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
772
1207
  configurations: new Map<string, Catalogs.Catalog>(),
773
1208
  connections: staged,
774
1209
  css: '',
1210
+ graphSharedCss: '',
775
1211
  sharedCss: '',
776
1212
  cssMap: map,
777
1213
  sharedCssMap: map,
@@ -807,166 +1243,181 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
807
1243
 
808
1244
  const configurations = new Map<string, Catalogs.Catalog>()
809
1245
  for (const [id, contract] of Object.entries(compiled))
810
- for (const configuration of Catalogs.read(contract)) {
1246
+ for (const configuration of read(contract)) {
811
1247
  const key = `${id}\0${configuration.identity}`
812
1248
  catalogs.set(key, configuration)
813
1249
  configurations.set(key, configuration)
814
1250
  }
815
1251
 
816
- const owners = new Map<string, string>()
1252
+ // Development renders the shared stylesheet for its own request only; transforms and module
1253
+ // stylesheets read module outputs.
1254
+ async function renderShared() {
1255
+ const owners = new Map<string, string>()
817
1256
 
818
- for (const id of new Set(Object.values(result.sharedAssetOwners ?? {}))) {
819
- if (!Path.isAbsolute(id)) continue
1257
+ for (const id of new Set(Object.values(result.sharedAssetOwners ?? {}))) {
1258
+ if (!Path.isAbsolute(id)) continue
820
1259
 
821
- let directory = Path.dirname(id.split(/[?#]/)[0]!)
1260
+ let directory = Path.dirname(id.split(/[?#]/)[0]!)
822
1261
 
823
- for (;;) {
824
- try {
825
- await Fs.access(Path.join(directory, 'package.json'))
826
- owners.set(id, await Fs.realpath(directory))
827
- break
828
- } catch (error) {
829
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
830
- }
1262
+ for (;;) {
1263
+ try {
1264
+ await Fs.access(Path.join(directory, 'package.json'))
1265
+ owners.set(id, await Fs.realpath(directory))
1266
+ break
1267
+ } catch (error) {
1268
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
1269
+ }
831
1270
 
832
- const parent = Path.dirname(directory)
833
- if (parent === directory)
834
- throw new Error('Packed assets require an owning package.json.')
1271
+ const parent = Path.dirname(directory)
1272
+ if (parent === directory)
1273
+ throw new Error('Packed assets require an owning package.json.')
835
1274
 
836
- directory = parent
1275
+ directory = parent
1276
+ }
837
1277
  }
838
- }
839
1278
 
840
- const assetUrls = new Map<string, string>()
1279
+ const assetUrls = new Map<string, string>()
841
1280
 
842
- for (const [key, target] of Object.entries(result.sharedAssets ?? {})) {
843
- const raw = target.split(/[?#]/)[0]!
1281
+ for (const [key, target] of Object.entries(result.sharedAssets ?? {})) {
1282
+ const raw = target.split(/[?#]/)[0]!
844
1283
 
845
- const identity = result.sharedAssetOwners?.[key]
846
- const path =
847
- sourceFile(decodeURIComponent(raw)) ??
848
- (Path.isAbsolute(raw) ? decodeURIComponent(raw) : undefined)
849
- if (!path) throw new Error('Asset path escapes the Vite graph.')
1284
+ const identity = result.sharedAssetOwners?.[key]
1285
+ const path =
1286
+ sourceFile(decodeURIComponent(raw)) ??
1287
+ (Path.isAbsolute(raw) ? decodeURIComponent(raw) : undefined)
1288
+ if (!path) throw new Error('Asset path escapes the Vite graph.')
850
1289
 
851
- const asset = await (async () => {
852
- try {
853
- return { file: await Fs.realpath(path), package: false }
854
- } catch (error) {
855
- if (
856
- (error as NodeJS.ErrnoException).code !== 'ENOENT' ||
857
- !identity ||
858
- !sourceFile(identity)
859
- )
860
- throw error
861
-
862
- const importer = sourceFile(identity)!
863
- const specifier = Path.relative(Path.dirname(importer), path)
864
- .split(Path.sep)
865
- .join('/')
866
- if (specifier.startsWith('.') || Path.isAbsolute(specifier))
867
- throw error
868
- const resolved = await host.resolve(specifier, importer)
869
- if (!resolved || resolved.external) throw error
870
- return {
871
- file: await Fs.realpath(resolved.id.split('?')[0]!),
872
- package: true,
1290
+ const asset = await (async () => {
1291
+ try {
1292
+ return { file: await Fs.realpath(path), package: false }
1293
+ } catch (error) {
1294
+ if (
1295
+ (error as NodeJS.ErrnoException).code !== 'ENOENT' ||
1296
+ !identity ||
1297
+ !sourceFile(identity)
1298
+ )
1299
+ throw error
1300
+
1301
+ const importer = sourceFile(identity)!
1302
+ const specifier = Path.relative(Path.dirname(importer), path)
1303
+ .split(Path.sep)
1304
+ .join('/')
1305
+ if (specifier.startsWith('.') || Path.isAbsolute(specifier))
1306
+ throw error
1307
+ const resolved = await host.resolve(specifier, importer)
1308
+ if (!resolved || resolved.external) throw error
1309
+ return {
1310
+ file: await Fs.realpath(resolved.id.split('?')[0]!),
1311
+ package: true,
1312
+ }
873
1313
  }
874
- }
875
- })()
876
- const file = asset.file
877
-
878
- const owner =
879
- identity && sourceFile(identity)
880
- ? sourceRoot(sourceFile(identity)!)
881
- : identity
882
- ? owners.get(identity)
883
- : undefined
884
-
885
- const relative = owner ? Path.relative(owner, file) : '..'
886
- if (
887
- !asset.package &&
888
- (relative === '..' ||
889
- relative.startsWith(`..${Path.sep}`) ||
890
- Path.isAbsolute(relative))
891
- )
892
- throw new Error('Asset path escapes its owning package.')
1314
+ })()
1315
+ const file = asset.file
893
1316
 
894
- host.watch(file)
895
- files.add(file)
1317
+ const owner =
1318
+ identity && sourceFile(identity)
1319
+ ? sourceRoot(sourceFile(identity)!)
1320
+ : identity
1321
+ ? owners.get(identity)
1322
+ : undefined
896
1323
 
897
- let url: string
1324
+ const relative = owner ? Path.relative(owner, file) : '..'
1325
+ if (
1326
+ !asset.package &&
1327
+ (relative === '..' ||
1328
+ relative.startsWith(`..${Path.sep}`) ||
1329
+ Path.isAbsolute(relative))
1330
+ )
1331
+ throw new Error('Asset path escapes its owning package.')
898
1332
 
899
- if (asset.package || /[?#%]/.test(file)) {
900
- if (entry.environment.mode === 'build') {
901
- url = await host.asset(file)
1333
+ host.watch(file)
1334
+ files.add(file)
902
1335
 
903
- if (target.slice(raw.length)) url += `$_${target.slice(raw.length)}__`
1336
+ let url: string
1337
+
1338
+ if (asset.package || /[?#%]/.test(file)) {
1339
+ if (entry.environment.mode === 'build') {
1340
+ url = await host.asset(file)
1341
+
1342
+ if (target.slice(raw.length))
1343
+ url += `$_${target.slice(raw.length)}__`
1344
+ } else {
1345
+ url = `/@zyzz/asset/${Crypto.hash('sha256', file)}/${encodeURIComponent(Path.basename(file))}`
1346
+ assets.set(url, file)
1347
+ url += target.slice(raw.length)
1348
+ }
904
1349
  } else {
905
- url = `/@zyzz/asset/${Crypto.hash('sha256', file)}/${encodeURIComponent(Path.basename(file))}`
906
- assets.set(url, file)
907
- url += target.slice(raw.length)
1350
+ // Vite reads `/@fs/` URLs with `decodeURI`, which keeps reserved escapes
1351
+ // such as `%40` in scoped package paths, so those characters stay raw.
1352
+ const pathname = encodeURI(file.replaceAll('\\', '/'))
1353
+
1354
+ url = `/@fs/${pathname}${target.slice(raw.length)}`
908
1355
  }
909
- } else {
910
- // Vite reads `/@fs/` URLs with `decodeURI`, which keeps reserved escapes
911
- // such as `%40` in scoped package paths, so those characters stay raw.
912
- const pathname = encodeURI(file.replaceAll('\\', '/'))
913
1356
 
914
- url = `/@fs/${pathname}${target.slice(raw.length)}`
1357
+ assetUrls.set(target, url)
915
1358
  }
916
1359
 
917
- assetUrls.set(target, url)
918
- }
919
-
920
- const shared = !Object.keys(result.sharedAssets ?? {}).length
921
- ? {
922
- code: new TextEncoder().encode(result.sharedCss ?? ''),
923
- map: new TextEncoder().encode(
924
- JSON.stringify(
1360
+ const shared = !Object.keys(result.sharedAssets ?? {}).length
1361
+ ? {
1362
+ code: new TextEncoder().encode(result.sharedCss ?? ''),
1363
+ map: new TextEncoder().encode(
1364
+ JSON.stringify(
1365
+ result.sharedCssMap ??
1366
+ Mapping.toEncodedMap(new Mapping.GenMapping()),
1367
+ ),
1368
+ ),
1369
+ }
1370
+ : AtRules.transform({
1371
+ filename: 'zyzz.shared.css',
1372
+ code: new TextEncoder().encode(result.sharedCss ?? ''),
1373
+ sourceMap: true,
1374
+ inputSourceMap: JSON.stringify(
925
1375
  result.sharedCssMap ??
926
1376
  Mapping.toEncodedMap(new Mapping.GenMapping()),
927
1377
  ),
928
- ),
929
- }
930
- : AtRules.transform({
931
- filename: 'zyzz.shared.css',
932
- code: new TextEncoder().encode(result.sharedCss ?? ''),
933
- sourceMap: true,
934
- inputSourceMap: JSON.stringify(
935
- result.sharedCssMap ??
936
- Mapping.toEncodedMap(new Mapping.GenMapping()),
937
- ),
938
- visitor: {
939
- Rule(rule) {
940
- if (rule.type !== 'import') return
941
- const target = result.sharedAssets?.[rule.value.url]
942
- if (target)
943
- return AtRules.relocateImport(
944
- rule.value,
945
- assetUrls.get(target)!,
946
- )
947
- },
948
- Url(url) {
949
- const target = result.sharedAssets?.[url.url]
950
- if (!target) return
1378
+ visitor: {
1379
+ Rule(rule) {
1380
+ if (rule.type !== 'import') return
1381
+ const target = result.sharedAssets?.[rule.value.url]
1382
+ if (target)
1383
+ return AtRules.relocateImport(
1384
+ rule.value,
1385
+ assetUrls.get(target)!,
1386
+ )
1387
+ },
1388
+ Url(url) {
1389
+ const target = result.sharedAssets?.[url.url]
1390
+ if (!target) return
951
1391
 
952
- return { ...url, url: assetUrls.get(target)! }
1392
+ return { ...url, url: assetUrls.get(target)! }
1393
+ },
953
1394
  },
954
- },
955
- })
1395
+ })
956
1396
 
957
- const sharedMap = JSON.parse(
958
- new TextDecoder().decode(shared.map!),
959
- ) as Mapping.EncodedSourceMap
1397
+ const sharedMap = JSON.parse(
1398
+ new TextDecoder().decode(shared.map!),
1399
+ ) as Mapping.EncodedSourceMap
960
1400
 
961
- sharedMap.sources = sharedMap.sources.map((source) =>
962
- source === null ? null : (sourceFile(source) ?? source),
963
- )
1401
+ sharedMap.sources = sharedMap.sources.map((source) =>
1402
+ source === null ? null : (sourceFile(source) ?? source),
1403
+ )
1404
+
1405
+ return { css: new TextDecoder().decode(shared.code), map: sharedMap }
1406
+ }
1407
+
1408
+ const sharedOutput =
1409
+ entry.environment.mode !== 'dev' || allSources
1410
+ ? await renderShared()
1411
+ : {
1412
+ css: '',
1413
+ map: Mapping.toEncodedMap(new Mapping.GenMapping()),
1414
+ }
964
1415
 
965
1416
  const output = result.modules[sourceId(entry.file)]!
966
1417
 
967
1418
  entry.files = files
968
1419
 
969
- return {
1420
+ const value = {
970
1421
  imports,
971
1422
  modules: result.modules,
972
1423
  code: output.code,
@@ -974,8 +1425,10 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
974
1425
  configurations,
975
1426
  connections: staged,
976
1427
  css: output.css,
977
- sharedCss: new TextDecoder().decode(shared.code),
978
- sharedCssMap: sharedMap,
1428
+ // Edits compare the graph's stylesheet, which precedes asset relocation.
1429
+ graphSharedCss: result.sharedCss ?? '',
1430
+ sharedCss: sharedOutput.css,
1431
+ sharedCssMap: sharedOutput.map,
979
1432
  cssMap: {
980
1433
  ...output.cssMap,
981
1434
  sources: output.cssMap.sources.map((source) =>
@@ -989,6 +1442,8 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
989
1442
  ),
990
1443
  },
991
1444
  }
1445
+
1446
+ return value
992
1447
  }
993
1448
 
994
1449
  /** Compiles every discovered source once so index.html can inline each configuration's script before modules load. */
@@ -997,9 +1452,8 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
997
1452
  reads.delete(environment)
998
1453
  const host: Host = {
999
1454
  asset: async (file) => file,
1000
- resolve: (source, importer) =>
1001
- environment.pluginContainer.resolveId(source, importer),
1002
- watch: (file) => server.watcher.add(file),
1455
+ resolve: resolver(environment),
1456
+ watch: (file) => watch(server, file),
1003
1457
  }
1004
1458
  const sources = await discover(environment, host)
1005
1459
  let entry = initializers.get(environment)
@@ -1009,8 +1463,9 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1009
1463
  const file = sources.keys().next().value
1010
1464
  if (file === undefined) return { catalogs: new Map(), edges: new Map() }
1011
1465
 
1466
+ // Whole-project compiles keep their own cache beside the transforms' merged graph.
1012
1467
  entry = {
1013
- compiler: compiler(environment),
1468
+ compiler: initializers.get(environment)?.compiler ?? Graph.create(),
1014
1469
  environment,
1015
1470
  file,
1016
1471
  files: new Set([file]),
@@ -1186,13 +1641,165 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1186
1641
  .map(([, configuration]) => configuration)
1187
1642
  }
1188
1643
 
1644
+ /**
1645
+ * The modules an edit invalidates: those whose code or stylesheet differs from what Vite served.
1646
+ * A concurrent compile can already hold the edit, so outputs are not compared with each other.
1647
+ */
1648
+ async function changes(
1649
+ environment: DevEnvironment,
1650
+ after: Graph.compile.ReturnType,
1651
+ served: Served,
1652
+ modules: readonly EnvironmentModuleNode[],
1653
+ timestamp: number,
1654
+ edited: string,
1655
+ deleted: string | undefined,
1656
+ ) {
1657
+ // Portable build adapters share this module and do not require Vite.
1658
+ const { transformWithOxc } = await import('vite')
1659
+ const code = new Set<string>()
1660
+ const css = new Set<string>()
1661
+
1662
+ for (const [
1663
+ entry,
1664
+ { code: current, css: stylesheet, imports },
1665
+ ] of served.entries) {
1666
+ const id = sourceId(entry.file)
1667
+ const next = after.modules[id]
1668
+
1669
+ if (!next) continue
1670
+
1671
+ // A stylesheet Vite never loaded has no module to invalidate.
1672
+ if (stylesheet !== undefined && stylesheet !== next.css)
1673
+ css.add(entry.file)
1674
+
1675
+ // A stylesheet request can compile a module whose code was never transformed here.
1676
+ if (current === undefined) continue
1677
+
1678
+ if (imports !== JSON.stringify(projects.get(environment)?.imports[id])) {
1679
+ code.add(entry.file)
1680
+ continue
1681
+ }
1682
+
1683
+ if (current === next.code) continue
1684
+
1685
+ // Generated type assertions can change without changing runtime bindings.
1686
+ const [left, right] = await Promise.all([
1687
+ transformWithOxc(current, entry.file, { sourcemap: false }),
1688
+ transformWithOxc(next.code, entry.file, { sourcemap: false }),
1689
+ ])
1690
+
1691
+ if (left.code !== right.code) code.add(entry.file)
1692
+ }
1693
+
1694
+ const stylesheets = new Set<string>()
1695
+
1696
+ if (deleted !== undefined) stylesheets.add(cssId(deleted))
1697
+ if (
1698
+ served.shared !== undefined &&
1699
+ served.shared !== (after.sharedCss ?? '')
1700
+ )
1701
+ stylesheets.add(sharedId)
1702
+ for (const file of css) stylesheets.add(cssId(file))
1703
+
1704
+ // Compiled modules the edit left unchanged skip Vite's own update; other modules, such as
1705
+ // plain JavaScript or assets, keep it.
1706
+ const affected = new Set(
1707
+ modules.filter((module) => {
1708
+ if (!module.id) return module.type !== 'asset'
1709
+
1710
+ const file = normalize(module.id)
1711
+
1712
+ return Object.hasOwn(after.modules, sourceId(file))
1713
+ ? code.has(file)
1714
+ : true
1715
+ }),
1716
+ )
1717
+
1718
+ for (const file of code) {
1719
+ const module = environment.moduleGraph.getModuleById(file)
1720
+ if (!module) continue
1721
+
1722
+ environment.moduleGraph.invalidateModule(
1723
+ module,
1724
+ new Set(),
1725
+ timestamp,
1726
+ true,
1727
+ )
1728
+ affected.add(module)
1729
+ }
1730
+
1731
+ const sheets: EnvironmentModuleNode[] = []
1732
+
1733
+ // The edited module's stylesheet updates first, ahead of stylesheets other modules share.
1734
+ for (const id of [...stylesheets].sort(
1735
+ (left, right) =>
1736
+ Number(right === cssId(edited)) - Number(left === cssId(edited)),
1737
+ )) {
1738
+ const module = environment.moduleGraph.getModuleById(id)
1739
+ if (!module) continue
1740
+
1741
+ environment.moduleGraph.invalidateModule(
1742
+ module,
1743
+ new Set(),
1744
+ timestamp,
1745
+ true,
1746
+ )
1747
+ sheets.push(module)
1748
+ }
1749
+
1750
+ // A stylesheet accepts its own updates and only its module imports it, so it is never within
1751
+ // an import cycle. Vite proves that by walking every importer of the importing module, which
1752
+ // costs each stylesheet of a shared edit the whole graph, so stylesheet-only edits send the
1753
+ // same update directly. Edits that also change code keep Vite's single batched update.
1754
+ if (
1755
+ !affected.size &&
1756
+ sheets.length &&
1757
+ sheets.every((module) => module.isSelfAccepting)
1758
+ ) {
1759
+ const updates = sheets.map((module) => {
1760
+ const path =
1761
+ module.url.startsWith('/') || module.url.startsWith('.')
1762
+ ? module.url
1763
+ : `/@id/${module.url.replace('\0', '__x00__')}`
1764
+
1765
+ return {
1766
+ acceptedPath: path,
1767
+ explicitImportRequired: false,
1768
+ isWithinCircularImport: false,
1769
+ path,
1770
+ timestamp,
1771
+ type: 'js-update' as const,
1772
+ }
1773
+ })
1774
+
1775
+ environment.logger.info(
1776
+ `hmr update ${updates.map((update) => update.path).join(', ')}`,
1777
+ { timestamp: true },
1778
+ )
1779
+ environment.hot.send({ type: 'update', updates })
1780
+ return []
1781
+ }
1782
+
1783
+ for (const module of sheets) affected.add(module)
1784
+
1785
+ return [...affected]
1786
+ }
1787
+
1189
1788
  return {
1190
1789
  buildStart() {
1790
+ edgeMemos.delete(this.environment)
1191
1791
  reads.delete(this.environment)
1792
+ lookups.delete(this.environment)
1192
1793
  },
1193
1794
  config: {
1194
1795
  order: 'post',
1195
- handler(config) {
1796
+ handler(config, env) {
1797
+ // Ordered after other config hooks, so plugin-provided aliases are visible.
1798
+ optimizeRuntime =
1799
+ env.command === 'serve' &&
1800
+ options.compiler !== false &&
1801
+ installed(config)
1802
+
1196
1803
  if (native) return
1197
1804
  // Inline color-scheme changes cannot initialize Lightning's lowered helpers.
1198
1805
  return {
@@ -1221,7 +1828,35 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1221
1828
  }
1222
1829
  },
1223
1830
  },
1831
+ configEnvironment: {
1832
+ // Plugins without enforce settle their environment options first.
1833
+ order: 'post',
1834
+ handler(name, config) {
1835
+ if (!optimizeRuntime) return
1836
+
1837
+ const optimizeDeps = config.optimizeDeps ?? {}
1838
+ const consumer =
1839
+ config.consumer ?? (name === 'client' ? 'client' : 'server')
1840
+
1841
+ // Vite defaults server environments to noDiscovery. An include list there
1842
+ // would create an explicit optimizer that otherwise never exists.
1843
+ if (optimizeDeps.noDiscovery ?? consumer === 'server') return
1844
+ if (
1845
+ optimizeDeps.include?.includes(runtime) ||
1846
+ optimizeDeps.exclude?.some((id) => id === 'zyzz' || id === runtime)
1847
+ )
1848
+ return
1849
+
1850
+ // Vite scans authored source, so the runtime import that compilation
1851
+ // adds first appears while a request transforms a module. Discovering
1852
+ // it then re-runs the optimizer, and modules loaded before the re-run
1853
+ // can keep a separate copy of a shared dependency such as React.
1854
+ return { optimizeDeps: { include: [runtime] } }
1855
+ },
1856
+ },
1224
1857
  configureServer(server) {
1858
+ devServer = server
1859
+
1225
1860
  // Directory watches belong to the server, not module import dependencies.
1226
1861
  server.watcher.add(directories.slice(1))
1227
1862
 
@@ -1257,6 +1892,15 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1257
1892
  }),
1258
1893
  )),
1259
1894
  ]
1895
+
1896
+ // Portable build adapters share this module and do not require Vite.
1897
+ const { createFilter } = await import('vite')
1898
+ const patterns = [...configFiles, ...(options.exclude ?? [])]
1899
+
1900
+ filter = createFilter(null, patterns, { resolve: root })
1901
+ exclusions.clear()
1902
+ roots.clear()
1903
+
1260
1904
  if (native) return
1261
1905
  const lightning = (config.css.lightningcss ??= {})
1262
1906
  lightning.include =
@@ -1348,10 +1992,65 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1348
1992
  },
1349
1993
  },
1350
1994
  async watchChange(file, change) {
1995
+ // A removed file leaves the watcher, so a later compile that reads it again adds it again.
1996
+ if (change.event === 'delete') watched.delete(file)
1997
+
1351
1998
  await updateDiscovery(this.environment, file, change.event)
1352
1999
  },
1353
2000
  async hotUpdate({ file, modules, timestamp, type, server }) {
1354
2001
  const state = entries(this.environment)
2002
+ const before = projects.get(this.environment)?.outcome
2003
+ // Concurrent transforms update entries while this compiles, so the served output is read now.
2004
+ const served: Served = {
2005
+ entries: new Map(
2006
+ [...state.values()].map((entry) => [
2007
+ entry,
2008
+ { code: entry.code, css: entry.css, imports: entry.imports },
2009
+ ]),
2010
+ ),
2011
+ shared: servedShared.get(this.environment),
2012
+ }
2013
+
2014
+ // The merged development graph compares module outputs before and after the edit, so
2015
+ // only modules whose code or stylesheet changed update.
2016
+ if (!native && before && 'result' in before) {
2017
+ await updateDiscovery(this.environment, file, type)
2018
+
2019
+ // The edited module's own entry, or one whose closure reads it, merges its new source.
2020
+ // A contribution source no module imports joins every closure.
2021
+ const first =
2022
+ state.get(file) ??
2023
+ [...state.values()].find((entry) => entry.files.has(file)) ??
2024
+ (eager(file, this.environment)
2025
+ ? state.values().next().value
2026
+ : undefined)
2027
+ if (!first && !removed.get(this.environment)?.has(file)) return modules
2028
+
2029
+ const compiled =
2030
+ first && type === 'update' && eligible(file)
2031
+ ? await compile(first, {
2032
+ resolve: resolver(this.environment),
2033
+ watch: (file) => watch(server, file),
2034
+ asset: async (file) => file,
2035
+ }).then(
2036
+ () => true,
2037
+ () => false,
2038
+ )
2039
+ : false
2040
+ const after = projects.get(this.environment)?.outcome
2041
+
2042
+ if (compiled && after && 'result' in after)
2043
+ return await changes(
2044
+ this.environment,
2045
+ after.result,
2046
+ served,
2047
+ modules,
2048
+ timestamp,
2049
+ file,
2050
+ type === 'delete' ? file : undefined,
2051
+ )
2052
+ }
2053
+
1355
2054
  const previous = [...state.values()].map((entry) => ({
1356
2055
  code: entry.code,
1357
2056
  file: entry.file,
@@ -1370,11 +2069,8 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1370
2069
  ? await compile(
1371
2070
  first,
1372
2071
  {
1373
- resolve: (source, importer) =>
1374
- this.environment.pluginContainer.resolveId(source, importer),
1375
- watch: (file) => {
1376
- server.watcher.add(file)
1377
- },
2072
+ resolve: resolver(this.environment),
2073
+ watch: (file) => watch(server, file),
1378
2074
  asset: async (file) => file,
1379
2075
  },
1380
2076
  undefined,
@@ -1454,19 +2150,49 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1454
2150
  id === sharedId
1455
2151
  ? undefined
1456
2152
  : decodeURIComponent(id.slice(prefix.length, -4))
1457
- const entry =
2153
+ let entry =
1458
2154
  file === undefined
1459
2155
  ? entries(this.environment).values().next().value
1460
2156
  : entries(this.environment).get(file)
1461
2157
  // A pruned JavaScript module can leave its stylesheet loaded in the browser.
1462
2158
  if (!entry && file && removed.get(this.environment)?.has(file)) return ''
2159
+
2160
+ // Server rendering links the stylesheets of modules this environment has not transformed
2161
+ // yet, so a development request compiles the module where the browser loads it.
2162
+ if (!entry && this.environment.mode === 'dev') {
2163
+ const anchor =
2164
+ file ??
2165
+ (
2166
+ await discover(this.environment, {
2167
+ asset: async (file) => file,
2168
+ resolve: resolver(this.environment),
2169
+ watch: watcher(this),
2170
+ })
2171
+ )
2172
+ .keys()
2173
+ .next().value
2174
+
2175
+ if (anchor !== undefined && eligible(anchor)) {
2176
+ entry = {
2177
+ compiler: compiler(this.environment),
2178
+ environment: this.environment,
2179
+ file: anchor,
2180
+ files: new Set([anchor]),
2181
+ }
2182
+ entries(this.environment).set(anchor, entry)
2183
+ }
2184
+ }
2185
+
1463
2186
  if (!entry) throw new Error(`Unknown Zyzz stylesheet: ${file}`)
1464
2187
 
1465
2188
  const output = await compile(
1466
2189
  entry,
1467
2190
  {
1468
- resolve: (source, importer) => this.resolve(source, importer),
1469
- watch: (file) => this.addWatchFile(file),
2191
+ resolve:
2192
+ this.environment.mode === 'dev'
2193
+ ? resolver(this.environment)
2194
+ : (source, importer) => this.resolve(source, importer),
2195
+ watch: watcher(this),
1470
2196
  asset: async (file) => {
1471
2197
  const reference = this.emitFile({
1472
2198
  type: 'asset',
@@ -1494,12 +2220,19 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1494
2220
  '-zyzz-fpv-000000000',
1495
2221
  )
1496
2222
  : css
1497
- if (id === sharedId)
2223
+ if (id === sharedId) {
2224
+ servedShared.set(this.environment, output.graphSharedCss)
1498
2225
  return {
1499
2226
  code: transport(output.sharedCss),
1500
2227
  map: JSON.stringify(output.sharedCssMap),
1501
2228
  }
1502
- return { code: transport(output.css), map: JSON.stringify(output.cssMap) }
2229
+ }
2230
+
2231
+ entry.css = output.css
2232
+ return {
2233
+ code: transport(output.css),
2234
+ map: JSON.stringify(output.cssMap),
2235
+ }
1503
2236
  },
1504
2237
  name: 'zyzz',
1505
2238
  resolveId(id) {
@@ -1524,11 +2257,16 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1524
2257
  removed.get(this.environment)?.delete(id)
1525
2258
  }
1526
2259
 
2260
+ entry.source = code
2261
+
1527
2262
  const output = await compile(
1528
2263
  entry,
1529
2264
  {
1530
- resolve: (source, importer) => this.resolve(source, importer),
1531
- watch: (file) => this.addWatchFile(file),
2265
+ resolve:
2266
+ this.environment.mode === 'dev'
2267
+ ? resolver(this.environment)
2268
+ : (source, importer) => this.resolve(source, importer),
2269
+ watch: watcher(this),
1532
2270
  asset: async (file) => {
1533
2271
  const reference = this.emitFile({
1534
2272
  type: 'asset',
@@ -1550,64 +2288,29 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1550
2288
 
1551
2289
  // Keep the CSS dependency even when the current graph has no live rules.
1552
2290
  // Later edits can introduce styles without changing this import boundary.
1553
- const parsed = Parser.parseSync('source.tsx', output.code, {
1554
- sourceType: 'module',
1555
- })
1556
- let offset = output.code.startsWith('#!')
1557
- ? output.code.indexOf('\n') + 1
1558
- : 0
1559
-
1560
- for (const node of parsed.program.body) {
1561
- if (node.type !== 'ExpressionStatement' || !node.directive) break
1562
-
1563
- offset = node.end
1564
- }
1565
-
2291
+ const offset = prologue(output.code)
1566
2292
  const sharedImport = `\nimport ${JSON.stringify(sharedId)};\n`
1567
- const map = Mapping.fromMap(JSON.stringify(output.map))
1568
2293
  // The inserted dependency shifts only generated positions after the prologue.
1569
2294
  const before = output.code.slice(0, offset)
1570
2295
  const insertionLine = before.split('\n').length
1571
2296
  const insertionColumn = before.length - (before.lastIndexOf('\n') + 1)
1572
- const shifted = new Mapping.GenMapping()
1573
-
1574
- for (const mapping of Mapping.allMappings(map)) {
1575
- const generated = { ...mapping.generated }
1576
-
1577
- if (
1578
- generated.line > insertionLine ||
1579
- (generated.line === insertionLine &&
1580
- generated.column >= insertionColumn)
1581
- ) {
1582
- if (generated.line === insertionLine)
1583
- generated.column -= insertionColumn
1584
-
1585
- generated.line += 2
1586
- }
1587
-
1588
- if (mapping.source !== undefined && mapping.original !== undefined) {
1589
- const location = {
1590
- generated,
1591
- original: mapping.original,
1592
- source: mapping.source,
1593
- }
1594
-
1595
- if (mapping.name === undefined) Mapping.addMapping(shifted, location)
1596
- else Mapping.addMapping(shifted, { ...location, name: mapping.name })
1597
- } else Mapping.addMapping(shifted, { generated })
1598
- }
1599
-
1600
- for (const [index, source] of output.map.sources.entries())
1601
- if (source !== null)
1602
- Mapping.setSourceContent(
1603
- shifted,
1604
- source,
1605
- output.map.sourcesContent?.[index] ?? null,
1606
- )
2297
+ // A build compiles a fixed project, so an empty module sheet stays empty. Importing it
2298
+ // would link an empty stylesheet from every chunk that holds no styled module.
2299
+ const stylesheetImport =
2300
+ this.environment.mode === 'build' && !output.css.trim()
2301
+ ? ''
2302
+ : `\nimport ${JSON.stringify(cssId(id))};`
1607
2303
 
1608
2304
  return {
1609
- code: `${before}${sharedImport}${output.code.slice(offset)}\nimport ${JSON.stringify(cssId(id))};`,
1610
- map: JSON.stringify(Mapping.toEncodedMap(shifted)),
2305
+ code: `${before}${sharedImport}${output.code.slice(offset)}${stylesheetImport}`,
2306
+ map: JSON.stringify({
2307
+ ...output.map,
2308
+ mappings: insert(
2309
+ output.map.mappings,
2310
+ insertionLine - 1,
2311
+ insertionColumn,
2312
+ ),
2313
+ }),
1611
2314
  }
1612
2315
  },
1613
2316
  transformIndexHtml: {
@@ -1629,8 +2332,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1629
2332
  return await documentCatalogs(
1630
2333
  html,
1631
2334
  context.filename,
1632
- (source, importer) =>
1633
- environment.pluginContainer.resolveId(source, importer),
2335
+ resolver(environment),
1634
2336
  state,
1635
2337
  )
1636
2338
  })()
@@ -1645,13 +2347,25 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1645
2347
  }
1646
2348
  }
1647
2349
 
2350
+ /** One module's import edges: its resolutions and, in order, its visits and contract reads. */
2351
+ type Edges = {
2352
+ lazy: ReadonlySet<string>
2353
+ resolutions: Readonly<Record<string, string | null>>
2354
+ steps: readonly (string | { id: string; physical: string })[]
2355
+ text: string
2356
+ }
2357
+
1648
2358
  type Entry = {
1649
2359
  code?: string
1650
- imports?: string
1651
- environment: Environment
1652
2360
  compiler: Graph.create.ReturnType
2361
+ /** The stylesheet Vite last loaded for this module. */
2362
+ css?: string | undefined
2363
+ environment: Environment
1653
2364
  file: string
1654
2365
  files: Set<string>
2366
+ imports?: string
2367
+ /** The code Vite last passed to the transform, until a watcher event changes the file. */
2368
+ source?: string | undefined
1655
2369
  }
1656
2370
 
1657
2371
  type Host = {
@@ -1666,7 +2380,202 @@ type Host = {
1666
2380
  watch: (file: string) => void
1667
2381
  }
1668
2382
 
2383
+ type Lookups = {
2384
+ contracts: Map<
2385
+ string,
2386
+ Promise<{ contract: string | undefined; files: ReadonlySet<string> }>
2387
+ >
2388
+ /** Files each settled contract read, so an edit to one drops only its contracts. */
2389
+ read: Map<string, ReadonlySet<string>>
2390
+ resolutions: Map<string, ReturnType<Host['resolve']>>
2391
+ }
2392
+
2393
+ /** One development environment's merged source graph and its last compilation. */
2394
+ type Project = {
2395
+ compiler: Graph.create.ReturnType
2396
+ contracts: Record<string, string>
2397
+ /** Compiles a closure alone while the merged graph fails. */
2398
+ fallback: Graph.create.ReturnType
2399
+ imports: Record<string, Record<string, string | null>>
2400
+ modules: Record<string, string>
2401
+ outcome:
2402
+ | { readonly error: unknown }
2403
+ | { readonly result: Graph.compile.ReturnType }
2404
+ | undefined
2405
+ }
2406
+
2407
+ /** What an environment served when an edit arrived. */
2408
+ type Served = {
2409
+ entries: ReadonlyMap<
2410
+ Entry,
2411
+ {
2412
+ code: string | undefined
2413
+ css: string | undefined
2414
+ imports: string | undefined
2415
+ }
2416
+ >
2417
+ shared: string | undefined
2418
+ }
2419
+
2420
+ /**
2421
+ * The end of a module's hashbang and directive prologue, where an added import keeps directives
2422
+ * first. Source without a leading string literal skips parsing.
2423
+ */
2424
+ function prologue(code: string) {
2425
+ let index = code.startsWith('#!') ? code.indexOf('\n') + 1 : 0
2426
+ const start = index
2427
+
2428
+ for (;;) {
2429
+ while (index < code.length && /\s/.test(code[index]!)) index++
2430
+
2431
+ if (code.startsWith('//', index)) {
2432
+ const end = code.indexOf('\n', index)
2433
+ index = end === -1 ? code.length : end
2434
+ } else if (code.startsWith('/*', index)) {
2435
+ const end = code.indexOf('*/', index + 2)
2436
+ index = end === -1 ? code.length : end + 2
2437
+ } else break
2438
+ }
2439
+
2440
+ if (code[index] !== "'" && code[index] !== '"') return start
2441
+
2442
+ let offset = start
2443
+ const { program } = Parser.parseSync('source.tsx', code, {
2444
+ sourceType: 'module',
2445
+ })
2446
+
2447
+ for (const node of program.body) {
2448
+ if (node.type !== 'ExpressionStatement' || !node.directive) break
2449
+
2450
+ offset = node.end
2451
+ }
2452
+
2453
+ return offset
2454
+ }
2455
+
2456
+ /**
2457
+ * Moves encoded mappings for two lines inserted at a generated position. Segments at or past
2458
+ * `column` on `line` start the line after the insertion, and every later line moves down by two.
2459
+ * Other fields stay relative to the segment before them, which keeps its order.
2460
+ */
2461
+ function insert(mappings: string, line: number, column: number) {
2462
+ const lines = mappings.split(';')
2463
+ const text = lines[line]
2464
+
2465
+ if (text === undefined) return mappings
2466
+
2467
+ let generated = 0
2468
+ let split = -1
2469
+ let position = 0
2470
+ const segments = text ? text.split(',') : []
2471
+
2472
+ for (const [index, segment] of segments.entries()) {
2473
+ const [delta] = Vlq.decode(segment)
2474
+
2475
+ generated += delta!
2476
+
2477
+ if (generated >= column) {
2478
+ split = index
2479
+ position = generated
2480
+ break
2481
+ }
2482
+ }
2483
+
2484
+ if (split === -1) {
2485
+ lines.splice(line + 1, 0, '', '')
2486
+ return lines.join(';')
2487
+ }
2488
+
2489
+ const moved = segments.slice(split)
2490
+ const fields = Vlq.decode(moved[0]!)
2491
+
2492
+ fields[0] = position - column
2493
+ moved[0] = Vlq.encode(fields)
2494
+ lines.splice(line, 1, segments.slice(0, split).join(','), '', moved.join(','))
2495
+
2496
+ return lines.join(';')
2497
+ }
2498
+
2499
+ /** Whether two import resolution records hold the same entries. */
2500
+ function same(
2501
+ left: Readonly<Record<string, string | null>> | undefined,
2502
+ right: Readonly<Record<string, string | null>>,
2503
+ ) {
2504
+ if (!left) return false
2505
+
2506
+ const keys = Object.keys(right)
2507
+ if (Object.keys(left).length !== keys.length) return false
2508
+
2509
+ return keys.every((key) => left[key] === right[key])
2510
+ }
2511
+
2512
+ /**
2513
+ * Whether `zyzz/runtime` resolves inside node_modules from the Vite root. Vite
2514
+ * optimizes a discovered import only there, so linked and aliased copies stay
2515
+ * source modules beside the other entrypoints.
2516
+ */
2517
+ function installed(config: UserConfig) {
2518
+ const aliases = config.resolve?.alias ?? {}
2519
+ const entries = Array.isArray(aliases)
2520
+ ? aliases
2521
+ : Object.entries(aliases).map(([find, replacement]) => ({
2522
+ find,
2523
+ replacement,
2524
+ }))
2525
+
2526
+ // Matches Vite's alias semantics: a string entry covers its subpaths.
2527
+ if (
2528
+ entries.some((entry) =>
2529
+ typeof entry.find === 'string'
2530
+ ? runtime === entry.find || runtime.startsWith(`${entry.find}/`)
2531
+ : entry.find.test(runtime),
2532
+ )
2533
+ )
2534
+ return false
2535
+
2536
+ try {
2537
+ const require = createRequire(
2538
+ Path.join(Path.resolve(config.root ?? ''), 'package.json'),
2539
+ )
2540
+
2541
+ // Vite keeps a symlinked package at its node_modules path, which Node dereferences.
2542
+ if (config.resolve?.preserveSymlinks)
2543
+ return (require.resolve.paths('zyzz') ?? []).some((directory) =>
2544
+ existsSync(Path.join(directory, 'zyzz/package.json')),
2545
+ )
2546
+
2547
+ return require.resolve(runtime).split(Path.sep).includes('node_modules')
2548
+ } catch {
2549
+ return false
2550
+ }
2551
+ }
2552
+
2553
+ // Vite forwards `scan` to `resolveId` hooks but leaves it out of its public types.
2554
+ type ScanOptions = NonNullable<
2555
+ Parameters<DevEnvironment['pluginContainer']['resolveId']>[2]
2556
+ > & { scan: boolean }
2557
+
2558
+ const scan: ScanOptions = { scan: true }
2559
+
2560
+ /**
2561
+ * Resolves imports read from source before other plugins transform their
2562
+ * importers. Marking them as scans, as Vite's dependency scanner does, keeps the
2563
+ * optimizer from discovering their dependencies and lets plugins tell them apart
2564
+ * from imports that reach the module graph.
2565
+ */
2566
+ function resolver(environment: DevEnvironment): Host['resolve'] {
2567
+ return (source, importer) =>
2568
+ environment.pluginContainer.resolveId(source, importer, scan)
2569
+ }
2570
+
2571
+ // Vite and Vitest load their own configuration modules, which application code never imports.
2572
+ const configFiles = [
2573
+ '**/vite.config.*',
2574
+ '**/vitest.config.*',
2575
+ '**/vitest.workspace.*',
2576
+ ]
1669
2577
  const prefix = '\0zyzz:'
2578
+ const runtime = 'zyzz/runtime'
1670
2579
  const sharedId = `${prefix}shared.css`
1671
2580
 
1672
2581
  function cssId(file: string) {
@@ -1679,6 +2588,13 @@ export declare namespace zyzz {
1679
2588
  type Options = {
1680
2589
  /** False retains authored calls and requires explicit identities where needed. */
1681
2590
  readonly compiler?: boolean | undefined
2591
+ /**
2592
+ * Source modules left out of discovery and transforms. Globs resolve against the
2593
+ * Vite root unless they start with `**`, and regular expressions test absolute paths.
2594
+ * Imports of excluded modules stay runtime imports. Vite and Vitest configuration
2595
+ * modules are always excluded.
2596
+ */
2597
+ readonly exclude?: readonly (RegExp | string)[] | undefined
1682
2598
  /** Additional source directories, absolute or relative to the Vite root. */
1683
2599
  readonly include?: readonly string[] | undefined
1684
2600
  /** Native context captured at creation. Disables CSS delivery and initialization scripts. */