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
@@ -9,6 +9,7 @@ import type * as Ast from '@oxc-project/types'
9
9
  import * as Css from '../web/Css.js'
10
10
  import * as ClassName from '../web/internal/ClassName.js'
11
11
  import * as Expression from './internal/Expression.js'
12
+ import * as Identity from '../internal/Identity.js'
12
13
  import MagicString from 'magic-string'
13
14
  import * as Mapping from '@jridgewell/gen-mapping'
14
15
  import * as Namespaces from './internal/Namespaces.js'
@@ -183,6 +184,47 @@ export function compile(options: compile.Options): compile.ReturnType {
183
184
  },
184
185
  })
185
186
 
187
+ const found = localApplications?.find() ?? []
188
+ // Compositions supersede their inputs' classes, so those reads stay unfolded.
189
+ const composing = (application: Span) =>
190
+ extracted.calls.some(
191
+ (call) =>
192
+ (call.composition || call.runtimeComposition) &&
193
+ call.start < application.start &&
194
+ application.end <= call.end,
195
+ )
196
+ const folded = new Set(
197
+ found.filter(
198
+ (application) =>
199
+ !composing(application) &&
200
+ !extracted.calls.some(
201
+ (call) =>
202
+ call.output === 'html' &&
203
+ call.runtimeComposition?.some(
204
+ (input) => input.applicationStart === application.start,
205
+ ),
206
+ ),
207
+ ),
208
+ )
209
+
210
+ const local =
211
+ portable || options.development
212
+ ? undefined
213
+ : localApplications?.definitions()
214
+
215
+ // Styles applied only alone to DOM elements, or inside cx, never meet another
216
+ // style's classes. Atomic ones share rules like independent styles.
217
+ const standalone = new Set(
218
+ [...(local?.values.keys() ?? [])].filter((name) =>
219
+ found.every(
220
+ (application) =>
221
+ application.name !== name ||
222
+ application.terminal ||
223
+ composing(application),
224
+ ),
225
+ ),
226
+ )
227
+
186
228
  // Scheme rules accompany modules whose runtime helpers can apply a scheme class.
187
229
  const schemes =
188
230
  options.schemes ??
@@ -196,6 +238,12 @@ export function compile(options: compile.Options): compile.ReturnType {
196
238
  [ThemeRules.shared]: options[ThemeRules.shared],
197
239
  development: options.development,
198
240
  [ClassName.labels]: extracted.styles[ClassName.labels],
241
+ [ClassName.units]:
242
+ options[ClassName.units] ??
243
+ ClassName.registry().scope({
244
+ owner: options.moduleId,
245
+ qualifier: Identity.compact(options.moduleId).slice(-6),
246
+ }),
199
247
  composition: options.composition,
200
248
  cssOutput: options.cssOutput,
201
249
  names: portable ? portableNames : undefined,
@@ -206,14 +254,23 @@ export function compile(options: compile.Options): compile.ReturnType {
206
254
  if (portable || options.development) return extracted.styles
207
255
 
208
256
  const dead = localApplications?.dead()
209
- if (!dead?.size) return extracted.styles
257
+ if (!dead?.size && !standalone.size) return extracted.styles
210
258
 
211
259
  return {
212
- styles: extracted.styles.styles.map((style) =>
213
- dead.has(style.name)
214
- ? { cssOutput: style.cssOutput, declarations: [], name: style.name }
215
- : style,
216
- ),
260
+ styles: extracted.styles.styles.map((style) => {
261
+ if (dead?.has(style.name))
262
+ return {
263
+ cssOutput: style.cssOutput,
264
+ declarations: [],
265
+ name: style.name,
266
+ }
267
+
268
+ // Grouped output keeps one class per style.
269
+ return standalone.has(style.name) &&
270
+ (style.cssOutput ?? options.cssOutput ?? 'atomic') === 'atomic'
271
+ ? { ...style, composition: 'independent' as const }
272
+ : style
273
+ }),
217
274
  }
218
275
  })(),
219
276
  contributions: extracted.contributions,
@@ -319,6 +376,35 @@ export function compile(options: compile.Options): compile.ReturnType {
319
376
  ),
320
377
  )
321
378
 
