zyzz 0.0.24 → 0.0.26

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 (106) 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/Vars.d.ts +18 -2
  5. package/dist/Vars.d.ts.map +1 -1
  6. package/dist/Vars.js +21 -3
  7. package/dist/Vars.js.map +1 -1
  8. package/dist/compiler/Graph.d.ts +3 -0
  9. package/dist/compiler/Graph.d.ts.map +1 -1
  10. package/dist/compiler/Graph.js +38 -2
  11. package/dist/compiler/Graph.js.map +1 -1
  12. package/dist/compiler/Source.d.ts.map +1 -1
  13. package/dist/compiler/Source.js +11 -3
  14. package/dist/compiler/Source.js.map +1 -1
  15. package/dist/compiler/Transform.d.ts +4 -0
  16. package/dist/compiler/Transform.d.ts.map +1 -1
  17. package/dist/compiler/Transform.js +79 -15
  18. package/dist/compiler/Transform.js.map +1 -1
  19. package/dist/compiler/internal/Applications.d.ts +20 -0
  20. package/dist/compiler/internal/Applications.d.ts.map +1 -1
  21. package/dist/compiler/internal/Applications.js +151 -47
  22. package/dist/compiler/internal/Applications.js.map +1 -1
  23. package/dist/compiler/internal/Compositions.d.ts.map +1 -1
  24. package/dist/compiler/internal/Compositions.js +6 -0
  25. package/dist/compiler/internal/Compositions.js.map +1 -1
  26. package/dist/compiler/internal/Configurations.d.ts +3 -0
  27. package/dist/compiler/internal/Configurations.d.ts.map +1 -1
  28. package/dist/compiler/internal/Configurations.js +6 -1
  29. package/dist/compiler/internal/Configurations.js.map +1 -1
  30. package/dist/compiler/internal/Contract.d.ts.map +1 -1
  31. package/dist/compiler/internal/Contract.js +34 -7
  32. package/dist/compiler/internal/Contract.js.map +1 -1
  33. package/dist/compiler/internal/PackedStyles.d.ts.map +1 -1
  34. package/dist/compiler/internal/PackedStyles.js +7 -0
  35. package/dist/compiler/internal/PackedStyles.js.map +1 -1
  36. package/dist/compiler/internal/Themes.d.ts.map +1 -1
  37. package/dist/compiler/internal/Themes.js +123 -35
  38. package/dist/compiler/internal/Themes.js.map +1 -1
  39. package/dist/internal/Configuration.d.ts +10 -0
  40. package/dist/internal/Configuration.d.ts.map +1 -1
  41. package/dist/internal/Configuration.js +13 -1
  42. package/dist/internal/Configuration.js.map +1 -1
  43. package/dist/internal/Token.d.ts +2 -0
  44. package/dist/internal/Token.d.ts.map +1 -1
  45. package/dist/internal/Token.js.map +1 -1
  46. package/dist/internal/VariableSets.d.ts +20 -4
  47. package/dist/internal/VariableSets.d.ts.map +1 -1
  48. package/dist/internal/VariableSets.js +70 -19
  49. package/dist/internal/VariableSets.js.map +1 -1
  50. package/dist/next/internal/Project.d.ts.map +1 -1
  51. package/dist/next/internal/Project.js +30 -25
  52. package/dist/next/internal/Project.js.map +1 -1
  53. package/dist/next/loader.js +2 -1
  54. package/dist/next/loader.js.map +1 -1
  55. package/dist/node/Host.d.ts.map +1 -1
  56. package/dist/node/Host.js +32 -23
  57. package/dist/node/Host.js.map +1 -1
  58. package/dist/node/internal/Contracts.d.ts +22 -0
  59. package/dist/node/internal/Contracts.d.ts.map +1 -0
  60. package/dist/node/internal/Contracts.js +287 -0
  61. package/dist/node/internal/Contracts.js.map +1 -0
  62. package/dist/react-native/internal/Tokens.d.ts.map +1 -1
  63. package/dist/react-native/internal/Tokens.js +3 -6
  64. package/dist/react-native/internal/Tokens.js.map +1 -1
  65. package/dist/unplugin/index.d.ts.map +1 -1
  66. package/dist/unplugin/index.js +31 -13
  67. package/dist/unplugin/index.js.map +1 -1
  68. package/dist/vite/index.d.ts.map +1 -1
  69. package/dist/vite/index.js +33 -44
  70. package/dist/vite/index.js.map +1 -1
  71. package/dist/web/Css.d.ts +10 -5
  72. package/dist/web/Css.d.ts.map +1 -1
  73. package/dist/web/Css.js +170 -31
  74. package/dist/web/Css.js.map +1 -1
  75. package/dist/web/internal/ClassName.d.ts +49 -0
  76. package/dist/web/internal/ClassName.d.ts.map +1 -1
  77. package/dist/web/internal/ClassName.js +57 -0
  78. package/dist/web/internal/ClassName.js.map +1 -1
  79. package/dist/web/internal/Themes.d.ts.map +1 -1
  80. package/dist/web/internal/Themes.js +4 -2
  81. package/dist/web/internal/Themes.js.map +1 -1
  82. package/package.json +1 -1
  83. package/src/Style.ts +2 -0
  84. package/src/Vars.ts +104 -12
  85. package/src/compiler/Graph.ts +42 -2
  86. package/src/compiler/Source.ts +11 -3
  87. package/src/compiler/Transform.ts +130 -29
  88. package/src/compiler/internal/Applications.ts +239 -63
  89. package/src/compiler/internal/Compositions.ts +9 -0
  90. package/src/compiler/internal/Configurations.ts +8 -1
  91. package/src/compiler/internal/Contract.ts +59 -7
  92. package/src/compiler/internal/PackedStyles.ts +9 -0
  93. package/src/compiler/internal/Themes.ts +174 -52
  94. package/src/internal/Configuration.ts +26 -4
  95. package/src/internal/Token.ts +2 -0
  96. package/src/internal/VariableSets.ts +104 -24
  97. package/src/next/internal/Project.ts +28 -23
  98. package/src/next/loader.ts +2 -1
  99. package/src/node/Host.ts +31 -24
  100. package/src/node/internal/Contracts.ts +384 -0
  101. package/src/react-native/internal/Tokens.ts +5 -6
  102. package/src/unplugin/index.ts +35 -13
  103. package/src/vite/index.ts +38 -45
  104. package/src/web/Css.ts +228 -42
  105. package/src/web/internal/ClassName.ts +114 -0
  106. package/src/web/internal/Themes.ts +12 -11
