zyzz 0.0.24 → 0.0.25

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 (87) hide show
  1. package/README.md +23 -365
  2. package/dist/Style.d.ts +2 -0
  3. package/dist/Style.d.ts.map +1 -1
  4. package/dist/compiler/Graph.d.ts +3 -0
  5. package/dist/compiler/Graph.d.ts.map +1 -1
  6. package/dist/compiler/Graph.js +38 -2
  7. package/dist/compiler/Graph.js.map +1 -1
  8. package/dist/compiler/Source.d.ts.map +1 -1
  9. package/dist/compiler/Source.js +11 -3
  10. package/dist/compiler/Source.js.map +1 -1
  11. package/dist/compiler/Transform.d.ts +4 -0
  12. package/dist/compiler/Transform.d.ts.map +1 -1
  13. package/dist/compiler/Transform.js +79 -15
  14. package/dist/compiler/Transform.js.map +1 -1
  15. package/dist/compiler/internal/Applications.d.ts +20 -0
  16. package/dist/compiler/internal/Applications.d.ts.map +1 -1
  17. package/dist/compiler/internal/Applications.js +151 -47
  18. package/dist/compiler/internal/Applications.js.map +1 -1
  19. package/dist/compiler/internal/Compositions.d.ts.map +1 -1
  20. package/dist/compiler/internal/Compositions.js +6 -0
  21. package/dist/compiler/internal/Compositions.js.map +1 -1
  22. package/dist/compiler/internal/Configurations.d.ts.map +1 -1
  23. package/dist/compiler/internal/Configurations.js +4 -0
  24. package/dist/compiler/internal/Configurations.js.map +1 -1
  25. package/dist/compiler/internal/Contract.d.ts.map +1 -1
  26. package/dist/compiler/internal/Contract.js +34 -7
  27. package/dist/compiler/internal/Contract.js.map +1 -1
  28. package/dist/compiler/internal/PackedStyles.d.ts.map +1 -1
  29. package/dist/compiler/internal/PackedStyles.js +7 -0
  30. package/dist/compiler/internal/PackedStyles.js.map +1 -1
  31. package/dist/compiler/internal/Themes.d.ts.map +1 -1
  32. package/dist/compiler/internal/Themes.js +20 -8
  33. package/dist/compiler/internal/Themes.js.map +1 -1
  34. package/dist/internal/Configuration.d.ts +10 -0
  35. package/dist/internal/Configuration.d.ts.map +1 -1
  36. package/dist/internal/Configuration.js +8 -0
  37. package/dist/internal/Configuration.js.map +1 -1
  38. package/dist/internal/Token.d.ts +2 -0
  39. package/dist/internal/Token.d.ts.map +1 -1
  40. package/dist/internal/Token.js.map +1 -1
  41. package/dist/next/internal/Project.d.ts.map +1 -1
  42. package/dist/next/internal/Project.js +30 -25
  43. package/dist/next/internal/Project.js.map +1 -1
  44. package/dist/next/loader.js +2 -1
  45. package/dist/next/loader.js.map +1 -1
  46. package/dist/node/Host.d.ts.map +1 -1
  47. package/dist/node/Host.js +32 -23
  48. package/dist/node/Host.js.map +1 -1
  49. package/dist/node/internal/Contracts.d.ts +22 -0
  50. package/dist/node/internal/Contracts.d.ts.map +1 -0
  51. package/dist/node/internal/Contracts.js +287 -0
  52. package/dist/node/internal/Contracts.js.map +1 -0
  53. package/dist/unplugin/index.d.ts.map +1 -1
  54. package/dist/unplugin/index.js +31 -13
  55. package/dist/unplugin/index.js.map +1 -1
  56. package/dist/vite/index.d.ts.map +1 -1
  57. package/dist/vite/index.js +30 -37
  58. package/dist/vite/index.js.map +1 -1
  59. package/dist/web/Css.d.ts +10 -5
  60. package/dist/web/Css.d.ts.map +1 -1
  61. package/dist/web/Css.js +170 -31
  62. package/dist/web/Css.js.map +1 -1
  63. package/dist/web/internal/ClassName.d.ts +49 -0
  64. package/dist/web/internal/ClassName.d.ts.map +1 -1
  65. package/dist/web/internal/ClassName.js +57 -0
  66. package/dist/web/internal/ClassName.js.map +1 -1
  67. package/package.json +1 -1
  68. package/src/Style.ts +2 -0
  69. package/src/compiler/Graph.ts +42 -2
  70. package/src/compiler/Source.ts +11 -3
  71. package/src/compiler/Transform.ts +130 -29
  72. package/src/compiler/internal/Applications.ts +239 -63
  73. package/src/compiler/internal/Compositions.ts +9 -0
  74. package/src/compiler/internal/Configurations.ts +4 -0
  75. package/src/compiler/internal/Contract.ts +59 -7
  76. package/src/compiler/internal/PackedStyles.ts +9 -0
  77. package/src/compiler/internal/Themes.ts +25 -7
  78. package/src/internal/Configuration.ts +21 -0
  79. package/src/internal/Token.ts +2 -0
  80. package/src/next/internal/Project.ts +28 -23
  81. package/src/next/loader.ts +2 -1
  82. package/src/node/Host.ts +31 -24
  83. package/src/node/internal/Contracts.ts +384 -0
  84. package/src/unplugin/index.ts +35 -13
  85. package/src/vite/index.ts +35 -36
  86. package/src/web/Css.ts +228 -42
  87. package/src/web/internal/ClassName.ts +114 -0