379
+ // Definitions read only through folded applications need no class list of
380
+ // their own. True marks definitions whose every read follows initialization.
381
+ const unread = new Map(
382
+ [...(local?.values ?? [])].filter(([name]) =>
383
+ found.every(
384
+ (application) => application.name !== name || folded.has(application),
385
+ ),
386
+ ),
387
+ )
388
+ const namespaces = (local?.namespaces ?? []).filter((namespace) =>
389
+ [...namespace.ends.keys()].every((name) => unread.get(name)),
390
+ )
391
+
392
+ const erased = new Map(namespaces.flatMap((namespace) => [...namespace.ends]))
393
+
394
+ // An ambient namespace keeps member types without runtime initialization.
395
+ for (const namespace of namespaces)
396
+ module.prependRight(namespace.start, 'declare ')
397
+
398
+ for (const call of extracted.calls) {
399
+ const end = erased.get(call.name)
400
+ if (end !== undefined)
401
+ module.overwrite(
402
+ end,
403
+ call.start,
404
+ `: import('zyzz').style.ReturnType${call.output === 'html' ? "<'html'>" : ''}`,
405
+ )
406
+ }
407
+
322
408
  let callable = false
323
409
 
324
410
  const composed = (node: Span) =>
@@ -438,6 +524,17 @@ export function compile(options: compile.Options): compile.ReturnType {
438
524
  return typed ? `(${result} as ${type})` : result
439
525
  }
440
526
 
527
+ const initialized = unread.get(call.name)
528
+ if (initialized !== undefined) {
529
+ if (erased.has(call.name)) return ''
530
+
531
+ // Guards read only truthiness, and failures before initialization still call it.
532
+ const value = initialized ? 'void 0' : '1'
533
+ return /\.[cm]?tsx?$/.test(options.moduleId)
534
+ ? `(${value} as unknown as import('zyzz').style.ReturnType${call.output === 'html' ? "<'html'>" : ''})`
535
+ : value
536
+ }
537
+
441
538
  if (application.folded)
442
539
  return composeHtml
443
540
  ? `${compositionHtml}.from({className:${JSON.stringify(classes[call.name])}})`
@@ -463,31 +560,13 @@ export function compile(options: compile.Options): compile.ReturnType {
463
560
  !call.slots &&
464
561
  !call.runtimeComposition &&
465
562
  !application.folded &&
563
+ !unread.has(call.name) &&
466
564
  call.output !== 'html'
467
565
  )
468
566
  callable = true
469
567
  }
470
568
 
