zyzz 0.0.28 → 0.0.30

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 (302) 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 -1267
  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 -961
  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/Literal.d.ts +4 -1
  51. package/dist/internal/Literal.d.ts.map +1 -1
  52. package/dist/internal/Literal.js +4 -2
  53. package/dist/internal/Literal.js.map +1 -1
  54. package/dist/internal/Token.d.ts +2 -1
  55. package/dist/internal/Token.d.ts.map +1 -1
  56. package/dist/internal/Token.js +2 -2
  57. package/dist/internal/Token.js.map +1 -1
  58. package/dist/internal/Vlq.d.ts +14 -0
  59. package/dist/internal/Vlq.d.ts.map +1 -0
  60. package/dist/internal/Vlq.js +47 -0
  61. package/dist/internal/Vlq.js.map +1 -0
  62. package/dist/internal/Wasm.d.ts +22 -0
  63. package/dist/internal/Wasm.d.ts.map +1 -0
  64. package/dist/internal/Wasm.js +83 -0
  65. package/dist/internal/Wasm.js.map +1 -0
  66. package/dist/metro/Graph.d.ts +0 -1
  67. package/dist/metro/Graph.d.ts.map +1 -1
  68. package/dist/metro/transformer.d.ts.map +1 -1
  69. package/dist/metro/transformer.js +0 -1
  70. package/dist/metro/transformer.js.map +1 -1
  71. package/dist/next/internal/Project.d.ts.map +1 -1
  72. package/dist/next/internal/Project.js +2 -5
  73. package/dist/next/internal/Project.js.map +1 -1
  74. package/dist/node/Host.d.ts.map +1 -1
  75. package/dist/node/Host.js +0 -2
  76. package/dist/node/Host.js.map +1 -1
  77. package/dist/node/internal/Contracts.d.ts.map +1 -1
  78. package/dist/node/internal/Contracts.js +3 -7
  79. package/dist/node/internal/Contracts.js.map +1 -1
  80. package/dist/node/internal/Snapshot.d.ts +0 -1
  81. package/dist/node/internal/Snapshot.d.ts.map +1 -1
  82. package/dist/node/internal/Snapshot.js +0 -6
  83. package/dist/node/internal/Snapshot.js.map +1 -1
  84. package/dist/runtime/Composition.d.ts +2 -3
  85. package/dist/runtime/Composition.d.ts.map +1 -1
  86. package/dist/runtime/Composition.js +51 -30
  87. package/dist/runtime/Composition.js.map +1 -1
  88. package/dist/runtime/Recipe.d.ts.map +1 -1
  89. package/dist/runtime/Recipe.js +16 -12
  90. package/dist/runtime/Recipe.js.map +1 -1
  91. package/dist/unplugin/index.d.ts.map +1 -1
  92. package/dist/unplugin/index.js +3 -6
  93. package/dist/unplugin/index.js.map +1 -1
  94. package/dist/vite/index.d.ts.map +1 -1
  95. package/dist/vite/index.js +696 -201
  96. package/dist/vite/index.js.map +1 -1
  97. package/dist/web/Css.d.ts +8 -3
  98. package/dist/web/Css.d.ts.map +1 -1
  99. package/dist/web/Css.js +5 -721
  100. package/dist/web/Css.js.map +1 -1
  101. package/dist/web/internal/Block.d.ts +0 -2
  102. package/dist/web/internal/Block.d.ts.map +1 -1
  103. package/dist/web/internal/Block.js +1 -12
  104. package/dist/web/internal/Block.js.map +1 -1
  105. package/dist/web/internal/ClassName.d.ts +23 -18
  106. package/dist/web/internal/ClassName.d.ts.map +1 -1
  107. package/dist/web/internal/ClassName.js +26 -55
  108. package/dist/web/internal/ClassName.js.map +1 -1
  109. package/dist/web/internal/Contributions.d.ts +3 -5
  110. package/dist/web/internal/Contributions.d.ts.map +1 -1
  111. package/dist/web/internal/Contributions.js +0 -45
  112. package/dist/web/internal/Contributions.js.map +1 -1
  113. package/dist/web/internal/Engine.browser.d.ts +8 -0
  114. package/dist/web/internal/Engine.browser.d.ts.map +1 -0
  115. package/dist/web/internal/Engine.browser.js +42 -0
  116. package/dist/web/internal/Engine.browser.js.map +1 -0
  117. package/dist/web/internal/Engine.d.ts +9 -0
  118. package/dist/web/internal/Engine.d.ts.map +1 -0
  119. package/dist/web/internal/Engine.js +14 -0
  120. package/dist/web/internal/Engine.js.map +1 -0
  121. package/dist/web/internal/Themes.d.ts +1 -9
  122. package/dist/web/internal/Themes.d.ts.map +1 -1
  123. package/dist/web/internal/Themes.js +3 -221
  124. package/dist/web/internal/Themes.js.map +1 -1
  125. package/dist/web/internal/Wasm.generated.d.ts +4 -0
  126. package/dist/web/internal/Wasm.generated.d.ts.map +1 -0
  127. package/dist/web/internal/Wasm.generated.js +4 -0
  128. package/dist/web/internal/Wasm.generated.js.map +1 -0
  129. package/dist/web/internal/Wire.d.ts +19 -0
  130. package/dist/web/internal/Wire.d.ts.map +1 -0
  131. package/dist/web/internal/Wire.js +387 -0
  132. package/dist/web/internal/Wire.js.map +1 -0
  133. package/package.json +20 -1
  134. package/src/babel/index.ts +2 -5
  135. package/src/babel/internal/Compilation.ts +0 -1
  136. package/src/compiler/Graph.ts +89 -1930
  137. package/src/compiler/Native.ts +18 -871
  138. package/src/compiler/Source.ts +144 -1803
  139. package/src/compiler/Transform.ts +16 -1315
  140. package/src/compiler/internal/AtRules.ts +27 -115
  141. package/src/compiler/internal/Context.ts +11 -0
  142. package/src/compiler/internal/Results.ts +406 -0
  143. package/src/compiler/internal/Syntax.ts +0 -3
  144. package/src/compiler/internal/Wire.ts +1045 -0
  145. package/src/internal/Addon.ts +118 -0
  146. package/src/internal/Literal.ts +4 -2
  147. package/src/internal/Token.ts +2 -2
  148. package/src/internal/Vlq.ts +59 -0
  149. package/src/internal/Wasm.ts +111 -0
  150. package/src/metro/transformer.ts +0 -1
  151. package/src/next/internal/Project.ts +2 -9
  152. package/src/node/Host.ts +0 -2
  153. package/src/node/internal/Contracts.ts +3 -7
  154. package/src/node/internal/Snapshot.ts +0 -8
  155. package/src/runtime/Composition.ts +48 -25
  156. package/src/runtime/Recipe.ts +21 -14
  157. package/src/unplugin/index.ts +6 -6
  158. package/src/vite/index.ts +922 -221
  159. package/src/web/Css.ts +12 -908
  160. package/src/web/internal/Block.ts +0 -14
  161. package/src/web/internal/ClassName.ts +52 -85
  162. package/src/web/internal/Contributions.ts +3 -59
  163. package/src/web/internal/Engine.browser.ts +51 -0
  164. package/src/web/internal/Engine.ts +17 -0
  165. package/src/web/internal/Themes.ts +3 -279
  166. package/src/web/internal/Wasm.generated.ts +3 -0
  167. package/src/web/internal/Wire.ts +466 -0
  168. package/src/web/internal/wasm.d.ts +9 -0
  169. package/dist/compiler/internal/Applications.d.ts +0 -55
  170. package/dist/compiler/internal/Applications.d.ts.map +0 -1
  171. package/dist/compiler/internal/Applications.js +0 -393
  172. package/dist/compiler/internal/Applications.js.map +0 -1
  173. package/dist/compiler/internal/ColorProfiles.d.ts +0 -3
  174. package/dist/compiler/internal/ColorProfiles.d.ts.map +0 -1
  175. package/dist/compiler/internal/ColorProfiles.js +0 -84
  176. package/dist/compiler/internal/ColorProfiles.js.map +0 -1
  177. package/dist/compiler/internal/CompositionBindings.d.ts +0 -16
  178. package/dist/compiler/internal/CompositionBindings.d.ts.map +0 -1
  179. package/dist/compiler/internal/CompositionBindings.js +0 -89
  180. package/dist/compiler/internal/CompositionBindings.js.map +0 -1
  181. package/dist/compiler/internal/Compositions.d.ts +0 -37
  182. package/dist/compiler/internal/Compositions.d.ts.map +0 -1
  183. package/dist/compiler/internal/Compositions.js +0 -494
  184. package/dist/compiler/internal/Compositions.js.map +0 -1
  185. package/dist/compiler/internal/Configurations.d.ts +0 -26
  186. package/dist/compiler/internal/Configurations.d.ts.map +0 -1
  187. package/dist/compiler/internal/Configurations.js +0 -259
  188. package/dist/compiler/internal/Configurations.js.map +0 -1
  189. package/dist/compiler/internal/Contract.d.ts +0 -30
  190. package/dist/compiler/internal/Contract.d.ts.map +0 -1
  191. package/dist/compiler/internal/Contract.js +0 -753
  192. package/dist/compiler/internal/Contract.js.map +0 -1
  193. package/dist/compiler/internal/Contributions.d.ts +0 -54
  194. package/dist/compiler/internal/Contributions.d.ts.map +0 -1
  195. package/dist/compiler/internal/Contributions.js +0 -1151
  196. package/dist/compiler/internal/Contributions.js.map +0 -1
  197. package/dist/compiler/internal/Descriptors.d.ts +0 -15
  198. package/dist/compiler/internal/Descriptors.d.ts.map +0 -1
  199. package/dist/compiler/internal/Descriptors.js +0 -192
  200. package/dist/compiler/internal/Descriptors.js.map +0 -1
  201. package/dist/compiler/internal/Dynamic.d.ts +0 -14
  202. package/dist/compiler/internal/Dynamic.d.ts.map +0 -1
  203. package/dist/compiler/internal/Dynamic.js +0 -175
  204. package/dist/compiler/internal/Dynamic.js.map +0 -1
  205. package/dist/compiler/internal/Expression.d.ts +0 -19
  206. package/dist/compiler/internal/Expression.d.ts.map +0 -1
  207. package/dist/compiler/internal/Expression.js +0 -164
  208. package/dist/compiler/internal/Expression.js.map +0 -1
  209. package/dist/compiler/internal/Functions.d.ts +0 -5
  210. package/dist/compiler/internal/Functions.d.ts.map +0 -1
  211. package/dist/compiler/internal/Functions.js +0 -191
  212. package/dist/compiler/internal/Functions.js.map +0 -1
  213. package/dist/compiler/internal/Keyframes.d.ts +0 -5
  214. package/dist/compiler/internal/Keyframes.d.ts.map +0 -1
  215. package/dist/compiler/internal/Keyframes.js +0 -51
  216. package/dist/compiler/internal/Keyframes.js.map +0 -1
  217. package/dist/compiler/internal/Names.d.ts +0 -5
  218. package/dist/compiler/internal/Names.d.ts.map +0 -1
  219. package/dist/compiler/internal/Names.js +0 -37
  220. package/dist/compiler/internal/Names.js.map +0 -1
  221. package/dist/compiler/internal/NativeBindings.d.ts +0 -72
  222. package/dist/compiler/internal/NativeBindings.d.ts.map +0 -1
  223. package/dist/compiler/internal/NativeBindings.js +0 -171
  224. package/dist/compiler/internal/NativeBindings.js.map +0 -1
  225. package/dist/compiler/internal/NativeMedia.d.ts +0 -14
  226. package/dist/compiler/internal/NativeMedia.d.ts.map +0 -1
  227. package/dist/compiler/internal/NativeMedia.js +0 -287
  228. package/dist/compiler/internal/NativeMedia.js.map +0 -1
  229. package/dist/compiler/internal/NativeVars.d.ts +0 -28
  230. package/dist/compiler/internal/NativeVars.d.ts.map +0 -1
  231. package/dist/compiler/internal/NativeVars.js +0 -160
  232. package/dist/compiler/internal/NativeVars.js.map +0 -1
  233. package/dist/compiler/internal/PackedStyles.d.ts +0 -37
  234. package/dist/compiler/internal/PackedStyles.d.ts.map +0 -1
  235. package/dist/compiler/internal/PackedStyles.js +0 -344
  236. package/dist/compiler/internal/PackedStyles.js.map +0 -1
  237. package/dist/compiler/internal/Properties.d.ts +0 -5
  238. package/dist/compiler/internal/Properties.d.ts.map +0 -1
  239. package/dist/compiler/internal/Properties.js +0 -82
  240. package/dist/compiler/internal/Properties.js.map +0 -1
  241. package/dist/compiler/internal/RecipeConditions.d.ts +0 -41
  242. package/dist/compiler/internal/RecipeConditions.d.ts.map +0 -1
  243. package/dist/compiler/internal/RecipeConditions.js +0 -82
  244. package/dist/compiler/internal/RecipeConditions.js.map +0 -1
  245. package/dist/compiler/internal/RecipePayloads.d.ts +0 -31
  246. package/dist/compiler/internal/RecipePayloads.d.ts.map +0 -1
  247. package/dist/compiler/internal/RecipePayloads.js +0 -99
  248. package/dist/compiler/internal/RecipePayloads.js.map +0 -1
  249. package/dist/compiler/internal/Recipes.d.ts +0 -32
  250. package/dist/compiler/internal/Recipes.d.ts.map +0 -1
  251. package/dist/compiler/internal/Recipes.js +0 -339
  252. package/dist/compiler/internal/Recipes.js.map +0 -1
  253. package/dist/compiler/internal/Selectors.d.ts +0 -13
  254. package/dist/compiler/internal/Selectors.d.ts.map +0 -1
  255. package/dist/compiler/internal/Selectors.js +0 -280
  256. package/dist/compiler/internal/Selectors.js.map +0 -1
  257. package/dist/compiler/internal/Static.d.ts +0 -39
  258. package/dist/compiler/internal/Static.d.ts.map +0 -1
  259. package/dist/compiler/internal/Static.js +0 -534
  260. package/dist/compiler/internal/Static.js.map +0 -1
  261. package/dist/compiler/internal/Themes.d.ts +0 -190
  262. package/dist/compiler/internal/Themes.d.ts.map +0 -1
  263. package/dist/compiler/internal/Themes.js +0 -1577
  264. package/dist/compiler/internal/Themes.js.map +0 -1
  265. package/dist/compiler/internal/Vars.d.ts +0 -33
  266. package/dist/compiler/internal/Vars.d.ts.map +0 -1
  267. package/dist/compiler/internal/Vars.js +0 -387
  268. package/dist/compiler/internal/Vars.js.map +0 -1
  269. package/dist/compiler/internal/ViewTransitions.d.ts +0 -3
  270. package/dist/compiler/internal/ViewTransitions.d.ts.map +0 -1
  271. package/dist/compiler/internal/ViewTransitions.js +0 -68
  272. package/dist/compiler/internal/ViewTransitions.js.map +0 -1
  273. package/dist/internal/Cascade.d.ts +0 -74
  274. package/dist/internal/Cascade.d.ts.map +0 -1
  275. package/dist/internal/Cascade.js +0 -210
  276. package/dist/internal/Cascade.js.map +0 -1
  277. package/src/compiler/internal/Applications.ts +0 -610
  278. package/src/compiler/internal/ColorProfiles.ts +0 -93
  279. package/src/compiler/internal/CompositionBindings.ts +0 -120
  280. package/src/compiler/internal/Compositions.ts +0 -678
  281. package/src/compiler/internal/Configurations.ts +0 -349
  282. package/src/compiler/internal/Contract.ts +0 -1028
  283. package/src/compiler/internal/Contributions.ts +0 -1561
  284. package/src/compiler/internal/Descriptors.ts +0 -242
  285. package/src/compiler/internal/Dynamic.ts +0 -275
  286. package/src/compiler/internal/Expression.ts +0 -218
  287. package/src/compiler/internal/Functions.ts +0 -206
  288. package/src/compiler/internal/Keyframes.ts +0 -62
  289. package/src/compiler/internal/Names.ts +0 -48
  290. package/src/compiler/internal/NativeBindings.ts +0 -235
  291. package/src/compiler/internal/NativeMedia.ts +0 -342
  292. package/src/compiler/internal/NativeVars.ts +0 -231
  293. package/src/compiler/internal/PackedStyles.ts +0 -473
  294. package/src/compiler/internal/Properties.ts +0 -121
  295. package/src/compiler/internal/RecipeConditions.ts +0 -139
  296. package/src/compiler/internal/RecipePayloads.ts +0 -132
  297. package/src/compiler/internal/Recipes.ts +0 -531
  298. package/src/compiler/internal/Selectors.ts +0 -353
  299. package/src/compiler/internal/Static.ts +0 -784
  300. package/src/compiler/internal/Themes.ts +0 -2398
  301. package/src/compiler/internal/Vars.ts +0 -516
  302. package/src/compiler/internal/ViewTransitions.ts +0 -82