@@ -11,7 +11,9 @@ import type { UnpluginBuildContext } from 'unplugin'
11
11
  import * as Graph from '../compiler/Graph.js'
12
12
  import * as AtRules from '../compiler/internal/AtRules.js'
13
13
  import * as Catalogs from '../compiler/internal/Catalogs.js'
14
+ import * as ClassName from '../web/internal/ClassName.js'
14
15
  import * as Contract from '../compiler/internal/Contract.js'
16
+ import * as Contracts from '../node/internal/Contracts.js'
15
17
  import * as Vite from '../vite/index.js'
16
18
  import * as ThemeRules from '../web/internal/Themes.js'
17
19
 
@@ -32,6 +34,7 @@ const portable = createUnplugin<Options | undefined, false>(
32
34
  (options = {}, meta) => {
33
35
  const root = Path.resolve(options.root ?? process.cwd())
34
36
  const compiler = Graph.create()
37
+ const packages = Contracts.create()
35
38
  const snapshot = Snapshot.create()
36
39
  let graph: Graph.compile.ReturnType | undefined
37
40
  let pending: Promise<void> | undefined
@@ -204,20 +207,17 @@ const portable = createUnplugin<Options | undefined, false>(
204
207
  required = false,
205
208
  ): Promise<boolean> {
206
209
  if (Object.hasOwn(contracts, file)) return true
207
- const sidecar = `${file}.zyzz.json`
208
210
 
209
- files.add(sidecar)
210
- try {
211
- contracts[file] = await snapshot.read(sidecar)
212
- } catch (error) {
213
- if (
214
- !required &&
215
- (error as NodeJS.ErrnoException).code === 'ENOENT'
216
- )
217
- return false
218
-
219
- throw error
220
- }
211
+ const source = await packages.read(file, {
212
+ read(path) {
213
+ files.add(path)
214
+ return snapshot.read(path)
215
+ },
216
+ required,
217
+ resolve,
218
+ })
219
+ if (source === undefined) return false
220
+ contracts[file] = source
221
221
  const metadata = Contract.read(contracts[file]!, new Map(), file)
222
222
  for (const section of metadata.stylesheets) {
223
223
  let owner = file
@@ -287,6 +287,7 @@ const portable = createUnplugin<Options | undefined, false>(
287
287
 
288
288
  // Only the combined stylesheet is emitted, so generated theme rules are shared once.
289
289
  const result = compiler.compile({
290
+ [ClassName.complete]: true,
290
291
  [ThemeRules.shared]: 'all',
291
292
  [Syntax.cache]: snapshot.programs(modules),
292
293
  compiler: options.compiler,
@@ -354,6 +355,7 @@ const portable = createUnplugin<Options | undefined, false>(
354
355
  boundary: string,
355
356
  outputDirectory = '.',
356
357
  targets?: Readonly<Record<string, string>>,
358
+ emitted: ReadonlySet<string> = new Set(),
357
359
  ) {
358
360
  const urls = new Set<string>()
359
361
  AtRules.transform({
@@ -405,6 +407,18 @@ const portable = createUnplugin<Options | undefined, false>(
405
407
  : { inputSourceMap: map, sourceMap: true }),
406
408
  visitor: {
407
409
  Rule(rule) {
410
+ // Shared rules repeat in each module that uses them, so keep the first.
411
+ if (rule.type === 'style') {
412
+ const [selector, ...others] = rule.value.selectors
413
+ const [component, ...rest] = selector ?? []
414
+ if (
415
+ !others.length &&
416
+ !rest.length &&
417
+ component?.type === 'class' &&
418
+ emitted.has(component.name)
419
+ )
420
+ return []
421
+ }
408
422
  if (rule.type !== 'import') return
409
423
  const url = replacements.get(rule.value.url)
410
424
  if (url) return AtRules.relocateImport(rule.value, url)
@@ -435,6 +449,7 @@ const portable = createUnplugin<Options | undefined, false>(
435
449
  },
436
450
  ]
437
451
  const seen = new Set<string>()
452
+ const units = new Set<string>()
438
453
  async function visit(id: string): Promise<void> {
439
454
  if (seen.has(id)) return
440
455
  seen.add(id)
@@ -442,12 +457,19 @@ const portable = createUnplugin<Options | undefined, false>(
442
457
  await visit(dependency)
443
458
  const output = result.modules[id]
444
459
  if (output) {
460
+ const names = [...(output[ClassName.units]?.values() ?? [])].map(
461
+ (decision) => decision.name,
462
+ )
445
463
  const processed = await stylesheet(
446
464
  output.css,
447
465
  JSON.stringify(output.cssMap),
448
466
  Path.join(root, id.slice(4)),
449
467
  await Fs.realpath(root),
468
+ '.',
469
+ undefined,
470
+ new Set(names.filter((name) => units.has(name))),
450
471
  )
472
+ for (const name of names) units.add(name)
451
473
  chunks.push({
452
474
  code: Buffer.from(processed.code).toString(),
453
475
  map: Buffer.from(processed.map!).toString(),
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 Contracts from '../node/internal/Contracts.js'
8
9
  import * as Mapping from '@jridgewell/gen-mapping'
9
10
  import * as Lightning from 'lightningcss'
10
11
  import * as Crypto from 'node:crypto'
@@ -51,6 +52,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
51
52
  const removed = new WeakMap<Environment, Set<string>>()
52
53
  const reads = new WeakMap<Environment, Map<string, string>>()
53
54
  const compilers = new WeakMap<Environment, Graph.create.ReturnType>()
55
+ const packages = Contracts.create()
54
56
 
55
57
  function compiler(environment: Environment) {
56
58
  let value = compilers.get(environment)
@@ -414,6 +416,16 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
414
416
  const lazy: Record<string, ReadonlySet<string>> = Object.create(null)
415
417
  const modules: Record<string, string> = Object.create(null)
416
418
  const files = new Set<string>()
419
+ const contractHost: Contracts.read.Options = {
420
+ read: async (file) => {
421
+ const source = await snapshot(entry.environment).read(file)
422
+ host.watch(file)
423
+ files.add(file)
424
+ return source
425
+ },
426
+ resolve: async (specifier, importer) =>
427
+ (await resolve(specifier, importer))?.id.split(/[?#]/)[0],
428
+ }
417
429
 
418
430
  async function visit(file: string, source?: string) {
419
431
  if (files.has(file)) return
@@ -512,19 +524,15 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
512
524
  }
513
525
  if (resolved.external || !eligible(resolved.id)) {
514
526
  const physical = resolved.id.split('?')[0]!.split('#')[0]!
515
- const sidecar = `${physical}.zyzz.json`
516
-
517
- if (Path.isAbsolute(physical) && /\.[cm]?[jt]sx?$/.test(physical)) {
518
- try {
519
- contracts[resolved.id] = await Fs.readFile(sidecar, 'utf8')
520
- host.watch(sidecar)
521
- files.add(sidecar)
522
- resolutions[specifier] = resolved.id
523
- continue
524
- } catch (error) {
525
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT')
526
- throw error
527
- }
527
+ const contract =
528
+ Path.isAbsolute(physical) && /\.[cm]?[jt]sx?$/.test(physical)
529
+ ? await packages.read(physical, contractHost)
530
+ : undefined
531
+
532
+ if (contract !== undefined) {
533
+ contracts[resolved.id] = contract
534
+ resolutions[specifier] = resolved.id
535
+ continue
528
536
  }
529
537
 
530
538
  resolutions[specifier] = null
@@ -649,25 +657,19 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
649
657
  if (!Path.isAbsolute(physical) || !/\.[cm]?[jt]sx?$/.test(physical))
650
658
  continue
651
659
 
652
- try {
653
- const sidecar = `${physical}.zyzz.json`
660
+ const contract = await packages.read(physical, contractHost)
661
+ if (contract === undefined) continue
654
662
 
655
- contracts[resolved.id] = await Fs.readFile(sidecar, 'utf8')
656
- host.watch(sidecar)
657
- files.add(sidecar)
663
+ contracts[resolved.id] = contract
658
664
 
659
- // Discover the package contribution without compiling unrelated authoring
660
- // expressions in an otherwise unreachable source file.
661
- const discovery = `${sourceId(file)}.zyzz-discovery`
665
+ // Discover the package contribution without compiling unrelated authoring
666
+ // expressions in an otherwise unreachable source file.
667
+ const discovery = `${sourceId(file)}.zyzz-discovery`
662
668
 
663
- modules[discovery] =
664
- (modules[discovery] ?? '') +
665
- `import ${JSON.stringify(specifier)};\n`
666
- ;(imports[discovery] ??= Object.create(null))[specifier] =
667
- resolved.id
668
- } catch (error) {
669
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
670
- }
669
+ modules[discovery] =
670
+ (modules[discovery] ?? '') +
671
+ `import ${JSON.stringify(specifier)};\n`
672
+ ;(imports[discovery] ??= Object.create(null))[specifier] = resolved.id
671
673
  }
672
674
  }
673
675
 
@@ -713,13 +715,10 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
713
715
  imports[owner] ??= Object.create(null)
714
716
  imports[owner]![specifier] = target.id
715
717
 
716
- if (!Object.hasOwn(contracts, target.id)) {
717
- const sidecar = `${target.id.split(/[?#]/)[0]}.zyzz.json`
718
-
719
- contracts[target.id] = await Fs.readFile(sidecar, 'utf8')
720
- host.watch(sidecar)
721
- files.add(sidecar)
722
- }
718
+ contracts[target.id] ??= (await packages.read(
719
+ target.id.split(/[?#]/)[0]!,
720
+ { ...contractHost, required: true },
721
+ ))!
723
722
 
724
723
  await dependencies(target.id)
725
724
  owner = target.id
package/src/web/Css.ts CHANGED
@@ -21,7 +21,8 @@ export type Contribution = Contributions.Definition
21
21
  /**
22
22
  * Emits atomic or grouped CSS without reading files or generating runtime code.
23
23
  * Shares only nonconflicting declaration domains; conflicting rules retain authored
24
- * order by default. Independent composition deduplicates complete applications.
24
+ * order by default. Independent composition deduplicates complete applications and
25
+ * shares equal atomic domain declarations under short names.
25
26
  * Class lists are scoped to the complete compilation input. Empty styles
26
27
  * return an empty class list and no rule.
27
28
  * @param options - Validated, ordered definitions from Style.define.
@@ -35,6 +36,9 @@ export function compile<
35
36
  options: compile.Options<name, themeName>,
36
37
  ): compile.ReturnType<name, themeName> {
37
38
  const labels = options[ClassName.labels] ?? options.styles[ClassName.labels]
39
+ const units =
40
+ options[ClassName.units] ??
41
+ ClassName.registry().scope({ owner: '', qualifier: '' })
38
42
  options = {
39
43
  ...options,
40
44
  styles: { styles: options.styles.styles.map(Targets.web) },
@@ -92,11 +96,17 @@ export function compile<
92
96
  const classes = Object.create(null) as Record<name, string>
93
97
  const diagnostics: Diagnostic[] = []
94
98
  const groups = new Map<string, false | string>()
95
- const nestedComposition = options.styles.styles.some((style) => style.rules)
99
+ const nestedComposition = options.styles.styles.some(
100
+ (style) => style.rules && !isolated(style),
101
+ )
96
102
 
97
103
  const properties = new Set<Style.Declaration['property']>()
98
- for (const style of analyzed)
104
+ // Conditional and composed declarations join the same conflict domains.
105
+ function collect(style: Style.NamedStyle): void {
99
106
  for (const { property } of style.declarations) properties.add(property)
107
+ for (const rule of style.rules ?? []) collect(rule.style)
108
+ }
109
+ for (const style of analyzed) collect(style)
100
110
 
101
111
  // Logical dimensions may alias either physical axis in inherited writing modes.
102
112
  // Preserve physical-only factoring when no logical dimension is authored.
@@ -361,32 +371,39 @@ export function compile<
361
371
 
362
372
  // Sharing is safe only when every use of a conflict domain has the same
363
373
  // declaration sequence. Conditions conservatively retain contextual identities.
374
+ // Isolated class lists never combine, so atomic sharing considers ordered styles only.
375
+ const ordered = new Map<string, false | string>()
376
+ const combinable = new Set<Style.NamedStyle>()
377
+ for (const [index, style] of options.styles.styles.entries())
378
+ if (!isolated(style)) combinable.add(canonicalStyles[index]!)
379
+
364
380
  for (const style of analyzed) {
365
- const domains = new Map<string, { body: string; property: string }>()
381
+ const domains = new Map<
382
+ string,
383
+ { body: string; mixed: boolean; property: string }
384
+ >()
366
385
 
367
386
  for (const item of style.declarations) {
368
387
  const property = item.property
369
388
  const key = conflict(property)
370
- const declaration = declarationBody(item)
371
- if (groups.get(key) === false) continue
372
- const entry = domains.get(key) ?? {
373
- body: '',
374
- property,
375
- }
376
- entry.body += declaration
377
- if (entry.property !== property) groups.set(key, false)
389
+ const entry = domains.get(key) ?? { body: '', mixed: false, property }
390
+ entry.body += declarationBody(item)
391
+ entry.mixed ||= entry.property !== property
378
392
  domains.set(key, entry)
379
393
  }
380
394
 
381
- for (const [key, entry] of domains) {
382
- const value = entry.body
383
-
384
- const previous = groups.get(key)
385
- groups.set(
386
- key,
387
- previous === undefined || previous === value ? value : false,
388
- )
389
- }
395
+ for (const [key, entry] of domains)
396
+ for (const map of combinable.has(style) ? [groups, ordered] : [groups]) {
397
+ const previous = map.get(key)
398
+ map.set(
399
+ key,
400
+ previous === false ||
401
+ entry.mixed ||
402
+ (previous !== undefined && previous !== entry.body)
403
+ ? false
404
+ : entry.body,
405
+ )
406
+ }
390
407
  }
391
408
 
392
409
  const defaultMode = options.cssOutput ?? 'atomic'
@@ -406,6 +423,75 @@ export function compile<
406
423
  | undefined
407
424
  }
408
425
 
426
+ function compositionMode(style: Style.NamedStyle) {
427
+ const mode = Object.getOwnPropertyDescriptor(style, 'composition')
428
+ ?.value as 'independent' | 'ordered' | undefined
429
+
430
+ return mode ?? options.composition ?? 'ordered'
431
+ }
432
+
433
+ // Development keeps every style ordered so names survive value edits.
434
+ function isolated(style: Style.NamedStyle) {
435
+ return compositionMode(style) === 'independent' && !options.development
436
+ }
437
+
438
+ type Item = {
439
+ readonly conditions: readonly string[]
440
+ readonly property: string
441
+ readonly value: string
442
+ }
443
+
444
+ // Lists declaration groups in authored order; undefined marks styles whose
445
+ // nested grouped output or anonymous layers emit one opaque rule.
446
+ function flatten(
447
+ style: Style.NamedStyle,
448
+ conditions: readonly string[] = [],
449
+ ): Item[] | undefined {
450
+ if (outputMode(style) === 'grouped') return undefined
451
+
452
+ if (style.rules) {
453
+ const items: Item[] = []
454
+ for (const rule of style.rules) {
455
+ if (rule.condition?.trim() === '@layer') return undefined
456
+
457
+ const nested = flatten(
458
+ rule.style,
459
+ rule.condition === undefined
460
+ ? conditions
461
+ : [...conditions, rule.condition],
462
+ )
463
+ if (!nested) return undefined
464
+
465
+ items.push(...nested)
466
+ }
467
+ return items
468
+ }
469
+
470
+ const items: Item[] = []
471
+ for (let index = 0; index < style.declarations.length; ) {
472
+ const property = style.declarations[index]!.property
473
+ let value = ''
474
+ // Keep same-property fallbacks ordered; they form one semantic value.
475
+ while (style.declarations[index]?.property === property) {
476
+ const declaration = style.declarations[index++]!
477
+ value += `${Literal.name(property)}:${serialize(declaration.value)}${declaration.important ? '!important' : ''};`
478
+ }
479
+ items.push({ conditions, property, value })
480
+ }
481
+ return items
482
+ }
483
+
484
+ function byDomain(items: readonly Item[]) {
485
+ const result = new Map<string, Item[]>()
486
+ for (const item of items) {
487
+ const key = conflict(item.property)
488
+ const domain = result.get(key)
489
+ if (domain) domain.push(item)
490
+ else result.set(key, [item])
491
+ }
492
+ return result
493
+ }
494
+
409
495
  function representation(style: Style.NamedStyle): unknown {
410
496
  return [
411
497
  outputMode(style),
@@ -444,8 +530,48 @@ export function compile<
444
530
  occurrences.set(key, (occurrences.get(key) ?? 0) + 1)
445
531
  }
446
532
 
533
+ // An ordered atomic style reuses the rules of the previous style that set a
534
+ // domain to the same sequence: no differing rule sits between them.
535
+ const runs = new Map<number, Map<string, number>>()
536
+ const owned = new Map<number, Map<string, string[]>>()
537
+ if (!options.development) {
538
+ const last = new Map<string, { key: string; owner: number }>()
539
+
540
+ for (const [index, style] of options.styles.styles.entries()) {
541
+ if (isolated(style)) continue
542
+
543
+ const items =
544
+ (outputMode(style) ?? defaultMode) === 'atomic' &&
545
+ options.names?.[style.name] === undefined
546
+ ? flatten(style)
547
+ : undefined
548
+ // Grouped, explicit, and opaque rules may set any domain.
549
+ if (!items) {
550
+ last.clear()
551
+ continue
552
+ }
553
+
554
+ for (const [domain, list] of byDomain(items)) {
555
+ const key = JSON.stringify(
556
+ list.map((item) => [item.conditions, item.value]),
557
+ )
558
+ const previous = last.get(domain)
559
+ if (previous?.key !== key) {
560
+ last.set(domain, { key, owner: index })
561
+ continue
562
+ }
563
+
564
+ const reuse = runs.get(index) ?? new Map<string, number>()
565
+ reuse.set(domain, previous.owner)
566
+ runs.set(index, reuse)
567
+ }
568
+ }
569
+ }
570
+
447
571
  for (const [styleIndex, style] of options.styles.styles.entries()) {
448
572
  const mode = outputMode(style) ?? defaultMode
573
+ const independent =
574
+ compositionMode(style) === 'independent' && !options.development
449
575
  if (!style.name || Object.hasOwn(classes, style.name)) {
450
576
  diagnostics.push({
451
577
  code: 'invalid_name',
@@ -473,9 +599,7 @@ export function compile<
473
599
  }
474
600
 
475
601
  const application =
476
- explicit === undefined &&
477
- options.composition === 'independent' &&
478
- !options.development
602
+ explicit === undefined && independent
479
603
  ? `${mode}:${style.rules ? JSON.stringify(representation(style)) : ''}:${nested(canonicalStyles[styleIndex]!)}`
480
604
  : undefined
481
605
  if (application !== undefined && applications.has(application)) {
@@ -501,19 +625,16 @@ export function compile<
501
625
  shared: boolean,
502
626
  output = mode,
503
627
  conditional?: ConditionalRule,
504
- ) {
505
- if (!body) return
506
-
507
- const independent =
508
- mode === 'grouped' &&
509
- !options.development &&
510
- output === mode &&
511
- options.composition === 'independent'
512
- const reusable = explicit === undefined && (shared || independent)
628
+ ): string | undefined {
629
+ if (!body) return undefined
630
+
631
+ const reusable =
632
+ explicit === undefined &&
633
+ (shared || (independent && mode === 'grouped' && output === mode))
513
634
  const previous = reusable ? sharedRules![output].get(body) : undefined
514
635
  if (previous && !names.includes(previous)) {
515
636
  names.push(previous)
516
- return
637
+ return previous
517
638
  }
518
639
 
519
640
  if (previous) shared = false
@@ -562,6 +683,8 @@ export function compile<
562
683
  if (reusable) sharedRules![output].set(body, identity)
563
684
  names.push(identity)
564
685
  }
686
+
687
+ return identity
565
688
  }
566
689
 
567
690
  function atoms(
@@ -605,6 +728,16 @@ export function compile<
605
728
  while (style.declarations[index]?.property === property)
606
729
  values.push(style.declarations[index++]!)
607
730
 
731
+ const domain = conflict(property)
732
+ const owner = runs.get(styleIndex)?.get(domain)
733
+ const reused =
734
+ owner === undefined ? undefined : owned.get(owner)?.get(domain)
735
+ if (reused) {
736
+ for (const identity of reused)
737
+ if (!names.includes(identity)) names.push(identity)
738
+ continue
739
+ }
740
+
608
741
  const value = values
609
742
  .map(
610
743
  ({ important, value }) =>
@@ -618,8 +751,54 @@ export function compile<
618
751
  const shared =
619
752
  !nestedComposition &&
620
753
  !conditions.length &&
621
- groups.get(conflict(property)) !== false
622
- emit(body, property, shared, mode, { conditions, value })
754
+ ordered.get(domain) !== false
755
+ const identity = emit(body, property, shared, mode, {
756
+ conditions,
757
+ value,
758
+ })
759
+ if (identity === undefined) continue
760
+
761
+ const domains = owned.get(styleIndex) ?? new Map<string, string[]>()
762
+ domains.set(domain, [...(domains.get(domain) ?? []), identity])
763
+ owned.set(styleIndex, domains)
764
+ }
765
+ }
766
+
767
+ // Independent class lists never combine, so only declarations within one
768
+ // conflict domain need relative order. Equal domain bodies share one rule.
769
+ function share(style: Style.NamedStyle) {
770
+ const items = flatten(style)
771
+ if (!items) return atoms(style)
772
+
773
+ for (const domain of byDomain(items).values()) {
774
+ // Conditional rules stay contextual to keep their order against base declarations.
775
+ if (domain.some((item) => item.conditions.length)) {
776
+ for (const item of domain)
777
+ emit(
778
+ item.conditions.reduceRight(
779
+ (body, condition) => `${condition}{${body}}`,
780
+ item.value,
781
+ ),
782
+ item.property,
783
+ false,
784
+ mode,
785
+ item,
786
+ )
787
+ continue
788
+ }
789
+
790
+ const body = domain.map((item) => item.value).join('')
791
+ const identity = units.name(body, cssNamespace)
792
+ if (rules.has(identity) && rules.get(identity) !== body)
793
+ diagnostics.push({
794
+ code: 'identity_collision',
795
+ message:
796
+ 'Generated class names conflict. Supply distinct config or style ids.',
797
+ path: [style.name],
798
+ })
799
+
800
+ rules.set(identity, body)
801
+ names.push(identity)
623
802
  }
624
803
  }
625
804
 
@@ -631,7 +810,7 @@ export function compile<
631
810
  const key = conflict(declaration.property)
632
811
  const reusable =
633
812
  !style.rules &&
634
- options.composition === 'independent' &&
813
+ independent &&
635
814
  groups.get(key) !== false &&
636
815
  (occurrences.get(key) ?? 0) > 1
637
816
  if (reusable) shared.push(declaration)
@@ -641,7 +820,8 @@ export function compile<
641
820
  emit(shared.map(declarationBody).join(''), 'shared', false)
642
821
  emit(local.map(declarationBody).join(''), 'style', false)
643
822
  } else emit(nested(style), 'style', false)
644
- } else atoms(style)
823
+ } else if (explicit === undefined && independent) share(style)
824
+ else atoms(style)
645
825
  } catch (error) {
646
826
  diagnostics.push({
647
827
  code: 'invalid_declaration',
@@ -730,6 +910,7 @@ export function compile<
730
910
  const result = {
731
911
  ...(scopes.resources.length ? { [Themes.shared]: scopes.resources } : {}),
732
912
  ...(contributionCss ? { contributionCss, scopedCss } : {}),
913
+ [ClassName.units]: units.used,
733
914
  [ClassName.rules]: Object.freeze({
734
915
  ...Object.fromEntries(rules),
735
916
  ...Object.fromEntries(
@@ -744,6 +925,7 @@ export function compile<
744
925
  vars: scopes.classes as Readonly<Record<themeName, string>>,
745
926
  }
746
927
  Object.defineProperty(result, ClassName.rules, { enumerable: false })
928
+ Object.defineProperty(result, ClassName.units, { enumerable: false })
747
929
  return Object.freeze(result)
748
930
  }
749
931
 
@@ -761,18 +943,20 @@ export declare namespace compile {
761
943
  readonly [Themes.shared]?: 'all' | 'defaults' | undefined
762
944
  /** Readable source bindings and consumer-owned configuration namespaces. */
763
945
  readonly [ClassName.labels]?: ClassName.Labels | undefined
946
+ /** Names shared independent atomic rules, with one name table across a graph's modules. */
947
+ readonly [ClassName.units]?: ClassName.Units | undefined
764
948
  /** Fixed class identities used by CSS-only consumers. */
765
949
  readonly names?: Readonly<Record<string, string>> | undefined
766
950
  /**
767
- * Defaults to ordered, preserving stylesheet precedence across combined class lists.
768
- * Independent deduplicates complete applications; its class lists must not be
769
- * combined with each other. Resolve composition before compiling in this mode.
951
+ * Ordered by default, preserving precedence across combined class lists.
952
+ * Independent shares applications and atomic declarations, so resolve
953
+ * composition first and never combine its lists.
770
954
  */
955
+ readonly composition?: 'independent' | 'ordered' | undefined
771
956
  /** Eager module-level stylesheet contributions, supplied as static data. */
772
957
  readonly contributions?: readonly Contribution[] | undefined
773
958
  /** CSS representation; atomic declarations are the default. */
774
959
  readonly cssOutput?: 'atomic' | 'grouped' | undefined
775
- readonly composition?: 'independent' | 'ordered' | undefined
776
960
  /** Retains live definitions for development updates. */
777
961
  readonly development?: boolean | undefined
778
962
  /**
@@ -797,6 +981,8 @@ export declare namespace compile {
797
981
  > = {
798
982
  /** Exact emitted rule bodies retained for graph-wide collision diagnostics. */
799
983
  readonly [ClassName.rules]: Readonly<Record<string, string>>
984
+ /** Shared rule names chosen by this compilation, for graph cache validation. */
985
+ readonly [ClassName.units]: ClassName.Units['used']
800
986
  /** Shared token resources omitted from this module's stylesheet. */
801
987
  readonly [Themes.shared]?: readonly Themes.Resource[] | undefined
802
988
  /** Contribution text separated for graph-wide hoisting. */