471
- for (const application of localApplications?.find() ?? []) {
472
- if (
473
- extracted.calls.some(
474
- (call) =>
475
- call.output === 'html' &&
476
- call.runtimeComposition?.some(
477
- (input) => input.applicationStart === application.start,
478
- ),
479
- )
480
- )
481
- continue
482
- if (
483
- extracted.calls.some(
484
- (call) =>
485
- (call.composition || call.runtimeComposition) &&
486
- call.start < application.start &&
487
- application.end <= call.end,
488
- )
489
- )
490
- continue
569
+ for (const application of folded) {
491
570
  const className = JSON.stringify(classes[application.name])
492
571
 
493
572
  const key =
@@ -495,12 +574,18 @@ export function compile(options: compile.Options): compile.ReturnType {
495
574
  'html'
496
575
  ? 'class'
497
576
  : 'className'
577
+ const callee = options.source.slice(
578
+ application.start,
579
+ application.calleeEnd,
580
+ )
498
581
 
499
- // Keep a callable guard so bundlers also retain failures before initialization.
500
582
  module.overwrite(
501
583
  application.start,
502
584
  application.end,
503
- `(${options.source.slice(application.start, application.calleeEnd)}?{${key}:${className}}:${options.source.slice(application.start, application.calleeEnd)}())`,
585
+ application.initialized
586
+ ? `({${key}:${className}})`
587
+ : // Keep a callable guard so bundlers also retain failures before initialization.
588
+ `(${callee}?{${key}:${className}}:${callee}())`,
504
589
  )
505
590
  }
506
591
 
@@ -688,7 +773,17 @@ export function compile(options: compile.Options): compile.ReturnType {
688
773
  specifier.importKind === 'type' ||
689
774
  !(
690
775
  node.source.value === 'zyzz'
691
- ? ['Config', 'cx', 'style', 'Vars', 'variable', 'variants']
776
+ ? [
777
+ 'Config',
778
+ 'cx',
779
+ 'defineConfig',
780
+ 'defineVars',
781
+ 'extendVars',
782
+ 'style',
783
+ 'Vars',
784
+ 'variable',
785
+ 'variants',
786
+ ]
692
787
  : [
693
788
  'Css',
694
789
  'cssFunction',
@@ -1135,6 +1230,7 @@ export function compile(options: compile.Options): compile.ReturnType {
1135
1230
  ? { [ThemeRules.shared]: emitted[ThemeRules.shared] }
1136
1231
  : {}),
1137
1232
  [ClassName.rules]: emitted[ClassName.rules],
1233
+ [ClassName.units]: emitted[ClassName.units],
1138
1234
  classes,
1139
1235
  code: portable ? options.source : module.toString(),
1140
1236
  css: namespaced.css,
@@ -1150,6 +1246,7 @@ export function compile(options: compile.Options): compile.ReturnType {
1150
1246
  vars: emitted.vars,
1151
1247
  }
1152
1248
  Object.defineProperty(result, ClassName.rules, { enumerable: false })
1249
+ Object.defineProperty(result, ClassName.units, { enumerable: false })
1153
1250
  return Object.freeze(result)
1154
1251
  }
1155
1252
 
@@ -1161,6 +1258,8 @@ export declare namespace compile {
1161
1258
  type Options = Source.extract.Options & {
1162
1259
  /** Separates generated token definitions for independently loaded modules. */
1163
1260
  readonly [ThemeRules.shared]?: 'all' | 'defaults' | undefined
1261
+ /** Names shared independent atomic rules, with one name table across a graph's modules. */
1262
+ readonly [ClassName.units]?: ClassName.Units | undefined
1164
1263
  /** Disable source rewriting while emitting CSS for runtime authoring. Defaults to true. */
1165
1264
  readonly compiler?: boolean | undefined
1166
1265
  /** Whether compiled applications can be combined with one another. */
@@ -1188,6 +1287,8 @@ export declare namespace compile {
1188
1287
  readonly code: string
1189
1288
  /** Exact emitted rules retained for collision diagnostics. */
1190
1289
  readonly [ClassName.rules]: Readonly<Record<string, string>>
1290
+ /** Shared rule names chosen by this module, for graph cache validation. */
1291
+ readonly [ClassName.units]?: ClassName.Units['used'] | undefined
1191
1292
  /** Ordered, unminified stylesheet text. */
1192
1293
  readonly css: string
1193
1294
  /** Standard stylesheet map with authored selector/declaration locations. */
@@ -34,6 +34,10 @@ export function create(
34
34
  }
35
35
  direct: Source.Call | undefined
36
36
  members: Map<string, Source.Call>
37
+ /** Namespace statement start and member identifier ends by extracted name. */
38
+ namespace?: Namespace | undefined
39
+ /** Offset after which top-level reads observe an initialized binding. */
40
+ ready: number
37
41
  }[] = []
38
42
 
39
43
  for (const statement of program.body) {
@@ -43,6 +47,7 @@ export function create(
43
47
  statement.body?.type === 'TSModuleBlock'
44
48
  ) {
45
49
  const members = new Map<string, Source.Call>()
50
+ const ends = new Map<string, number>()
46
51
  let valid = true
47
52
 
48
53
  for (const member of statement.body.body) {
@@ -71,6 +76,7 @@ export function create(
71
76
  }
72
77
 
73
78
  members.set(declaration.id.name, call)
79
+ ends.set(call.name, declaration.id.end)
74
80
  memberBindings.set(declaration.id.start, call.name)
75
81
  }
76
82
  }
@@ -80,6 +86,8 @@ export function create(
80
86
  declaration: { id: statement.id },
81
87
  direct: undefined,
82
88
  members,
89
+ namespace: { ends, start: statement.start },
90
+ ready: statement.end,
83
91
  })
84
92
 
85
93
  continue
@@ -142,6 +150,7 @@ export function create(
142
150
  declaration: { ...declaration, id: declaration.id },
143
151
  direct,
144
152
  members,
153
+ ready: declaration.end,
145
154
  })
146
155
  }
147
156
  }
@@ -149,6 +158,9 @@ export function create(
149
158
  if (!candidates.length) return undefined
150
159
 
151
160
  const lexical = new Set<Ast.Node>()
161
+ // Reads inside functions, classes, or namespaces may run before initialization.
162
+ const deferred = new Set<Ast.Node>()
163
+ const terminals = new Set<Ast.Node>()
152
164
  const scope = new Scope.Tracker({ preserveExitedScopes: true })
153
165
  Walker.walk(program, { scopeTracker: scope })
154
166
  scope.freeze()
@@ -185,8 +197,23 @@ export function create(
185
197
  candidates.some(
186
198
  (candidate) => candidate.declaration.id.start === id.start,
187
199
  )
188
- )
200
+ ) {
189
201
  lexical.add(node)
202
+ if (terminal(ancestors)) terminals.add(node)
203
+ if (
204
+ ancestors.some((node) =>
205
+ [
206
+ 'ArrowFunctionExpression',
207
+ 'ClassDeclaration',
208
+ 'ClassExpression',
209
+ 'FunctionDeclaration',
210
+ 'FunctionExpression',
211
+ 'TSModuleDeclaration',
212
+ ].includes(node.type),
213
+ )
214
+ )
215
+ deferred.add(node)
216
+ }
190
217
  },
191
218
  leave() {
192
219
  ancestors.pop()
@@ -200,6 +227,80 @@ export function create(
200
227
  >(candidates.map(({ declaration }) => [declaration.id.name, []]))
201
228
  let evaluation = false
202
229
 
230
+ // Returns undefined when any read escapes. Recipes, dynamic styles, and
231
+ // applications with overrides keep their runtime calls.
232
+ function collect(candidate: (typeof candidates)[number]):
233
+ | {
234
+ readonly applications: readonly Application[]
235
+ readonly called: ReadonlySet<string>
236
+ }
237
+ | undefined {
238
+ const { declaration, direct, members } = candidate
239
+ const applications: Application[] = []
240
+ const called = new Set<string>()
241
+
242
+ for (const reference of references.get(declaration.id.name) ?? []) {
243
+ if (reference === declaration.id) continue
244
+
245
+ let callee: Ast.Node = reference
246
+ let call = direct
247
+
248
+ if (!direct) {
249
+ const member = parents.get(reference)
250
+
251
+ if (
252
+ member?.type !== 'MemberExpression' ||
253
+ member.object !== reference ||
254
+ member.computed ||
255
+ member.optional ||
256
+ member.property.type !== 'Identifier'
257
+ )
258
+ return undefined
259
+
260
+ call = members.get(member.property.name)
261
+ callee = member
262
+ }
263
+
264
+ if (
265
+ call?.composition ||
266
+ call?.runtimeComposition ||
267
+ (!options.dynamic && (call?.slots || call?.recipe))
268
+ )
269
+ continue
270
+
271
+ const application = parents.get(callee)
272
+
273
+ if (
274
+ !call ||
275
+ application?.type !== 'CallExpression' ||
276
+ application.callee !== callee ||
277
+ application.optional
278
+ )
279
+ return undefined
280
+
281
+ // An override needs the runtime callable, but exposes no other definition.
282
+ if (!options.dynamic && application.arguments.length) {
283
+ called.add(call.name)
284
+ continue
285
+ }
286
+
287
+ applications.push({
288
+ calleeEnd: callee.end,
289
+ end: application.end,
290
+ initialized:
291
+ !deferred.has(reference) &&
292
+ reference.start >= candidate.ready &&
293
+ (!candidate.namespace ||
294
+ namespaceCounts.get(declaration.id.name) === 1),
295
+ name: call.name,
296
+ start: application.start,
297
+ terminal: terminals.has(reference),
298
+ })
299
+ }
300
+
301
+ return { applications, called }
302
+ }
303
+
203
304
  return {
204
305
  dead() {
205
306
  if (evaluation) return new Set<string>()
@@ -255,6 +356,58 @@ export function create(
255
356
 
256
357
  return result
257
358
  },
359
+ definitions() {
360
+ const namespaces: Namespace[] = []
361
+ const values = new Map<string, boolean>()
362
+
363
+ if (evaluation) return { namespaces, values }
364
+
365
+ const counts = new Map<string, number>()
366
+
367
+ for (const { declaration } of candidates)
368
+ counts.set(
369
+ declaration.id.name,
370
+ (counts.get(declaration.id.name) ?? 0) + 1,
371
+ )
372
+
373
+ for (const candidate of candidates) {
374
+ const name = candidate.declaration.id.name
375
+ if (counts.get(name)! > 1 || (namespaceCounts.get(name) ?? 0) > 1)
376
+ continue
377
+
378
+ const result = collect(candidate)
379
+ if (!result) continue
380
+
381
+ let erased = !!candidate.namespace
382
+
383
+ for (const call of candidate.direct
384
+ ? [candidate.direct]
385
+ : candidate.members.values()) {
386
+ if (
387
+ call.composition ||
388
+ call.runtimeComposition ||
389
+ call.slots ||
390
+ call.recipe ||
391
+ result.called.has(call.name)
392
+ ) {
393
+ erased = false
394
+ continue
395
+ }
396
+
397
+ const initialized = result.applications.every(
398
+ (application) =>
399
+ application.name !== call.name || application.initialized,
400
+ )
401
+
402
+ values.set(call.name, initialized)
403
+ erased &&= initialized
404
+ }
405
+
406
+ if (erased) namespaces.push(candidate.namespace!)
407
+ }
408
+
409
+ return { namespaces, values }
410
+ },
258
411
  enter(node, parent) {
259
412
  if (
260
413
  node.type === 'MemberExpression' &&
@@ -280,68 +433,9 @@ export function create(
280
433
  find() {
281
434
  if (evaluation) return []
282
435
 
283
- const result: Application[] = []
284
-
285
- for (const { declaration, direct, members } of candidates) {
286
- const applications: Application[] = []
287
- let valid = true
288
-
289
- for (const reference of references.get(declaration.id.name) ?? []) {
290
- if (reference === declaration.id) continue
291
-
292
- let callee: Ast.Node = reference
293
- let call = direct
294
-
295
- if (!direct) {
296
- const member = parents.get(reference)
297
-
298
- if (
299
- member?.type !== 'MemberExpression' ||
300
- member.object !== reference ||
301
- member.computed ||
302
- member.optional ||
303
- member.property.type !== 'Identifier'
304
- ) {
305
- valid = false
306
- break
307
- }
308
-
309
- call = members.get(member.property.name)
310
- callee = member
311
- }
312
-
313
- if (
314
- call?.composition ||
315
- call?.runtimeComposition ||
316
- (!options.dynamic && (call?.slots || call?.recipe))
317
- )
318
- continue
319
-
320
- const application = parents.get(callee)
321
-
322
- if (
323
- !call ||
324
- application?.type !== 'CallExpression' ||
325
- application.callee !== callee ||
326
- application.optional ||
327
- (!options.dynamic && application.arguments.length)
328
- ) {
329
- valid = false
330
- break
331
- }
332
-
333
- applications.push({
334
- calleeEnd: callee.end,
335
- end: application.end,
336
- name: call.name,
337
- start: application.start,
338
- })
339
- }
340
-
341
- if (valid) result.push(...applications)
342
- }
343
-
344
- return result
436
+ return candidates.flatMap(
437
+ (candidate) => collect(candidate)?.applications ?? [],
438
+ )
345
439
  },
346
440
  }
347
441
  }
@@ -352,16 +446,37 @@ type Application = {
352
446
  readonly calleeEnd: number
353
447
  /** End of the application. */
354
448
  readonly end: number
449
+ /** Whether the read runs at top level after its definition initializes. */
450
+ readonly initialized: boolean
355
451
  /** Extracted class identity. */
356
452
  readonly name: string
357
453
  /** Start of the application. */
358
454
  readonly start: number
455
+ /** Whether a DOM element receives the props alone, so its classes meet no other style's. */
456
+ readonly terminal: boolean
457
+ }
458
+
459
+ /** A top-level namespace whose members are all static definitions. */
460
+ type Namespace = {
461
+ /** Member identifier ends keyed by extracted name. */
462
+ readonly ends: ReadonlyMap<string, number>
463
+ /** Start of the namespace statement. */
464
+ readonly start: number
359
465
  }
360
466
 
361
467
  /** Binding references gathered during the transform's existing walk. */
362
468
  type Collector = {
363
469
  /** Definitions with no observable value reads, retaining escaped containers. */
364
470
  dead: () => ReadonlySet<string>
471
+ /**
472
+ * Static definitions whose every read is a found application, keyed by
473
+ * extracted name with whether every read is initialized. Namespaces list
474
+ * those whose definitions all qualify with initialized reads.
475
+ */
476
+ definitions: () => {
477
+ readonly namespaces: readonly Namespace[]
478
+ readonly values: ReadonlyMap<string, boolean>
479
+ }
365
480
  /** Records relevant reads and their immediate parents. */
366
481
  enter: (node: Ast.Node, parent: Ast.Node | null | undefined) => void
367
482
  /** Resolves applications after traversal completes. */
@@ -376,3 +491,64 @@ export declare namespace create {
376
491
  readonly dynamic?: boolean | undefined
377
492
  }
378
493
  }
494
+
495
+ // Matches a no-argument application spread onto, or read as the class of, a DOM element.
496
+ function terminal(path: readonly Ast.Node[]): boolean {
497
+ let index = path.length - 1
498
+ let node = path[index]
499
+ const member = path[index - 1]
500
+ if (member?.type === 'MemberExpression' && member.object === node)
501
+ node = path[--index]
502
+
503
+ const call = path[--index]
504
+ if (
505
+ call?.type !== 'CallExpression' ||
506
+ call.callee !== node ||
507
+ call.arguments.length
508
+ )
509
+ return false
510
+
511
+ const parent = path[--index]
512
+ if (parent?.type === 'JSXSpreadAttribute')
513
+ return parent.argument === call && alone(path[index - 1], parent)
514
+
515
+ if (
516
+ parent?.type !== 'MemberExpression' ||
517
+ parent.object !== call ||
518
+ parent.computed ||
519
+ parent.property.type !== 'Identifier' ||
520
+ !/^class(?:Name)?$/.test(parent.property.name)
521
+ )
522
+ return false
523
+
524
+ const container = path[--index]
525
+ const attribute = path[--index]
526
+ return (
527
+ container?.type === 'JSXExpressionContainer' &&
528
+ container.expression === parent &&
529
+ attribute?.type === 'JSXAttribute' &&
530
+ attribute.value === container &&
531
+ attribute.name.type === 'JSXIdentifier' &&
532
+ /^class(?:Name)?$/.test(attribute.name.name) &&
533
+ alone(path[index - 1], attribute)
534
+ )
535
+ }
536
+
537
+ // Other spreads or class attributes could add another style's classes.
538
+ function alone(element: Ast.Node | undefined, own: Ast.Node): boolean {
539
+ return (
540
+ element?.type === 'JSXOpeningElement' &&
541
+ element.name.type === 'JSXIdentifier' &&
542
+ /^[a-z]/.test(element.name.name) &&
543
+ element.attributes.every(
544
+ (attribute) =>
545
+ attribute === own ||
546
+ (attribute.type === 'JSXAttribute' &&
547
+ !(
548
+ attribute.name.type === 'JSXIdentifier'
549
+ ? attribute.name.name
550
+ : attribute.name.namespace.name
551
+ ).startsWith('class')),
552
+ )
553
+ )
554
+ }
@@ -405,7 +405,15 @@ export function collect(options: collect.Options) {
405
405
  node,
406
406
  )
407
407
  const name = `composition-${options.identity}-${node.start}`
408
+ // A composition of independent styles keeps their sharing contract.
409
+ const independent = (calls: readonly Source.Call[]) =>
410
+ calls.every(
411
+ (call) => styles.get(call.name)!.composition === 'independent',
412
+ )
413
+ ? ({ composition: 'independent' } as const)
414
+ : {}
408
415
  const style: Style.NamedStyle = {
416
+ ...independent(selected),
409
417
  cssOutput: 'atomic',
410
418
  name,
411
419
  declarations: [],
@@ -580,6 +588,7 @@ export function collect(options: collect.Options) {
580
588
  .join(' '),
581
589
  },
582
590
  style: {
591
+ ...independent(included),
583
592
  cssOutput: 'atomic',
584
593
  name,
585
594
  declarations: [],
@@ -149,6 +149,9 @@ export function collect(options: collect.Options): Themes.Link {
149
149
  : {}),
150
150
  [Token.identity]: identity,
151
151
  ...(input.id !== undefined ? { [Token.complete]: true } : {}),
152
+ ...(input.composition !== undefined
153
+ ? { composition: input.composition }
154
+ : {}),
152
155
  cssOutput: input.cssOutput ?? 'atomic',
153
156
  ...(input.defaultLayer !== undefined
154
157
  ? { defaultLayer: input.defaultLayer }
@@ -243,6 +246,7 @@ export function collect(options: collect.Options): Themes.Link {
243
246
  return {}
244
247
  })(),
245
248
  ...(input.id !== undefined ? { id: input.id } : {}),
249
+ ...(input.composition ? { composition: input.composition } : {}),
246
250
  ...(input.cssOutput ? { cssOutput: input.cssOutput } : {}),
247
251
  ...(input.defaultLayer !== undefined
248
252
  ? { defaultLayer: input.defaultLayer }