zyzz 0.0.19 → 0.0.21

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 (167) hide show
  1. package/README.md +9 -9
  2. package/dist/Config.d.ts +1 -0
  3. package/dist/Config.d.ts.map +1 -1
  4. package/dist/Config.js.map +1 -1
  5. package/dist/Style.d.ts +7 -0
  6. package/dist/Style.d.ts.map +1 -1
  7. package/dist/Style.js +1 -5
  8. package/dist/Style.js.map +1 -1
  9. package/dist/Vars.d.ts +22 -15
  10. package/dist/Vars.d.ts.map +1 -1
  11. package/dist/Vars.js +26 -0
  12. package/dist/Vars.js.map +1 -1
  13. package/dist/cli/index.js +5 -0
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/compiler/Graph.d.ts +3 -0
  16. package/dist/compiler/Graph.d.ts.map +1 -1
  17. package/dist/compiler/Graph.js +44 -37
  18. package/dist/compiler/Graph.js.map +1 -1
  19. package/dist/compiler/Source.d.ts +4 -0
  20. package/dist/compiler/Source.d.ts.map +1 -1
  21. package/dist/compiler/Source.js +66 -6
  22. package/dist/compiler/Source.js.map +1 -1
  23. package/dist/compiler/Transform.d.ts +4 -1
  24. package/dist/compiler/Transform.d.ts.map +1 -1
  25. package/dist/compiler/Transform.js +32 -46
  26. package/dist/compiler/Transform.js.map +1 -1
  27. package/dist/compiler/internal/Catalogs.d.ts.map +1 -1
  28. package/dist/compiler/internal/Catalogs.js +6 -1
  29. package/dist/compiler/internal/Catalogs.js.map +1 -1
  30. package/dist/compiler/internal/Compositions.d.ts.map +1 -1
  31. package/dist/compiler/internal/Compositions.js +5 -0
  32. package/dist/compiler/internal/Compositions.js.map +1 -1
  33. package/dist/compiler/internal/Configurations.d.ts.map +1 -1
  34. package/dist/compiler/internal/Configurations.js +5 -1
  35. package/dist/compiler/internal/Configurations.js.map +1 -1
  36. package/dist/compiler/internal/Contract.d.ts +4 -1
  37. package/dist/compiler/internal/Contract.d.ts.map +1 -1
  38. package/dist/compiler/internal/Contract.js +73 -138
  39. package/dist/compiler/internal/Contract.js.map +1 -1
  40. package/dist/compiler/internal/Contributions.d.ts.map +1 -1
  41. package/dist/compiler/internal/Contributions.js +1 -3
  42. package/dist/compiler/internal/Contributions.js.map +1 -1
  43. package/dist/compiler/internal/Dynamic.d.ts +1 -1
  44. package/dist/compiler/internal/Dynamic.d.ts.map +1 -1
  45. package/dist/compiler/internal/Dynamic.js +5 -5
  46. package/dist/compiler/internal/Dynamic.js.map +1 -1
  47. package/dist/compiler/internal/Names.d.ts +5 -0
  48. package/dist/compiler/internal/Names.d.ts.map +1 -0
  49. package/dist/compiler/internal/Names.js +37 -0
  50. package/dist/compiler/internal/Names.js.map +1 -0
  51. package/dist/compiler/internal/Selectors.d.ts +1 -1
  52. package/dist/compiler/internal/Selectors.d.ts.map +1 -1
  53. package/dist/compiler/internal/Selectors.js +5 -2
  54. package/dist/compiler/internal/Selectors.js.map +1 -1
  55. package/dist/compiler/internal/Stylesheets.js +2 -2
  56. package/dist/compiler/internal/Stylesheets.js.map +1 -1
  57. package/dist/compiler/internal/Themes.d.ts.map +1 -1
  58. package/dist/compiler/internal/Themes.js +85 -24
  59. package/dist/compiler/internal/Themes.js.map +1 -1
  60. package/dist/compiler/internal/Vars.d.ts +2 -0
  61. package/dist/compiler/internal/Vars.d.ts.map +1 -1
  62. package/dist/compiler/internal/Vars.js +7 -1
  63. package/dist/compiler/internal/Vars.js.map +1 -1
  64. package/dist/default.d.ts +11 -15
  65. package/dist/default.d.ts.map +1 -1
  66. package/dist/default.js +13 -14
  67. package/dist/default.js.map +2 -2
  68. package/dist/default.js.zyzz.json +1 -1
  69. package/dist/index.d.ts +15 -0
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +15 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/internal/Authoring.d.ts.map +1 -1
  74. package/dist/internal/Authoring.js +8 -3
  75. package/dist/internal/Authoring.js.map +1 -1
  76. package/dist/internal/Binding.d.ts +4 -0
  77. package/dist/internal/Binding.d.ts.map +1 -1
  78. package/dist/internal/Binding.js +2 -0
  79. package/dist/internal/Binding.js.map +1 -1
  80. package/dist/internal/Configuration.d.ts +2 -2
  81. package/dist/internal/Configuration.d.ts.map +1 -1
  82. package/dist/internal/Configuration.js +4 -2
  83. package/dist/internal/Configuration.js.map +1 -1
  84. package/dist/internal/Identity.d.ts +3 -3
  85. package/dist/internal/Identity.d.ts.map +1 -1
  86. package/dist/internal/Identity.js +11 -9
  87. package/dist/internal/Identity.js.map +1 -1
  88. package/dist/internal/Theme.js +2 -2
  89. package/dist/internal/Theme.js.map +1 -1
  90. package/dist/internal/Token.d.ts +16 -3
  91. package/dist/internal/Token.d.ts.map +1 -1
  92. package/dist/internal/Token.js +11 -2
  93. package/dist/internal/Token.js.map +1 -1
  94. package/dist/internal/VariableSets.d.ts.map +1 -1
  95. package/dist/internal/VariableSets.js +19 -2
  96. package/dist/internal/VariableSets.js.map +1 -1
  97. package/dist/node/Host.d.ts +2 -0
  98. package/dist/node/Host.d.ts.map +1 -1
  99. package/dist/node/Host.js +9 -0
  100. package/dist/node/Host.js.map +1 -1
  101. package/dist/react-native/StyleSheet.js +2 -0
  102. package/dist/react-native/StyleSheet.js.map +1 -1
  103. package/dist/runtime/Composition.d.ts +1 -1
  104. package/dist/runtime/ConditionalRecipe.d.ts +1 -1
  105. package/dist/runtime/PayloadRecipe.d.ts +1 -1
  106. package/dist/runtime/Recipe.d.ts +1 -1
  107. package/dist/styleFunction.d.ts +1 -1
  108. package/dist/styleFunction.d.ts.map +1 -1
  109. package/dist/styleFunction.js.map +1 -1
  110. package/dist/variable.js +1 -1
  111. package/dist/variable.js.map +1 -1
  112. package/dist/variants.d.ts +14 -6
  113. package/dist/variants.d.ts.map +1 -1
  114. package/dist/variants.js.map +1 -1
  115. package/dist/vite/index.d.ts +2 -0
  116. package/dist/vite/index.d.ts.map +1 -1
  117. package/dist/vite/index.js +111 -42
  118. package/dist/vite/index.js.map +1 -1
  119. package/dist/web/Css.d.ts +7 -2
  120. package/dist/web/Css.d.ts.map +1 -1
  121. package/dist/web/Css.js +46 -39
  122. package/dist/web/Css.js.map +1 -1
  123. package/dist/web/internal/ClassName.d.ts +13 -2
  124. package/dist/web/internal/ClassName.d.ts.map +1 -1
  125. package/dist/web/internal/ClassName.js +17 -30
  126. package/dist/web/internal/ClassName.js.map +1 -1
  127. package/dist/web/internal/Themes.d.ts +3 -0
  128. package/dist/web/internal/Themes.d.ts.map +1 -1
  129. package/dist/web/internal/Themes.js +52 -24
  130. package/dist/web/internal/Themes.js.map +1 -1
  131. package/package.json +1 -1
  132. package/src/Config.ts +5 -0
  133. package/src/Style.ts +4 -1
  134. package/src/Vars.ts +88 -29
  135. package/src/cli/index.ts +5 -0
  136. package/src/compiler/Graph.ts +60 -46
  137. package/src/compiler/Source.ts +86 -5
  138. package/src/compiler/Transform.ts +42 -60
  139. package/src/compiler/internal/Catalogs.ts +9 -1
  140. package/src/compiler/internal/Compositions.ts +7 -0
  141. package/src/compiler/internal/Configurations.ts +5 -1
  142. package/src/compiler/internal/Contract.ts +104 -239
  143. package/src/compiler/internal/Contributions.ts +1 -5
  144. package/src/compiler/internal/Dynamic.ts +14 -9
  145. package/src/compiler/internal/Names.ts +48 -0
  146. package/src/compiler/internal/Selectors.ts +6 -1
  147. package/src/compiler/internal/Stylesheets.ts +2 -2
  148. package/src/compiler/internal/Themes.ts +115 -35
  149. package/src/compiler/internal/Vars.ts +9 -1
  150. package/src/default.ts +17 -20
  151. package/src/index.ts +15 -0
  152. package/src/internal/Authoring.ts +9 -3
  153. package/src/internal/Binding.ts +5 -0
  154. package/src/internal/Configuration.ts +7 -4
  155. package/src/internal/Identity.ts +14 -12
  156. package/src/internal/Theme.ts +2 -2
  157. package/src/internal/Token.ts +52 -12
  158. package/src/internal/VariableSets.ts +26 -2
  159. package/src/node/Host.ts +22 -0
  160. package/src/react-native/StyleSheet.ts +6 -0
  161. package/src/styleFunction.ts +1 -3
  162. package/src/variable.ts +1 -1
  163. package/src/variants.ts +38 -16
  164. package/src/vite/index.ts +137 -51
  165. package/src/web/Css.ts +83 -49
  166. package/src/web/internal/ClassName.ts +31 -36
  167. package/src/web/internal/Themes.ts +79 -29