package/src/vite/index.ts CHANGED
@@ -5,6 +5,7 @@
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'
@@ -13,15 +14,16 @@ import { existsSync } from 'node:fs'
13
14
  import * as Fs from 'node:fs/promises'
14
15
  import * as Path from 'node:path'
15
16
  import * as Reset from '../node/Reset.js'
17
+ import * as Vlq from '../internal/Vlq.js'
16
18
  import { createRequire, isBuiltin } from 'node:module'
17
19
  import * as Parser from 'oxc-parser'
18
20
  import * as Walker from 'oxc-walker'
19
21
  import * as Snapshot from '../node/internal/Snapshot.js'
20
- import * as Syntax from '../compiler/internal/Syntax.js'
21
22
  import * as Scope from '../compiler/internal/Scope.js'
22
23
  import type {
23
24
  DevEnvironment,
24
25
  Environment,
26
+ EnvironmentModuleNode,
25
27
  Plugin,
26
28
  Rollup,
27
29
  UserConfig,
@@ -61,6 +63,33 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
61
63
  const reads = new WeakMap<Environment, Map<string, string>>()
62
64
  const lookups = new WeakMap<Environment, Lookups>()
63
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
+ >()
64
93
  const packages = Contracts.create()
65
94
 
66
95
  function compiler(environment: Environment) {
@@ -71,6 +100,168 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
71
100
  }
72
101
  return value
73
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
+ }
74
265
  const discoveries = new WeakMap<Environment, Promise<Map<string, string>>>()
