zyzz 0.0.23 → 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 (119) 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 +10 -1
  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 +64 -27
  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/react-native/internal/Device.js +90 -37
  54. package/dist/react-native/internal/Device.js.map +3 -3
  55. package/dist/react-native/internal/NativeZyzz.js.map +2 -2
  56. package/dist/react-native/internal/Store.d.ts +4 -0
  57. package/dist/react-native/internal/Store.d.ts.map +1 -1
  58. package/dist/react-native/internal/Store.js.map +1 -1
  59. package/dist/react-native/internal/Viewport.d.ts +6 -0
  60. package/dist/react-native/internal/Viewport.d.ts.map +1 -1
  61. package/dist/react-native/internal/Viewport.js +2 -0
  62. package/dist/react-native/internal/Viewport.js.map +1 -1
  63. package/dist/react-native/react.d.ts +5 -2
  64. package/dist/react-native/react.d.ts.map +1 -1
  65. package/dist/react-native/react.js +60 -8
  66. package/dist/react-native/react.js.map +1 -1
  67. package/dist/runtime/NativeContext.d.ts +6 -0
  68. package/dist/runtime/NativeContext.d.ts.map +1 -1
  69. package/dist/runtime/NativeContext.js +62 -9
  70. package/dist/runtime/NativeContext.js.map +1 -1
  71. package/dist/unplugin/index.d.ts.map +1 -1
  72. package/dist/unplugin/index.js +35 -14
  73. package/dist/unplugin/index.js.map +1 -1
  74. package/dist/variants.d.ts +3 -2
  75. package/dist/variants.d.ts.map +1 -1
  76. package/dist/variants.js.map +1 -1
  77. package/dist/vite/index.d.ts.map +1 -1
  78. package/dist/vite/index.js +30 -37
  79. package/dist/vite/index.js.map +1 -1
  80. package/dist/web/Css.d.ts +10 -5
  81. package/dist/web/Css.d.ts.map +1 -1
  82. package/dist/web/Css.js +170 -31
  83. package/dist/web/Css.js.map +1 -1
  84. package/dist/web/internal/ClassName.d.ts +49 -0
  85. package/dist/web/internal/ClassName.d.ts.map +1 -1
  86. package/dist/web/internal/ClassName.js +57 -0
  87. package/dist/web/internal/ClassName.js.map +1 -1
  88. package/native/NativeZyzz.cpp +52 -65
  89. package/native/NativeZyzz.h +0 -4
  90. package/native/ZyzzSpecJSI.h +0 -10
  91. package/native/android/ZyzzSpec-generated.cpp +0 -6
  92. package/package.json +1 -1
  93. package/src/Style.ts +2 -0
  94. package/src/compiler/Graph.ts +42 -2
  95. package/src/compiler/Source.ts +11 -3
  96. package/src/compiler/Transform.ts +130 -29
  97. package/src/compiler/internal/Applications.ts +239 -63
  98. package/src/compiler/internal/Compositions.ts +9 -0
  99. package/src/compiler/internal/Configurations.ts +4 -0
  100. package/src/compiler/internal/Contract.ts +59 -7
  101. package/src/compiler/internal/PackedStyles.ts +9 -0
  102. package/src/compiler/internal/Themes.ts +25 -7
  103. package/src/internal/Configuration.ts +23 -1
  104. package/src/internal/Token.ts +2 -0
  105. package/src/next/internal/Project.ts +28 -23
  106. package/src/next/loader.ts +2 -1
  107. package/src/node/Host.ts +90 -36
  108. package/src/node/internal/Contracts.ts +384 -0
  109. package/src/react-native/internal/Device.ts +129 -48
  110. package/src/react-native/internal/NativeZyzz.ts +4 -6
  111. package/src/react-native/internal/Store.ts +4 -0
  112. package/src/react-native/internal/Viewport.ts +7 -0
  113. package/src/react-native/react.ts +81 -11
  114. package/src/runtime/NativeContext.ts +83 -9
  115. package/src/unplugin/index.ts +39 -14
  116. package/src/variants.ts +3 -2
  117. package/src/vite/index.ts +35 -36
  118. package/src/web/Css.ts +228 -42
  119. package/src/web/internal/ClassName.ts +114 -0
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. */
@@ -95,6 +95,120 @@ export function selector(value: string): string {
95
95
  /** Carries exact emitted rule bodies for graph-wide collision checks. */
96
96
  export const rules = Symbol('zyzz.css.rules')
97
97
 
98
+ /** Carries a compilation's unit naming scope in, and its naming decisions out. */
99
+ export const units = Symbol('zyzz.css.units')
100
+
101
+ /**
102
+ * Marks a graph compiled once with every module, whose stylesheets load together.
103
+ * Only its modules may reuse names that unrelated modules introduce.
104
+ */
105
+ export const complete = Symbol('zyzz.css.complete')
106
+
107
+ /** One compilation's unit names. */
108
+ export type Units = {
109
+ /** Names a declaration unit, reusing a visible earlier name for an equal body. */
110
+ readonly name: (body: string, namespace: string | undefined) => string
111
+ /** Names chosen so far, keyed by namespace and body, and whether each was reused. */
112
+ readonly used: ReadonlyMap<string, Decision>
113
+ }
114
+
115
+ /** A naming decision recorded so cached output can be validated against a later build. */
116
+ export type Decision = {
117
+ /** Whether the name was introduced by an earlier compilation. */
118
+ readonly borrowed: boolean
119
+ /** Emitted class name. */
120
+ readonly name: string
121
+ }
122
+
123
+ /** Shares unit names across the compilations of one graph. */
124
+ export function registry(): registry.ReturnType {
125
+ const names = new Map<string, { name: string; owner: string }>()
126
+
127
+ // A borrowed name stays tied to its first owner, so later modules can resolve it.
128
+ function borrow(key: string, visible?: (owner: string) => boolean) {
129
+ const previous = names.get(key)
130
+ if (previous && (visible?.(previous.owner) ?? true)) return previous.name
131
+ return undefined
132
+ }
133
+
134
+ return {
135
+ restore(options) {
136
+ for (const [key, decision] of options.used) {
137
+ const name = borrow(key, options.visible)
138
+ if ((name !== undefined) !== decision.borrowed) return false
139
+ if (name !== undefined && name !== decision.name) return false
140
+ }
141
+
142
+ for (const [key, decision] of options.used)
143
+ if (!names.has(key))
144
+ names.set(key, { name: decision.name, owner: options.owner })
145
+
146
+ return true
147
+ },
148
+ scope(options) {
149
+ const local = new Map<string, number>()
150
+ const used = new Map<string, Decision>()
151
+
152
+ return {
153
+ name(body, namespace) {
154
+ const key = `${namespace ?? ''}\n${body}`
155
+
156
+ // Every distinct body consumes a slot, so own names never depend on reuse.
157
+ let index = local.get(key)
158
+ if (index === undefined) {
159
+ index = local.size
160
+ local.set(key, index)
161
+ }
162
+
163
+ const borrowed = borrow(key, options.visible)
164
+ // The underscore keeps unit names apart from readable names and anonymous scopes.
165
+ const name =
166
+ borrowed ??
167
+ `z_${namespace ? `${namespace}-` : ''}${options.qualifier}${index.toString(36)}`
168
+
169
+ if (!names.has(key)) names.set(key, { name, owner: options.owner })
170
+ used.set(key, { borrowed: borrowed !== undefined, name })
171
+
172
+ return name
173
+ },
174
+ used,
175
+ }
176
+ },
177
+ }
178
+ }
179
+
180
+ /** Graph-wide unit naming contracts. */
181
+ export declare namespace registry {
182
+ /** Visibility and identity of one compilation in the shared name table. */
183
+ type Options = {
184
+ /** Compilation identity recorded on the names it introduces. */
185
+ readonly owner: string
186
+ /** Limits reuse to names introduced by these owners. Defaults to every owner. */
187
+ readonly visible?: ((owner: string) => boolean) | undefined
188
+ }
189
+
190
+ /** Creates per-compilation naming scopes over one shared name table. */
191
+ type ReturnType = {
192
+ /**
193
+ * Registers a cached compilation's names. Returns false, registering nothing,
194
+ * when compiling it now would choose different names.
195
+ */
196
+ readonly restore: (
197
+ options: Options & {
198
+ /** Decisions recorded by the cached compilation. */
199
+ readonly used: ReadonlyMap<string, Decision>
200
+ },
201
+ ) => boolean
202
+ /** Names units for one compilation, whose qualifier keeps new names distinct. */
203
+ readonly scope: (
204
+ options: Options & {
205
+ /** Fixed prefix distinguishing this compilation's new names. */
206
+ readonly qualifier: string
207
+ },
208
+ ) => Units
209
+ }
210
+ }
211
+
98
212
  /** Carries authored names between source extraction and CSS emission. */
99
213
  export const labels = Symbol('zyzz.css.labels')
100
214