package/src/vite/index.ts CHANGED
@@ -69,6 +69,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
69
69
  const sourceEntrypoints = new Set<string>()
70
70
  const identities = new Map<string, string>()
71
71
  const lazyImports = new WeakMap<Parser.ParseResult, string[]>()
72
+ let directories: readonly string[]
72
73
  let root: string
73
74
 
74
75
  function snapshot(environment: Environment) {
@@ -94,12 +95,33 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
94
95
  function sourceId(file: string) {
95
96
  let id = identities.get(file)
96
97
  if (!id) {
97
- id = `app/${Path.relative(root, file).split(Path.sep).join('/')}`
98
+ const directory = sourceRoot(file) ?? root
99
+ const namespace =
100
+ directory === root
101
+ ? 'app'
102
+ : `include-${directories.indexOf(directory) - 1}`
103
+ id = `${namespace}/${Path.relative(directory, file).split(Path.sep).join('/')}`
98
104
  identities.set(file, id)
99
105
  }
100
106
  return id
101
107
  }
102
108
 
109
+ // Included directories have portable namespaces rather than traversal IDs.
110
+ function sourceFile(id: string) {
111
+ const offset = id.indexOf('/')
112
+ const namespace = id.slice(0, offset)
113
+ const directory = (() => {
114
+ if (namespace === 'app') return root
115
+ if (/^include-\d+$/.test(namespace))
116
+ return directories[Number(namespace.slice(8)) + 1]
117
+ return undefined
118
+ })()
119
+
120
+ return directory === undefined
121
+ ? undefined
122
+ : Path.join(directory, id.slice(offset + 1))
123
+ }
124
+
103
125
  function resource(id: string) {
104
126
  const query = id.split('?')[1]?.split('#')[0]
105
127
 
@@ -119,17 +141,28 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
119
141
  if (sourceEntrypoints.has(normalize(id))) return true
120
142
  // Ids carrying foreign queries are Vite resources such as inline-module proxies, not source.
121
143
  if (resource(id)) return false
122
- const relative = Path.relative(root, id)
123
144
 
124
145
  return (
125
146
  Path.isAbsolute(id) &&
126
- !relative.startsWith('..') &&
127
- !relative.split(Path.sep).includes('node_modules') &&
147
+ sourceRoot(id) !== undefined &&
128
148
  /\.[cm]?[jt]sx?$/.test(id) &&
129
- !/\.(?:d|test|test-d|bench)\.[cm]?[jt]sx?$/.test(id)
149
+ !/\.(?:d|test|test-d|bench|bench-d)\.[cm]?[jt]sx?$/.test(id)
130
150
  )
131
151
  }
132
152
 
153
+ function sourceRoot(file: string) {
154
+ return directories.find((directory) => {
155
+ const relative = Path.relative(directory, file)
156
+
157
+ return (
158
+ relative !== '..' &&
159
+ !relative.startsWith(`..${Path.sep}`) &&
160
+ !Path.isAbsolute(relative) &&
161
+ !relative.split(Path.sep).includes('node_modules')
162
+ )
163
+ })
164
+ }
165
+
133
166
  function discover(environment: Environment, host: Host) {
134
167
  let pending = discoveries.get(environment)
135
168
 
@@ -137,10 +170,14 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
137
170
  pending = (async () => {
138
171
  const sources = new Map<string, string>()
139
172
  const eagerFiles = new Set<string>()
173
+ const visited = new Set<string>()
140
174
 
141
175
  contributionFiles.set(environment, eagerFiles)
142
176
 
143
177
  async function collect(directory: string): Promise<void> {
178
+ if (visited.has(directory)) return
179
+ visited.add(directory)
180
+
144
181
  for (const item of await Fs.readdir(directory, {
145
182
  withFileTypes: true,
146
183
  })) {
@@ -163,7 +200,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
163
200
  const file = Path.join(directory, item.name)
164
201
 
165
202
  if (item.isDirectory()) await collect(file)
166
- else if (item.isFile() && eager(file)) {
203
+ else if (item.isFile() && eager(file, environment)) {
167
204
  host.watch(file)
168
205
 
169
206
  const source = await Fs.readFile(file, 'utf8')
@@ -175,7 +212,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
175
212
  }
176
213
  }
177
214
 
178
- await collect(root)
215
+ for (const directory of directories) await collect(directory)
179
216
 
180
217
  return sources
181
218
  })()
@@ -185,10 +222,17 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
185
222
  return pending
186
223
  }
187
224
 
188
- function eager(file: string) {
225
+ function eager(file: string, environment: Environment) {
189
226
  return (
190
227
  eligible(file) &&
191
- !Path.relative(root, file)
228
+ file !== environment.config.configFile &&
229
+ !environment.config.configFileDependencies.includes(file) &&
230
+ // Server entrypoints must not contribute to the client stylesheet scan.
231
+ !(
232
+ environment.config.consumer === 'client' &&
233
+ /\.server\.[cm]?[jt]sx?$/.test(file)
234
+ ) &&
235
+ !Path.relative(sourceRoot(file)!, file)
192
236
  .split(Path.sep)
193
237
  .some((part) =>
194
238
  ['test', 'tests', '__tests__', 'fixtures', '__fixtures__'].includes(
@@ -323,7 +367,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
323
367
  }
324
368
 
325
369
  const pending = discoveries.get(environment)
326
- if (!pending || !eager(file)) return
370
+ if (!pending || !eager(file, environment)) return
327
371
 
328
372
  const sources = await pending
329
373
 
@@ -733,7 +777,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
733
777
  map: {
734
778
  ...output.map,
735
779
  sources: output.map.sources.map((source) =>
736
- source === null ? null : Path.join(root, source.slice(4)),
780
+ source === null ? null : (sourceFile(source) ?? source),
737
781
  ),
738
782
  },
739
783
  }
@@ -773,7 +817,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
773
817
  let line = 0
774
818
 
775
819
  for (const [id, output] of Object.entries(result.modules)) {
776
- if (!connected.has(Path.join(root, id.slice(4)))) continue
820
+ if (!connected.has(sourceFile(id)!)) continue
777
821
  if (!output.css) continue
778
822
 
779
823
  for (const mapping of Mapping.allMappings(
@@ -788,7 +832,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
788
832
  const location = {
789
833
  generated,
790
834
  original: mapping.original,
791
- source: Path.join(root, mapping.source.slice(4)),
835
+ source: sourceFile(mapping.source) ?? mapping.source,
792
836
  }
793
837
 
794
838
  if (mapping.name === undefined) Mapping.addMapping(map, location)
@@ -800,7 +844,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
800
844
  if (source !== null)
801
845
  Mapping.setSourceContent(
802
846
  map,
803
- Path.join(root, source.slice(4)),
847
+ sourceFile(source) ?? source,
804
848
  output.cssMap.sourcesContent?.[index] ?? null,
805
849
  )
806
850
 
@@ -808,7 +852,6 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
808
852
  line += output.css.split('\n').length
809
853
  }
810
854
 
811
- const appRoot = await Fs.realpath(root)
812
855
  const owners = new Map<string, string>()
813
856
 
814
857
  for (const id of new Set(Object.values(result.sharedAssetOwners ?? {}))) {
@@ -839,11 +882,9 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
839
882
  const raw = target.split(/[?#]/)[0]!
840
883
 
841
884
  const identity = result.sharedAssetOwners?.[key]
842
- const path = target.startsWith('app/')
843
- ? Path.join(root, decodeURIComponent(raw.slice(4)))
844
- : Path.isAbsolute(raw)
845
- ? decodeURIComponent(raw)
846
- : undefined
885
+ const path =
886
+ sourceFile(decodeURIComponent(raw)) ??
887
+ (Path.isAbsolute(raw) ? decodeURIComponent(raw) : undefined)
847
888
  if (!path) throw new Error('Asset path escapes the Vite graph.')
848
889
 
849
890
  const asset = await (async () => {
@@ -852,11 +893,12 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
852
893
  } catch (error) {
853
894
  if (
854
895
  (error as NodeJS.ErrnoException).code !== 'ENOENT' ||
855
- !identity?.startsWith('app/')
896
+ !identity ||
897
+ !sourceFile(identity)
856
898
  )
857
899
  throw error
858
900
 
859
- const importer = Path.join(root, identity.slice(4))
901
+ const importer = sourceFile(identity)!
860
902
  const specifier = Path.relative(Path.dirname(importer), path)
861
903
  .split(Path.sep)
862
904
  .join('/')
@@ -872,11 +914,12 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
872
914
  })()
873
915
  const file = asset.file
874
916
 
875
- const owner = identity?.startsWith('app/')
876
- ? appRoot
877
- : identity
878
- ? owners.get(identity)
879
- : undefined
917
+ const owner =
918
+ identity && sourceFile(identity)
919
+ ? sourceRoot(sourceFile(identity)!)
920
+ : identity
921
+ ? owners.get(identity)
922
+ : undefined
880
923
 
881
924
  const relative = owner ? Path.relative(owner, file) : '..'
882
925
  if (
@@ -961,7 +1004,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
961
1004
  ) as Mapping.EncodedSourceMap
962
1005
 
963
1006
  sharedMap.sources = sharedMap.sources.map((source) =>
964
- source?.startsWith('app/') ? Path.join(root, source.slice(4)) : source,
1007
+ source === null ? null : (sourceFile(source) ?? source),
965
1008
  )
966
1009
 
967
1010
  const output = result.modules[sourceId(entry.file)]!
@@ -982,7 +1025,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
982
1025
  map: {
983
1026
  ...output.map,
984
1027
  sources: output.map.sources.map((source) =>
985
- source === null ? null : Path.join(root, source.slice(4)),
1028
+ source === null ? null : (sourceFile(source) ?? source),
986
1029
  ),
987
1030
  },
988
1031
  }
@@ -1219,6 +1262,9 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1219
1262
  },
1220
1263
  },
1221
1264
  configureServer(server) {
1265
+ // Directory watches belong to the server, not module import dependencies.
1266
+ server.watcher.add(directories.slice(1))
1267
+
1222
1268
  server.middlewares.use((request, response, next) => {
1223
1269
  const path = new URL(request.url ?? '/', 'http://localhost').pathname
1224
1270
  const file = assets.get(path)
@@ -1236,8 +1282,21 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1236
1282
  }, next)
1237
1283
  })
1238
1284
  },
1239
- configResolved(config) {
1240
- root = config.root
1285
+ async configResolved(config) {
1286
+ root = await Fs.realpath(config.root)
1287
+ directories = [
1288
+ root,
1289
+ ...(await Promise.all(
1290
+ (options.include ?? []).map(async (directory) => {
1291
+ const file = await Fs.realpath(Path.resolve(root, directory))
1292
+ if (!(await Fs.stat(file)).isDirectory())
1293
+ throw new Error(
1294
+ `Zyzz include paths must be directories: ${directory}`,
1295
+ )
1296
+ return file
1297
+ }),
1298
+ )),
1299
+ ]
1241
1300
  if (native) return
1242
1301
  const lightning = (config.css.lightningcss ??= {})
1243
1302
  lightning.include =
@@ -1332,11 +1391,16 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1332
1391
  await updateDiscovery(this.environment, file, change.event)
1333
1392
  },
1334
1393
  async hotUpdate({ file, modules, timestamp, type, server }) {
1335
- await updateDiscovery(this.environment, file, type)
1336
-
1337
1394
  const state = entries(this.environment)
1395
+ const previous = [...state.values()].map((entry) => ({
1396
+ code: entry.code,
1397
+ file: entry.file,
1398
+ imports: entry.imports,
1399
+ }))
1400
+
1401
+ await updateDiscovery(this.environment, file, type)
1338
1402
  const first = [...state.values()].find(
1339
- (entry) => entry.files.has(file) || eager(file),
1403
+ (entry) => entry.files.has(file) || eager(file, this.environment),
1340
1404
  )
1341
1405
  if (!first && !removed.get(this.environment)?.has(file)) return modules
1342
1406
 
@@ -1358,31 +1422,51 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1358
1422
  true,
1359
1423
  ).catch(() => undefined)
1360
1424
  : undefined
1361
- const affected = new Set(
1362
- modules.filter((module) => {
1363
- const entry = module.id && state.get(module.id)
1364
- return (
1365
- !output ||
1366
- !entry ||
1367
- entry.code !== output.modules[sourceId(entry.file)]?.code ||
1368
- entry.imports !==
1425
+ const javascriptChanged = await (async () => {
1426
+ if (!output) return true
1427
+
1428
+ // Portable build adapters share this module and do not require Vite.
1429
+ const { transformWithOxc } = await import('vite')
1430
+ const changed = await Promise.all(
1431
+ previous.map(async (entry) => {
1432
+ const code = output.modules[sourceId(entry.file)]?.code
1433
+ if (
1434
+ entry.imports !==
1369
1435
  JSON.stringify(output.imports[sourceId(entry.file)])
1370
- )
1371
- }),
1436
+ )
1437
+ return true
1438
+ if (entry.code === code) return false
1439
+ if (entry.code === undefined || code === undefined) return true
1440
+
1441
+ // Generated type assertions can change without changing runtime bindings.
1442
+ const [before, after] = await Promise.all([
1443
+ transformWithOxc(entry.code, entry.file, { sourcemap: false }),
1444
+ transformWithOxc(code, entry.file, { sourcemap: false }),
1445
+ ])
1446
+ return before.code !== after.code
1447
+ }),
1448
+ )
1449
+ return changed.some(Boolean)
1450
+ })()
1451
+ // Compiler-only inputs have watch edges, but their stylesheets update separately.
1452
+ const affected = new Set(
1453
+ modules.filter(
1454
+ (module) =>
1455
+ javascriptChanged ||
1456
+ (module.id
1457
+ ? !output?.modules[sourceId(module.id)]
1458
+ : module.type !== 'asset'),
1459
+ ),
1372
1460
  )
1373
1461
  const ids = new Set<string>()
1374
1462
  if (type === 'delete') ids.add(cssId(file))
1375
1463
 
1376
1464
  for (const entry of entries(this.environment).values()) {
1377
- if (!entry.files.has(file) && !eager(file)) continue
1465
+ if (!entry.files.has(file) && !eager(file, this.environment)) continue
1378
1466
 
1379
1467
  // Theme scopes affect CSS even when Vite's JavaScript import was erased.
1380
- if (
1381
- !output ||
1382
- entry.code !== output.modules[sourceId(entry.file)]?.code ||
1383
- entry.imports !== JSON.stringify(output.imports[sourceId(entry.file)])
1384
- )
1385
- ids.add(entry.file)
1468
+ // Rewritten importers can lose a source edge, so changed bindings must update together.
1469
+ if (javascriptChanged) ids.add(entry.file)
1386
1470
  ids.add(cssId(entry.file))
1387
1471
  ids.add(sharedId)
1388
1472
  }
@@ -1635,6 +1719,8 @@ export declare namespace zyzz {
1635
1719
  type Options = {
1636
1720
  /** False retains authored calls and requires explicit identities where needed. */
1637
1721
  readonly compiler?: boolean | undefined
1722
+ /** Additional source directories, absolute or relative to the Vite root. */
1723
+ readonly include?: readonly string[] | undefined
1638
1724
  /** Native context captured at creation. Disables CSS delivery and initialization scripts. */
1639
1725
  readonly native?: Graph.compile.Options['native']
1640
1726
  /** Include the CSS reset. Defaults to false. */
package/src/web/Css.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import * as Targets from '../internal/Targets.js'
6
6
  import * as ClassName from './internal/ClassName.js'
7
+ import * as Identity from '../internal/Identity.js'
7
8
  import * as Contributions from './internal/Contributions.js'
8
9
  import * as Binding from '../internal/Binding.js'
9
10
  import * as Cascade from '../internal/Cascade.js'
@@ -33,6 +34,7 @@ export function compile<
33
34
  >(
34
35
  options: compile.Options<name, themeName>,
35
36
  ): compile.ReturnType<name, themeName> {
37
+ const labels = options[ClassName.labels] ?? options.styles[ClassName.labels]
36
38
  options = {
37
39
  ...options,
38
40
  styles: { styles: options.styles.styles.map(Targets.web) },
@@ -417,6 +419,8 @@ export function compile<
417
419
 
418
420
  const identities = new Map<string, string>()
419
421
  const rules = new Map<string, string>()
422
+ type ConditionalRule = { conditions: readonly string[]; value: string }
423
+ const conditionalRules = new Map<string, ConditionalRule>()
420
424
  const identical = new Map<string, string>()
421
425
  const explicitBodies = new Map<string, string>()
422
426
  const selectors = new Map<string, string>()
@@ -470,12 +474,18 @@ export function compile<
470
474
 
471
475
  const names: string[] = explicit === undefined ? [] : [explicit]
472
476
  let ordinal = 0
473
- const slots = new Map<string, number>()
474
- const developmentName = options.scope
475
- ? `definition-${styleIndex}`
476
- : style.name
477
-
478
- function emit(body: string, label: string, shared: boolean, output = mode) {
477
+ const cssName = labels?.[style.name]?.name ?? encode(style.name)
478
+ const cssNamespace =
479
+ labels?.[style.name]?.namespace ??
480
+ (options.scope ? Identity.name(options.scope) : undefined)
481
+
482
+ function emit(
483
+ body: string,
484
+ label: string,
485
+ shared: boolean,
486
+ output = mode,
487
+ conditional?: ConditionalRule,
488
+ ) {
479
489
  if (!body) return
480
490
 
481
491
  const independent =
@@ -483,12 +493,8 @@ export function compile<
483
493
  !options.development &&
484
494
  output === mode &&
485
495
  options.composition === 'independent'
486
- const key = `${output}:${body}`
487
- // Development slots belong to each style even when their initial values match.
488
- const reusable =
489
- explicit === undefined &&
490
- !options.development &&
491
- (shared || independent)
496
+ const key = JSON.stringify([cssNamespace, output, body])
497
+ const reusable = explicit === undefined && (shared || independent)
492
498
  const previous = reusable ? identical.get(key) : undefined
493
499
  if (previous && !names.includes(previous)) {
494
500
  names.push(previous)
@@ -498,43 +504,23 @@ export function compile<
498
504
  if (previous) shared = false
499
505
 
500
506
  // Contextual slots preserve authored ordering; development names survive value edits.
501
- const ordinalSlot = ordinal++
502
- const slot = options.development ? (slots.get(label) ?? 0) : ordinalSlot
503
- slots.set(label, slot + 1)
507
+ const slot = ordinal++
504
508
  const identity = (() => {
505
509
  if (explicit !== undefined)
506
510
  return `${explicit}-${mode}-${encode(label)}-${slot}`
507
- if (
508
- output === 'grouped' &&
509
- mode === 'grouped' &&
510
- !options.development &&
511
- options.composition === 'independent'
512
- )
513
- return `g_${rules.size.toString(36)}`
514
511
  if (output === 'grouped' && mode === 'grouped') {
515
- const name = options.development ? developmentName : style.name
516
- const scope =
517
- options.development && options.scope
518
- ? `_m${encode(options.scope)}`
519
- : ''
520
- return `g-${encode(name)}${scope}${slot ? `_s${slot}` : ''}`
512
+ return `z-${cssNamespace ? `${cssNamespace}-` : ''}${cssName}${slot ? `__${slot}` : ''}`
521
513
  }
522
514
  return ClassName.create({
523
515
  body,
524
- context:
525
- !shared || options.development
526
- ? JSON.stringify([
527
- options.scope,
528
- mode,
529
- options.development ? developmentName : style.name,
530
- ])
531
- : options.scope,
516
+ context: !shared ? cssName : undefined,
517
+ namespace: cssNamespace,
532
518
  property: label,
533
- slot: !shared || options.development ? slot : undefined,
534
- stable: options.development,
519
+ slot: !shared ? slot : undefined,
535
520
  })
536
521
  })()
537
- const owner = mode === 'atomic' ? `${style.name}:${slot}` : undefined
522
+ const owner =
523
+ mode === 'atomic' && !shared ? `${style.name}:${slot}` : undefined
538
524
  if (
539
525
  (rules.has(identity) && rules.get(identity) !== body) ||
540
526
  (owner !== undefined &&
@@ -543,12 +529,19 @@ export function compile<
543
529
  )
544
530
  diagnostics.push({
545
531
  code: 'identity_collision',
546
- message: 'Distinct rules produced the same class identifier.',
532
+ message:
533
+ 'Generated class names conflict. Supply distinct config or style ids.',
547
534
  path: [style.name],
548
535
  })
549
536
 
550
537
  if (owner !== undefined) identities.set(identity, owner)
551
538
  rules.set(identity, body)
539
+ if (
540
+ conditional?.conditions.some((condition) =>
541
+ /^@(media|supports)\b/.test(condition),
542
+ )
543
+ )
544
+ conditionalRules.set(identity, conditional)
552
545
  if (explicit !== undefined) selectors.set(identity, explicit)
553
546
  else {
554
547
  if (reusable) identical.set(key, identity)
@@ -611,7 +604,7 @@ export function compile<
611
604
  !nestedComposition &&
612
605
  !conditions.length &&
613
606
  groups.get(conflict(property)) !== false
614
- emit(body, property, shared)
607
+ emit(body, property, shared, mode, { conditions, value })
615
608
  }
616
609
  }
617
610
 
@@ -673,7 +666,7 @@ export function compile<
673
666
  options.schemes ?? false,
674
667
  options[Themes.shared],
675
668
  )
676
- : { classes: Object.freeze({}), css: '', resources: [] }
669
+ : { classes: Object.freeze({}), css: '', resources: [], rules: [] }
677
670
  } catch (error) {
678
671
  throw new CompileError([
679
672
  {
@@ -686,20 +679,57 @@ export function compile<
686
679
 
687
680
  const scopedCss = [
688
681
  scopes.css,
689
- ...[...rules].map(
690
- ([name, body]) => `.${selectors.get(name) ?? name}{${body}}`,
691
- ),
682
+ ...[...rules].map((entry) => {
683
+ const [name, body] = entry
684
+ const selector = `.${ClassName.selector(selectors.get(name) ?? name)}`
685
+ const conditional = conditionalRules.get(name)
686
+ if (
687
+ !conditional ||
688
+ conditional.conditions.some(
689
+ (condition) =>
690
+ condition.startsWith('@') &&
691
+ !/^@(media|supports)\b/.test(condition),
692
+ )
693
+ )
694
+ return `${selector}{${body}}`
695
+
696
+ // Inactive conditions must surround the class rather than leave a matched empty wrapper.
697
+ const conditions = conditional.conditions.filter((condition) =>
698
+ condition.startsWith('@'),
699
+ )
700
+ const nested = conditional.conditions
701
+ .filter((condition) => !condition.startsWith('@'))
702
+ .reduceRight(
703
+ (body, condition) => `${condition}{${body}}`,
704
+ conditional.value,
705
+ )
706
+ return conditions.reduceRight(
707
+ (body, condition) => `${condition}{${body}}`,
708
+ `${selector}{${nested}}`,
709
+ )
710
+ }),
692
711
  ]
693
712
  .filter(Boolean)
694
713
  .join('\n')
695
714
 
696
- return Object.freeze({
715
+ const result = {
697
716
  ...(scopes.resources.length ? { [Themes.shared]: scopes.resources } : {}),
698
717
  ...(contributionCss ? { contributionCss, scopedCss } : {}),
718
+ [ClassName.rules]: Object.freeze({
719
+ ...Object.fromEntries(rules),
720
+ ...Object.fromEntries(
721
+ scopes.rules.map((rule) => [
722
+ `@scope:${rule.selector.slice(1)}:${Identity.name(rule.property)}:${Identity.name(JSON.stringify(rule.conditions))}`,
723
+ rule.value,
724
+ ]),
725
+ ),
726
+ }),
699
727
  classes: Object.freeze(classes),
700
728
  css: [contributionCss, scopedCss].filter(Boolean).join('\n'),
701
729
  vars: scopes.classes as Readonly<Record<themeName, string>>,
702
- })
730
+ }
731
+ Object.defineProperty(result, ClassName.rules, { enumerable: false })
732
+ return Object.freeze(result)
703
733
  }
704
734
 
705
735
  /** Input and output contracts for literal compilation. */
@@ -714,6 +744,8 @@ export declare namespace compile {
714
744
  > = {
715
745
  /** Separates generated token rules for hosts with independent CSS resources. */
716
746
  readonly [Themes.shared]?: 'all' | 'defaults' | undefined
747
+ /** Readable source bindings and consumer-owned configuration namespaces. */
748
+ readonly [ClassName.labels]?: ClassName.Labels | undefined
717
749
  /** Fixed class identities used by CSS-only consumers. */
718
750
  readonly names?: Readonly<Record<string, string>> | undefined
719
751
  /**
@@ -726,14 +758,14 @@ export declare namespace compile {
726
758
  /** CSS representation; atomic declarations are the default. */
727
759
  readonly cssOutput?: 'atomic' | 'grouped' | undefined
728
760
  readonly composition?: 'independent' | 'ordered' | undefined
729
- /** Stable declaration names for CSS-only development updates. */
761
+ /** Retains live definitions for development updates. */
730
762
  readonly development?: boolean | undefined
731
763
  /**
732
764
  * Emit the `color-scheme` selection classes. Set by modules whose runtime
733
765
  * helpers can apply a scheme, so lowered `light-dark()` resolves there.
734
766
  */
735
767
  readonly schemes?: boolean | undefined
736
- /** Optional module scope for independently delivered stylesheets. */
768
+ /** Explicit consumer-owned namespace for independently delivered stylesheets. */
737
769
  readonly scope?: string | undefined
738
770
  /** Ordered definitions; no themes or source adapter is required. */
739
771
  readonly styles: Style.Definition<name>
@@ -748,6 +780,8 @@ export declare namespace compile {
748
780
  name extends string = string,
749
781
  themeName extends string = string,
750
782
  > = {
783
+ /** Exact emitted rule bodies retained for graph-wide collision diagnostics. */
784
+ readonly [ClassName.rules]: Readonly<Record<string, string>>
751
785
  /** Shared token resources omitted from this module's stylesheet. */
752
786
  readonly [Themes.shared]?: readonly Themes.Resource[] | undefined
753
787
  /** Contribution text separated for graph-wide hoisting. */