75
266
  const contributionFiles = new WeakMap<Environment, Set<string>>()
76
267
  const assets = new Map<string, string>()
@@ -81,6 +272,8 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
81
272
  const sourceEntrypoints = new Set<string>()
82
273
  const identities = new Map<string, string>()
83
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>()
84
277
  const lazyImports = new WeakMap<Parser.ParseResult, string[]>()
85
278
  let directories: readonly string[]
86
279
  let filter: (id: unknown) => boolean
@@ -182,7 +375,9 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
182
375
  }
183
376
 
184
377
  function sourceRoot(file: string) {
185
- return directories.find((directory) => {
378
+ if (roots.has(file)) return roots.get(file)
379
+
380
+ const root = directories.find((directory) => {
186
381
  const relative = Path.relative(directory, file)
187
382
 
188
383
  return (
@@ -192,6 +387,9 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
192
387
  !relative.split(Path.sep).includes('node_modules')
193
388
  )
194
389
  })
390
+
391
+ roots.set(file, root)
392
+ return root
195
393
  }
196
394
 
197
395
  function discover(environment: Environment, host: Host) {
@@ -383,7 +581,46 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
383
581
  event: string,
384
582
  ) {
385
583
  reads.delete(environment)
386
- lookups.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
+
387
624
  if (event === 'delete') {
388
625
  if (entries(environment).has(file)) {
389
626
  let files = removed.get(environment)
@@ -421,10 +658,66 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
421
658
  allSources = false,
422
659
  everything = false,
423
660
  ) {
424
- // Every compile visits the discovered project, so modules share lookups
425
- // until the watcher reports a change. Full-project compiles look up afresh.
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.
426
715
  const cache = (() => {
427
- const fresh: Lookups = { contracts: new Map(), resolutions: new Map() }
716
+ const fresh: Lookups = {
717
+ contracts: new Map(),
718
+ read: new Map(),
719
+ resolutions: new Map(),
720
+ }
428
721
  if (everything) return fresh
429
722
 
430
723
  let value = lookups.get(entry.environment)
@@ -435,6 +728,9 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
435
728
  return value
436
729
  })()
437
730
 
731
+ // Full-project compiles walk afresh, as they look up afresh.
732
+ const memos = everything ? undefined : importEdges(entry.environment)
733
+
438
734
  async function resolve(source: string, importer: string) {
439
735
  if (isBuiltin(source)) return { id: source, external: true }
440
736
 
@@ -450,16 +746,39 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
450
746
  if (!resolved) return resolved
451
747
 
452
748
  if (entry.environment.mode === 'dev') {
453
- const optimized = Object.values({
454
- ...entry.environment.depsOptimizer?.metadata.optimized,
455
- ...entry.environment.depsOptimizer?.metadata.discovered,
456
- }).find((item) => item.file === resolved.id.split('?')[0])
457
- if (optimized?.src) return { ...resolved, id: optimized.src }
749
+ const source = optimized().get(resolved.id.split('?')[0]!)
750
+ if (source) return { ...resolved, id: source }
458
751
  }
459
752
 
460
753
  return { ...resolved, id: normalize(resolved.id) }
461
754
  }
462
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
+
463
782
  const imports: Record<
464
783
  string,
465
784
  Record<string, string | null>
@@ -488,7 +807,10 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
488
807
  resolve: async (specifier, importer) =>
489
808
  (await resolve(specifier, importer))?.id.split(/[?#]/)[0],
490
809
  })
491
- .then((contract) => ({ contract, files: read }))
810
+ .then((contract) => {
811
+ cache.read.set(key, read)
812
+ return { contract, files: read }
813
+ })
492
814
  cache.contracts.set(key, pending)
493
815
  pending.catch(() => cache.contracts.delete(key))
494
816
  }
@@ -517,16 +839,43 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
517
839
  cached = new Map()
518
840
  reads.set(entry.environment, cached)
519
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.
520
845
  // Full-project compilation must not reuse reads from concurrent module transforms.
521
846
  const text =
522
847
  source ??
848
+ (everything
849
+ ? undefined
850
+ : entries(entry.environment).get(file)?.source) ??
523
851
  (everything ? undefined : cached.get(file)) ??
524
852
  (await snapshot(entry.environment).read(file))
525
853
  if (!everything) cached.set(file, text)
526
854
 
527
855
  modules[id] = text
528
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
+
529
877
  const resolutions: Record<string, string | null> = Object.create(null)
878
+ const steps: Edges['steps'][number][] = []
530
879
 
531
880
  imports[id] = resolutions
532
881
 
@@ -597,6 +946,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
597
946
  ) {
598
947
  sourceEntrypoints.add(normalize(resolved.id))
599
948
  resolutions[specifier] = sourceId(resolved.id)
949
+ steps.push(resolved.id)
600
950
  await visit(resolved.id)
601
951
  continue
602
952
  }
@@ -609,10 +959,12 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
609
959
 
610
960
  if (resolved.external || !eligible(resolved.id)) {
611
961
  const physical = resolved.id.split('?')[0]!.split('#')[0]!
612
- const contract =
962
+ const readable =
613
963
  Path.isAbsolute(physical) && /\.[cm]?[jt]sx?$/.test(physical)
614
- ? await readContract(physical)
615
- : undefined
964
+
965
+ if (readable) steps.push({ id: resolved.id, physical })
966
+
967
+ const contract = readable ? await readContract(physical) : undefined
616
968
 
617
969
  if (contract !== undefined) {
618
970
  contracts[resolved.id] = contract
@@ -625,6 +977,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
625
977
  }
626
978
 
627
979
  resolutions[specifier] = sourceId(resolved.id)
980
+ steps.push(resolved.id)
628
981
  await visit(resolved.id)
629
982
  }
630
983
 
@@ -658,6 +1011,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
658
1011
  }
659
1012
 
660
1013
  lazy[id] = targets
1014
+ memos?.set(file, { lazy: targets, resolutions, steps, text })
661
1015
  }
662
1016
 
663
1017
  await visit(entry.file, code)
@@ -774,22 +1128,10 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
774
1128
 
775
1129
  loaded.add(id)
776
1130
 
777
- const metadata = (() => {
778
- try {
779
- return JSON.parse(contracts[id]!)
780
- } catch (error) {
781
- Graph.compile({ modules: {}, contracts: { [id]: contracts[id]! } })
782
- throw error
783
- }
784
- })()
785
- if (!Array.isArray(metadata?.stylesheets)) return
786
-
787
- for (const section of metadata.stylesheets) {
788
- if (!Array.isArray(section?.dependency)) continue
789
-
1131
+ for (const dependency of stylesheetDependencies(id, contracts[id]!)) {
790
1132
  let owner = id
791
1133
 
792
- for (const specifier of section.dependency) {
1134
+ for (const specifier of dependency) {
793
1135
  if (
794
1136
  typeof specifier !== 'string' ||
795
1137
  !specifier ||
@@ -837,9 +1179,9 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
837
1179
  ]),
838
1180
  )
839
1181
 
840
- const result = entry.compiler.compile({
1182
+ const input: Graph.compile.Options = {
1183
+ [Results.lean]: true,
841
1184
  [ThemeRules.shared]: native ? undefined : 'all',
842
- [Syntax.cache]: snapshot(entry.environment).programs(modules),
843
1185
  compiler: options.compiler,
844
1186
  reset: options.reset ? Reset.read() : undefined,
845
1187
  native,
@@ -847,7 +1189,11 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
847
1189
  development: entry.environment.mode !== 'build',
848
1190
  imports,
849
1191
  modules,
850
- })
1192
+ }
1193
+ const result =
1194
+ entry.environment.mode === 'dev' && !native && !everything
1195
+ ? compileProject(entry.environment, input, sourceId(entry.file))
1196
+ : entry.compiler.compile(input)
851
1197
 
852
1198
  if (native) {
853
1199
  const output = result.modules[sourceId(entry.file)]!
@@ -861,6 +1207,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
861
1207
  configurations: new Map<string, Catalogs.Catalog>(),
862
1208
  connections: staged,
863
1209
  css: '',
1210
+ graphSharedCss: '',
864
1211
  sharedCss: '',
865
1212
  cssMap: map,
866
1213
  sharedCssMap: map,
@@ -896,166 +1243,181 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
896
1243
 
897
1244
  const configurations = new Map<string, Catalogs.Catalog>()
898
1245
  for (const [id, contract] of Object.entries(compiled))
899
- for (const configuration of Catalogs.read(contract)) {
1246
+ for (const configuration of read(contract)) {
900
1247
  const key = `${id}\0${configuration.identity}`
901
1248
  catalogs.set(key, configuration)
902
1249
  configurations.set(key, configuration)
903
1250
  }
904
1251
 
905
- 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>()
906
1256
 
907
- for (const id of new Set(Object.values(result.sharedAssetOwners ?? {}))) {
908
- if (!Path.isAbsolute(id)) continue
1257
+ for (const id of new Set(Object.values(result.sharedAssetOwners ?? {}))) {
1258
+ if (!Path.isAbsolute(id)) continue
909
1259
 
910
- let directory = Path.dirname(id.split(/[?#]/)[0]!)
1260
+ let directory = Path.dirname(id.split(/[?#]/)[0]!)
911
1261
 
912
- for (;;) {
913
- try {
914
- await Fs.access(Path.join(directory, 'package.json'))
915
- owners.set(id, await Fs.realpath(directory))
916
- break
917
- } catch (error) {
918
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
919
- }
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
+ }
920
1270
 
921
- const parent = Path.dirname(directory)
922
- if (parent === directory)
923
- 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.')
924
1274
 
925
- directory = parent
1275
+ directory = parent
1276
+ }
926
1277
  }
927
- }
928
1278
 
929
- const assetUrls = new Map<string, string>()
1279
+ const assetUrls = new Map<string, string>()
930
1280
 
931
- for (const [key, target] of Object.entries(result.sharedAssets ?? {})) {
932
- const raw = target.split(/[?#]/)[0]!
1281
+ for (const [key, target] of Object.entries(result.sharedAssets ?? {})) {
1282
+ const raw = target.split(/[?#]/)[0]!
933
1283
 
934
- const identity = result.sharedAssetOwners?.[key]
935
- const path =
936
- sourceFile(decodeURIComponent(raw)) ??
937
- (Path.isAbsolute(raw) ? decodeURIComponent(raw) : undefined)
938
- 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.')
939
1289
 
940
- const asset = await (async () => {
941
- try {
942
- return { file: await Fs.realpath(path), package: false }
943
- } catch (error) {
944
- if (
945
- (error as NodeJS.ErrnoException).code !== 'ENOENT' ||
946
- !identity ||
947
- !sourceFile(identity)
948
- )
949
- throw error
950
-
951
- const importer = sourceFile(identity)!
952
- const specifier = Path.relative(Path.dirname(importer), path)
953
- .split(Path.sep)
954
- .join('/')
955
- if (specifier.startsWith('.') || Path.isAbsolute(specifier))
956
- throw error
957
- const resolved = await host.resolve(specifier, importer)
958
- if (!resolved || resolved.external) throw error
959
- return {
960
- file: await Fs.realpath(resolved.id.split('?')[0]!),
961
- 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
+ }
962
1313
  }
963
- }
964
- })()
965
- const file = asset.file
966
-
967
- const owner =
968
- identity && sourceFile(identity)
969
- ? sourceRoot(sourceFile(identity)!)
970
- : identity
971
- ? owners.get(identity)
972
- : undefined
973
-
974
- const relative = owner ? Path.relative(owner, file) : '..'
975
- if (
976
- !asset.package &&
977
- (relative === '..' ||
978
- relative.startsWith(`..${Path.sep}`) ||
979
- Path.isAbsolute(relative))
980
- )
981
- throw new Error('Asset path escapes its owning package.')
1314
+ })()
1315
+ const file = asset.file
982
1316
 
983
- host.watch(file)
984
- files.add(file)
1317
+ const owner =
1318
+ identity && sourceFile(identity)
1319
+ ? sourceRoot(sourceFile(identity)!)
1320
+ : identity
1321
+ ? owners.get(identity)
1322
+ : undefined
985
1323
 
986
- 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.')
987
1332
 
988
- if (asset.package || /[?#%]/.test(file)) {
989
- if (entry.environment.mode === 'build') {
990
- url = await host.asset(file)
1333
+ host.watch(file)
1334
+ files.add(file)
991
1335
 
992
- 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
+ }
993
1349
  } else {
994
- url = `/@zyzz/asset/${Crypto.hash('sha256', file)}/${encodeURIComponent(Path.basename(file))}`
995
- assets.set(url, file)
996
- 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)}`
997
1355
  }
998
- } else {
999
- // Vite reads `/@fs/` URLs with `decodeURI`, which keeps reserved escapes
1000
- // such as `%40` in scoped package paths, so those characters stay raw.
1001
- const pathname = encodeURI(file.replaceAll('\\', '/'))
1002
1356
 
1003
- url = `/@fs/${pathname}${target.slice(raw.length)}`
1357
+ assetUrls.set(target, url)
1004
1358
  }
1005
1359
 
1006
- assetUrls.set(target, url)
1007
- }
1008
-
1009
- const shared = !Object.keys(result.sharedAssets ?? {}).length
1010
- ? {
1011
- code: new TextEncoder().encode(result.sharedCss ?? ''),
1012
- map: new TextEncoder().encode(
1013
- 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(
1014
1375
  result.sharedCssMap ??
1015
1376
  Mapping.toEncodedMap(new Mapping.GenMapping()),
1016
1377
  ),
1017
- ),
1018
- }
1019
- : AtRules.transform({
1020
- filename: 'zyzz.shared.css',
1021
- code: new TextEncoder().encode(result.sharedCss ?? ''),
1022
- sourceMap: true,
1023
- inputSourceMap: JSON.stringify(
1024
- result.sharedCssMap ??
1025
- Mapping.toEncodedMap(new Mapping.GenMapping()),
1026
- ),
1027
- visitor: {
1028
- Rule(rule) {
1029
- if (rule.type !== 'import') return
1030
- const target = result.sharedAssets?.[rule.value.url]
1031
- if (target)
1032
- return AtRules.relocateImport(
1033
- rule.value,
1034
- assetUrls.get(target)!,
1035
- )
1036
- },
1037
- Url(url) {
1038
- const target = result.sharedAssets?.[url.url]
1039
- 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
1040
1391
 
1041
- return { ...url, url: assetUrls.get(target)! }
1392
+ return { ...url, url: assetUrls.get(target)! }
1393
+ },
1042
1394
  },
1043
- },
1044
- })
1395
+ })
1045
1396
 
1046
- const sharedMap = JSON.parse(
1047
- new TextDecoder().decode(shared.map!),
1048
- ) as Mapping.EncodedSourceMap
1397
+ const sharedMap = JSON.parse(
1398
+ new TextDecoder().decode(shared.map!),
1399
+ ) as Mapping.EncodedSourceMap
1049
1400
 
1050
- sharedMap.sources = sharedMap.sources.map((source) =>
1051
- source === null ? null : (sourceFile(source) ?? source),
1052
- )
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
+ }
1053
1415
 
1054
1416
  const output = result.modules[sourceId(entry.file)]!
1055
1417
 
1056
1418
  entry.files = files
1057
1419
 
1058
- return {
1420
+ const value = {
1059
1421
  imports,
1060
1422
  modules: result.modules,
1061
1423
  code: output.code,
@@ -1063,8 +1425,10 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1063
1425
  configurations,
1064
1426
  connections: staged,
1065
1427
  css: output.css,
1066
- sharedCss: new TextDecoder().decode(shared.code),
1067
- 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,
1068
1432
  cssMap: {
1069
1433
  ...output.cssMap,
1070
1434
  sources: output.cssMap.sources.map((source) =>
@@ -1078,17 +1442,18 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1078
1442
  ),
1079
1443
  },
1080
1444
  }
1445
+
1446
+ return value
1081
1447
  }
1082
1448
 
1083
1449
  /** Compiles every discovered source once so index.html can inline each configuration's script before modules load. */
1084
1450
  async function initializations(server: ViteDevServer) {
1085
1451
  const environment = server.environments.client
1086
1452
  reads.delete(environment)
1087
- lookups.delete(environment)
1088
1453
  const host: Host = {
1089
1454
  asset: async (file) => file,
1090
1455
  resolve: resolver(environment),
1091
- watch: (file) => server.watcher.add(file),
1456
+ watch: (file) => watch(server, file),
1092
1457
  }
1093
1458
  const sources = await discover(environment, host)
1094
1459
  let entry = initializers.get(environment)
@@ -1098,8 +1463,9 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1098
1463
  const file = sources.keys().next().value
1099
1464
  if (file === undefined) return { catalogs: new Map(), edges: new Map() }
1100
1465
 
1466
+ // Whole-project compiles keep their own cache beside the transforms' merged graph.
1101
1467
  entry = {
1102
- compiler: compiler(environment),
1468
+ compiler: initializers.get(environment)?.compiler ?? Graph.create(),
1103
1469
  environment,
1104
1470
  file,
1105
1471
  files: new Set([file]),
@@ -1275,8 +1641,153 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1275
1641
  .map(([, configuration]) => configuration)
1276
1642
  }
1277
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
+
1278
1788
  return {
1279
1789
  buildStart() {
1790
+ edgeMemos.delete(this.environment)
1280
1791
  reads.delete(this.environment)
1281
1792
  lookups.delete(this.environment)
1282
1793
  },
@@ -1344,6 +1855,8 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1344
1855
  },
1345
1856
  },
1346
1857
  configureServer(server) {
1858
+ devServer = server
1859
+
1347
1860
  // Directory watches belong to the server, not module import dependencies.
1348
1861
  server.watcher.add(directories.slice(1))
1349
1862
 
@@ -1386,6 +1899,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1386
1899
 
1387
1900
  filter = createFilter(null, patterns, { resolve: root })
1388
1901
  exclusions.clear()
1902
+ roots.clear()
1389
1903
 
1390
1904
  if (native) return
1391
1905
  const lightning = (config.css.lightningcss ??= {})
@@ -1478,10 +1992,65 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1478
1992
  },
1479
1993
  },
1480
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
+
1481
1998
  await updateDiscovery(this.environment, file, change.event)
1482
1999
  },
1483
2000
  async hotUpdate({ file, modules, timestamp, type, server }) {
1484
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
+
1485
2054
  const previous = [...state.values()].map((entry) => ({
1486
2055
  code: entry.code,
1487
2056
  file: entry.file,
@@ -1501,9 +2070,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1501
2070
  first,
1502
2071
  {
1503
2072
  resolve: resolver(this.environment),
1504
- watch: (file) => {
1505
- server.watcher.add(file)
1506
- },
2073
+ watch: (file) => watch(server, file),
1507
2074
  asset: async (file) => file,
1508
2075
  },
1509
2076
  undefined,
@@ -1583,12 +2150,39 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1583
2150
  id === sharedId
1584
2151
  ? undefined
1585
2152
  : decodeURIComponent(id.slice(prefix.length, -4))
1586
- const entry =
2153
+ let entry =
1587
2154
  file === undefined
1588
2155
  ? entries(this.environment).values().next().value
1589
2156
  : entries(this.environment).get(file)
1590
2157
  // A pruned JavaScript module can leave its stylesheet loaded in the browser.
1591
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
+
1592
2186
  if (!entry) throw new Error(`Unknown Zyzz stylesheet: ${file}`)
1593
2187
 
1594
2188
  const output = await compile(
@@ -1598,7 +2192,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1598
2192
  this.environment.mode === 'dev'
1599
2193
  ? resolver(this.environment)
1600
2194
  : (source, importer) => this.resolve(source, importer),
1601
- watch: (file) => this.addWatchFile(file),
2195
+ watch: watcher(this),
1602
2196
  asset: async (file) => {
1603
2197
  const reference = this.emitFile({
1604
2198
  type: 'asset',
@@ -1626,12 +2220,19 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1626
2220
  '-zyzz-fpv-000000000',
1627
2221
  )
1628
2222
  : css
1629
- if (id === sharedId)
2223
+ if (id === sharedId) {
2224
+ servedShared.set(this.environment, output.graphSharedCss)
1630
2225
  return {
1631
2226
  code: transport(output.sharedCss),
1632
2227
  map: JSON.stringify(output.sharedCssMap),
1633
2228
  }
1634
- 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
+ }
1635
2236
  },
1636
2237
  name: 'zyzz',
1637
2238
  resolveId(id) {
@@ -1656,6 +2257,8 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1656
2257
  removed.get(this.environment)?.delete(id)
1657
2258
  }
1658
2259
 
2260
+ entry.source = code
2261
+
1659
2262
  const output = await compile(
1660
2263
  entry,
1661
2264
  {
@@ -1663,7 +2266,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1663
2266
  this.environment.mode === 'dev'
1664
2267
  ? resolver(this.environment)
1665
2268
  : (source, importer) => this.resolve(source, importer),
1666
- watch: (file) => this.addWatchFile(file),
2269
+ watch: watcher(this),
1667
2270
  asset: async (file) => {
1668
2271
  const reference = this.emitFile({
1669
2272
  type: 'asset',
@@ -1685,64 +2288,29 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1685
2288
 
1686
2289
  // Keep the CSS dependency even when the current graph has no live rules.
1687
2290
  // Later edits can introduce styles without changing this import boundary.
1688
- const parsed = Parser.parseSync('source.tsx', output.code, {
1689
- sourceType: 'module',
1690
- })
1691
- let offset = output.code.startsWith('#!')
1692
- ? output.code.indexOf('\n') + 1
1693
- : 0
1694
-
1695
- for (const node of parsed.program.body) {
1696
- if (node.type !== 'ExpressionStatement' || !node.directive) break
1697
-
1698
- offset = node.end
1699
- }
1700
-
2291
+ const offset = prologue(output.code)
1701
2292
  const sharedImport = `\nimport ${JSON.stringify(sharedId)};\n`
1702
- const map = Mapping.fromMap(JSON.stringify(output.map))
1703
2293
  // The inserted dependency shifts only generated positions after the prologue.
1704
2294
  const before = output.code.slice(0, offset)
1705
2295
  const insertionLine = before.split('\n').length
1706
2296
  const insertionColumn = before.length - (before.lastIndexOf('\n') + 1)
1707
- const shifted = new Mapping.GenMapping()
1708
-
1709
- for (const mapping of Mapping.allMappings(map)) {
1710
- const generated = { ...mapping.generated }
1711
-
1712
- if (
1713
- generated.line > insertionLine ||
1714
- (generated.line === insertionLine &&
1715
- generated.column >= insertionColumn)
1716
- ) {
1717
- if (generated.line === insertionLine)
1718
- generated.column -= insertionColumn
1719
-
1720
- generated.line += 2
1721
- }
1722
-
1723
- if (mapping.source !== undefined && mapping.original !== undefined) {
1724
- const location = {
1725
- generated,
1726
- original: mapping.original,
1727
- source: mapping.source,
1728
- }
1729
-
1730
- if (mapping.name === undefined) Mapping.addMapping(shifted, location)
1731
- else Mapping.addMapping(shifted, { ...location, name: mapping.name })
1732
- } else Mapping.addMapping(shifted, { generated })
1733
- }
1734
-
1735
- for (const [index, source] of output.map.sources.entries())
1736
- if (source !== null)
1737
- Mapping.setSourceContent(
1738
- shifted,
1739
- source,
1740
- output.map.sourcesContent?.[index] ?? null,
1741
- )
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))};`
1742
2303
 
1743
2304
  return {
1744
- code: `${before}${sharedImport}${output.code.slice(offset)}\nimport ${JSON.stringify(cssId(id))};`,
1745
- 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
+ }),
1746
2314
  }
1747
2315
  },
1748
2316
  transformIndexHtml: {
@@ -1779,13 +2347,25 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1779
2347
  }
1780
2348
  }
1781
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
+
1782
2358
  type Entry = {
1783
2359
  code?: string
1784
- imports?: string
1785
- environment: Environment
1786
2360
  compiler: Graph.create.ReturnType
2361
+ /** The stylesheet Vite last loaded for this module. */
2362
+ css?: string | undefined
2363
+ environment: Environment
1787
2364
  file: string
1788
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
1789
2369
  }
1790
2370
 
1791
2371
  type Host = {
@@ -1805,9 +2385,130 @@ type Lookups = {
1805
2385
  string,
1806
2386
  Promise<{ contract: string | undefined; files: ReadonlySet<string> }>
1807
2387
  >
2388
+ /** Files each settled contract read, so an edit to one drops only its contracts. */
2389
+ read: Map<string, ReadonlySet<string>>
1808
2390
  resolutions: Map<string, ReturnType<Host['resolve']>>
1809
2391
  }
1810
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
+
1811
2512
  /**
1812
2513
  * Whether `zyzz/runtime` resolves inside node_modules from the Vite root. Vite
1813
2514
  * optimizes a discovered import only there, so linked and aliased copies stay