@@ -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 }
@@ -330,7 +334,10 @@ export function type(value: unknown): string {
330
334
  .join(';')}}`
331
335
  }
332
336
 
333
- function values(tree: Theme.References<Theme.Tokens>): Record<string, unknown> {
337
+ /** Reads the authored leaf values of a reference tree. */
338
+ export function values(
339
+ tree: Theme.References<Theme.Tokens>,
340
+ ): Record<string, unknown> {
334
341
  return Object.fromEntries(
335
342
  Object.entries(tree).map(([key, value]) => [
336
343
  key,
@@ -26,7 +26,7 @@ export function read(
26
26
  if (
27
27
  ![
28
28
  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
29
- 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
29
+ 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34,
30
30
  ].includes(data.version as number)
31
31
  )
32
32
  throw new Error('Unsupported Zyzz contract version.')
@@ -149,6 +149,22 @@ export function read(
149
149
  | 'grouped'
150
150
  | undefined
151
151
 
152
+ if (entry.composition !== undefined) {
153
+ if ((data.version as number) < 34)
154
+ throw new Error(
155
+ 'Composition modes require contract version 34 or later.',
156
+ )
157
+ if (
158
+ entry.composition !== 'independent' &&
159
+ entry.composition !== 'ordered'
160
+ )
161
+ throw new Error('Invalid packed composition mode.')
162
+ }
163
+ const composition = entry.composition as
164
+ | 'independent'
165
+ | 'ordered'
166
+ | undefined
167
+
152
168
  if (entry.defaultLayer !== undefined) {
153
169
  if ((data.version as number) < 27)
154
170
  throw new Error('Default layers require contract version 27 or later.')
@@ -180,6 +196,14 @@ export function read(
180
196
  'Conflicting packed shorthand mappings for one theme identity.',
181
197
  )
182
198
 
199
+ if (
200
+ contract &&
201
+ (contract.composition ?? 'ordered') !== (composition ?? 'ordered')
202
+ )
203
+ throw new Error(
204
+ 'Conflicting packed composition modes for one theme identity.',
205
+ )
206
+
183
207
  if (
184
208
  contract &&
185
209
  (contract.cssOutput ?? 'atomic') !== (cssOutput ?? 'atomic')
@@ -204,6 +228,7 @@ export function read(
204
228
  ...(entry.shorthands !== undefined
205
229
  ? { shorthands: Shorthands.read(entry.shorthands) }
206
230
  : {}),
231
+ ...(composition ? { composition } : {}),
207
232
  ...(cssOutput ? { cssOutput } : {}),
208
233
  ...(defaultLayer !== undefined ? { defaultLayer } : {}),
209
234
  [Token.complete]: true,
@@ -333,7 +358,7 @@ export function read(
333
358
  if (
334
359
  ![
335
360
  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
336
- 27, 28, 29, 30, 31, 32, 33,
361
+ 27, 28, 29, 30, 31, 32, 33, 34,
337
362
  ].includes(data.version as number) ||
338
363
  ![
339
364
  'cssFunction',
@@ -514,6 +539,14 @@ export function read(
514
539
  'Configuration CSS output disagrees with linked theme metadata.',
515
540
  )
516
541
 
542
+ if (
543
+ (options.composition ?? 'ordered') !==
544
+ (definition[Token.definition].contract.composition ?? 'ordered')
545
+ )
546
+ throw new Error(
547
+ 'Configuration composition disagrees with linked theme metadata.',
548
+ )
549
+
517
550
  if (
518
551
  options.defaultLayer !==
519
552
  definition[Token.definition].contract.defaultLayer
@@ -829,6 +862,9 @@ export function write(
829
862
  ...(theme[Token.definition].contract.defaultLayer !== undefined
830
863
  ? { defaultLayer: theme[Token.definition].contract.defaultLayer }
831
864
  : {}),
865
+ ...(theme[Token.definition].contract.composition
866
+ ? { composition: theme[Token.definition].contract.composition }
867
+ : {}),
832
868
  ...(theme[Token.definition].contract.cssOutput
833
869
  ? { cssOutput: theme[Token.definition].contract.cssOutput }
834
870
  : {}),
@@ -843,13 +879,29 @@ export function write(
843
879
  },
844
880
  ]),
845
881
  ),
846
- version: Object.values(links).some(nativeCatalog)
847
- ? 33
848
- : Object.values(links).some(nativeProvider)
849
- ? 32
850
- : 31,
882
+ version: (() => {
883
+ if (
884
+ Object.values(themes).some(
885
+ (theme) => theme[Token.definition].contract.composition,
886
+ ) ||
887
+ Object.values(links).some(composed)
888
+ )
889
+ return 34
890
+
891
+ if (Object.values(links).some(nativeCatalog)) return 33
892
+ if (Object.values(links).some(nativeProvider)) return 32
893
+
894
+ return 31
895
+ })(),
851
896
  })
852
897
 
898
+ function composed(link: Themes.Link): boolean {
899
+ return (
900
+ Boolean(link.style?.style.composition) ||
901
+ Object.values(link.members ?? {}).some(composed)
902
+ )
903
+ }
904
+
853
905
  function nativeProvider(link: Themes.Link): boolean {
854
906
  return (
855
907
  link.call.nativeProvider === true ||
@@ -160,7 +160,16 @@ export function read(
160
160
  item.cssOutput !== 'grouped'
161
161
  )
162
162
  throw new Error('Invalid packed style CSS output mode.')
163
+ if (
164
+ item.composition !== undefined &&
165
+ (version < 34 ||
166
+ (item.composition !== 'independent' && item.composition !== 'ordered'))
167
+ )
168
+ throw new Error('Invalid packed style composition mode.')
163
169
  return {
170
+ ...(item.composition
171
+ ? { composition: item.composition as 'independent' | 'ordered' }
172
+ : {}),
164
173
  ...(item.cssOutput
165
174
  ? { cssOutput: item.cssOutput as 'atomic' | 'grouped' }
166
175
  : {}),