zyzz 0.0.26 → 0.0.28

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 (49) hide show
  1. package/dist/compiler/Source.d.ts.map +1 -1
  2. package/dist/compiler/Source.js +165 -11
  3. package/dist/compiler/Source.js.map +1 -1
  4. package/dist/compiler/Transform.d.ts.map +1 -1
  5. package/dist/compiler/Transform.js +3 -0
  6. package/dist/compiler/Transform.js.map +1 -1
  7. package/dist/compiler/internal/Applications.d.ts.map +1 -1
  8. package/dist/compiler/internal/Applications.js +42 -1
  9. package/dist/compiler/internal/Applications.js.map +1 -1
  10. package/dist/compiler/internal/Selectors.d.ts +3 -2
  11. package/dist/compiler/internal/Selectors.d.ts.map +1 -1
  12. package/dist/compiler/internal/Selectors.js +23 -11
  13. package/dist/compiler/internal/Selectors.js.map +1 -1
  14. package/dist/internal/Authoring.d.ts.map +1 -1
  15. package/dist/internal/Authoring.js +37 -7
  16. package/dist/internal/Authoring.js.map +1 -1
  17. package/dist/internal/Configuration.d.ts +6 -1
  18. package/dist/internal/Configuration.d.ts.map +1 -1
  19. package/dist/internal/Configuration.js.map +1 -1
  20. package/dist/internal/Literal.d.ts +1 -1
  21. package/dist/internal/Literal.d.ts.map +1 -1
  22. package/dist/internal/Literal.js +3 -0
  23. package/dist/internal/Literal.js.map +1 -1
  24. package/dist/internal/Theme.d.ts +4 -0
  25. package/dist/internal/Theme.d.ts.map +1 -1
  26. package/dist/internal/Theme.js.map +1 -1
  27. package/dist/styleFunction.d.ts +11 -2
  28. package/dist/styleFunction.d.ts.map +1 -1
  29. package/dist/styleFunction.js +3 -1
  30. package/dist/styleFunction.js.map +1 -1
  31. package/dist/vite/index.d.ts +7 -0
  32. package/dist/vite/index.d.ts.map +1 -1
  33. package/dist/vite/index.js +173 -22
  34. package/dist/vite/index.js.map +1 -1
  35. package/dist/web/internal/Themes.d.ts.map +1 -1
  36. package/dist/web/internal/Themes.js +38 -2
  37. package/dist/web/internal/Themes.js.map +1 -1
  38. package/package.json +13 -1
  39. package/src/compiler/Source.ts +237 -13
  40. package/src/compiler/Transform.ts +4 -1
  41. package/src/compiler/internal/Applications.ts +57 -1
  42. package/src/compiler/internal/Selectors.ts +25 -11
  43. package/src/internal/Authoring.ts +53 -18
  44. package/src/internal/Configuration.ts +38 -1
  45. package/src/internal/Literal.ts +3 -0
  46. package/src/internal/Theme.ts +15 -0
  47. package/src/styleFunction.ts +31 -2
  48. package/src/vite/index.ts +242 -27
  49. package/src/web/internal/Themes.ts +47 -2
@@ -504,11 +504,21 @@ export function extract(options: extract.Options): extract.ReturnType {
504
504
  const opaque = new Set(variables.references.keys())
505
505
  const normalized = new Map<number, Ast.Node | Error>()
506
506
  const selectorKeys = new Set<number>()
507
+ const composed: Ast.Node[] = []
507
508
  for (const call of pending) {
508
509
  if (!call.arguments[0]) continue
509
510
  try {
510
511
  const value = staticData.normalize(call.arguments[0], staticCalls, opaque)
511
512
  normalized.set(call.start, value)
513
+ if (value.type === 'ArrayExpression')
514
+ for (const element of value.elements) {
515
+ const entry = element && Expression.unwrap(element)
516
+ if (
517
+ entry?.type === 'Identifier' ||
518
+ entry?.type === 'MemberExpression'
519
+ )
520
+ composed.push(entry)
521
+ }
512
522
  const body =
513
523
  value.type === 'ArrowFunctionExpression'
514
524
  ? staticData.normalize(value.body, staticCalls, opaque)
@@ -564,6 +574,7 @@ export function extract(options: extract.Options): extract.ReturnType {
564
574
  }),
565
575
  ),
566
576
  options.moduleId,
577
+ composed,
567
578
  )
568
579
  } catch (error) {
569
580
  if (!(error instanceof Themes.InvalidError)) throw error
@@ -573,6 +584,7 @@ export function extract(options: extract.Options): extract.ReturnType {
573
584
  }
574
585
  })()
575
586
 
587
+ const bodies = new Map<number, Ast.ObjectExpression>()
576
588
  const cssNameCounts = new Map<string, number>()
577
589
  for (const call of pending) {
578
590
  const name = authoredNames.get(call.start)!
@@ -686,10 +698,20 @@ export function extract(options: extract.Options): extract.ReturnType {
686
698
  }
687
699
  }
688
700
 
689
- if (call.arguments.length > 2 || argument?.type !== 'ObjectExpression') {
701
+ const entries =
702
+ argument?.type === 'ArrayExpression' &&
703
+ !dynamic &&
704
+ !recipes.has(call.start)
705
+ ? argument.elements
706
+ : undefined
707
+
708
+ if (
709
+ call.arguments.length > 2 ||
710
+ (argument?.type !== 'ObjectExpression' && !entries)
711
+ ) {
690
712
  report(
691
713
  'unsupported_syntax',
692
- 'Expected one literal object or typed callback; spreads and referenced definitions are not supported.',
714
+ 'Expected one literal object, typed callback, or array of style definitions and literal objects; spreads are not supported.',
693
715
  call,
694
716
  )
695
717
  continue
@@ -700,7 +722,7 @@ export function extract(options: extract.Options): extract.ReturnType {
700
722
  let recipeTypes: Call['recipeTypes']
701
723
  let staticRecipe: StaticRecipe.Definition<Ast.ObjectExpression> | undefined
702
724
  let dynamicRecipe: StaticRecipe.Definition<Ast.ObjectExpression> | undefined
703
- if (recipes.has(call.start)) {
725
+ if (recipes.has(call.start) && argument?.type === 'ObjectExpression') {
704
726
  try {
705
727
  if (dynamic)
706
728
  throw new Themes.InvalidError(
@@ -838,7 +860,18 @@ export function extract(options: extract.Options): extract.ReturnType {
838
860
  ) {
839
861
  report(
840
862
  'unsupported_syntax',
841
- 'Only explicit literal properties are supported; spreads, computed keys, shorthand, and methods are not evaluated.',
863
+ (() => {
864
+ const key =
865
+ property.type === 'Property' && property.computed
866
+ ? Expression.unwrap(property.key)
867
+ : undefined
868
+
869
+ // Template keys with an & target are selectors outside their group.
870
+ return key?.type === 'TemplateLiteral' &&
871
+ key.quasis.some((quasi) => quasi.value.raw.includes('&'))
872
+ ? 'Template selector keys require a selectors object.'
873
+ : 'Only explicit literal properties are supported; spreads, computed keys, shorthand, and methods are not evaluated.'
874
+ })(),
842
875
  property,
843
876
  )
844
877
  continue
@@ -1231,14 +1264,175 @@ export function extract(options: extract.Options): extract.ReturnType {
1231
1264
  return values
1232
1265
  }
1233
1266
 
1234
- const values = object(argument)
1267
+ // Each entry becomes an ordered declaration segment, so later entries win as authored declarations do.
1268
+ function compose(elements: Ast.ArrayExpression['elements']) {
1269
+ const parts: Style.NamedStyle[] = []
1270
+ const properties: Ast.ObjectExpression['properties'] = []
1271
+
1272
+ for (const element of elements) {
1273
+ const entry =
1274
+ element && element.type !== 'SpreadElement'
1275
+ ? Expression.unwrap(element)
1276
+ : undefined
1277
+
1278
+ if (entry?.type === 'ObjectExpression') {
1279
+ const values = object(entry)
1280
+ if (diagnostics.length !== before) continue
1281
+
1282
+ parts.push(
1283
+ define(
1284
+ { [name]: values },
1285
+ { locations, vars: themes?.styles.get(call.start)?.theme },
1286
+ ).styles[0]!,
1287
+ )
1288
+ properties.push(...entry.properties)
1289
+ continue
1290
+ }
1291
+
1292
+ const target = entry && selectors.compositions.get(entry.start)
1293
+ const local =
1294
+ target && 'type' in target
1295
+ ? calls.find((value) => value.start === target.start)
1296
+ : undefined
1297
+ const packed = target && !('type' in target) ? target.style : undefined
1298
+ const style = local
1299
+ ? styles.find((style) => style.name === local.name)
1300
+ : packed?.style
1301
+
1302
+ if (!style) {
1303
+ report(
1304
+ 'unsupported_syntax',
1305
+ 'Composition entries require literal objects or previously declared style definitions.',
1306
+ element ?? call,
1307
+ )
1308
+ continue
1309
+ }
1310
+
1311
+ if (
1312
+ local?.recipe ||
1313
+ local?.slots ||
1314
+ packed?.dynamic ||
1315
+ (packed?.staticRecipe &&
1316
+ !(
1317
+ packed.staticRecipe.rules.length === 1 &&
1318
+ packed.staticRecipe.rules[0]!.value.styles.length === 1 &&
1319
+ packed.staticRecipe.rules[0]!.value.styles[0]!.name ===
1320
+ packed.style.name
1321
+ ))
1322
+ ) {
1323
+ report(
1324
+ 'unsupported_syntax',
1325
+ 'Composition entries cannot be variants or dynamic styles; apply them with cx.',
1326
+ entry!,
1327
+ )
1328
+ continue
1329
+ }
1330
+
1331
+ // Native tables select variable alternatives from one context per call.
1332
+ if (
1333
+ options.target === 'native' &&
1334
+ !context(
1335
+ local
1336
+ ? themes?.nativeContext(local.start, local.end)
1337
+ : packed?.nativeContext,
1338
+ themes?.nativeContext(call.start, call.end),
1339
+ )
1340
+ ) {
1341
+ report(
1342
+ 'unsupported_syntax',
1343
+ 'Native composition entries require the variable catalog of the composing style.',
1344
+ entry!,
1345
+ )
1346
+ continue
1347
+ }
1348
+
1349
+ // The composing configuration decides output for every copied entry.
1350
+ parts.push(
1351
+ Object.fromEntries(
1352
+ Object.entries(style).filter(
1353
+ ([key]) => key !== 'composition' && key !== 'cssOutput',
1354
+ ),
1355
+ ) as Style.NamedStyle,
1356
+ )
1357
+
1358
+ if (local) {
1359
+ properties.push(...(bodies.get(local.start)?.properties ?? []))
1360
+ for (const [start, count] of local.typography ?? [])
1361
+ typography.set(start, count)
1362
+ for (const [start, count] of local.typographyQueries ?? [])
1363
+ typographyQueries.set(start, count)
1364
+ continue
1365
+ }
1366
+
1367
+ // Packed declarations have no authored source here, so they map to the entry.
1368
+ properties.push(
1369
+ ...declarations(style).map(
1370
+ (declaration): Ast.ObjectProperty => ({
1371
+ type: 'Property',
1372
+ computed: false,
1373
+ kind: 'init',
1374
+ method: false,
1375
+ shorthand: false,
1376
+ start: entry!.start,
1377
+ end: entry!.end,
1378
+ key: {
1379
+ type: 'Literal',
1380
+ value: declaration.property,
1381
+ raw: JSON.stringify(declaration.property),
1382
+ start: entry!.start,
1383
+ end: entry!.end,
1384
+ },
1385
+ value: {
1386
+ type: 'Literal',
1387
+ value: '',
1388
+ raw: '""',
1389
+ start: entry!.start,
1390
+ end: entry!.end,
1391
+ },
1392
+ }),
1393
+ ),
1394
+ )
1395
+ }
1396
+
1397
+ return {
1398
+ body: {
1399
+ type: 'ObjectExpression',
1400
+ start: call.start,
1401
+ end: call.end,
1402
+ properties,
1403
+ } satisfies Ast.ObjectExpression,
1404
+ definition: {
1405
+ styles: [
1406
+ Object.freeze<Style.NamedStyle>({
1407
+ name,
1408
+ declarations: Object.freeze([]),
1409
+ rules: Object.freeze(
1410
+ parts.map((style) => Object.freeze({ style })),
1411
+ ),
1412
+ }),
1413
+ ],
1414
+ } satisfies Style.Definition as Style.Definition,
1415
+ }
1416
+ }
1417
+
1418
+ const values = entries
1419
+ ? undefined
1420
+ : object(argument as Ast.ObjectExpression)
1235
1421
  if (diagnostics.length !== before) continue
1236
1422
 
1237
1423
  try {
1238
- const definition = define(
1239
- { [name]: values },
1240
- { locations, vars: themes?.styles.get(call.start)?.theme },
1241
- )
1424
+ const composite = entries && compose(entries)
1425
+ if (diagnostics.length !== before) continue
1426
+
1427
+ const definition =
1428
+ composite?.definition ??
1429
+ define(
1430
+ { [name]: values! },
1431
+ { locations, vars: themes?.styles.get(call.start)?.theme },
1432
+ )
1433
+
1434
+ const body = composite?.body ?? (argument as Ast.ObjectExpression)
1435
+ bodies.set(call.start, body)
1242
1436
 
1243
1437
  function validate(style: Style.NamedStyle, path: readonly string[]) {
1244
1438
  for (const declaration of style.declarations) {
@@ -1331,10 +1525,7 @@ export function extract(options: extract.Options): extract.ReturnType {
1331
1525
  ...(selectors.identities.has(call.start)
1332
1526
  ? { identity: selectors.identities.get(call.start)! }
1333
1527
  : {}),
1334
- ...(argument !== (dynamic?.body ?? original) &&
1335
- argument.type === 'ObjectExpression'
1336
- ? { body: argument }
1337
- : {}),
1528
+ ...(body !== (dynamic?.body ?? original) ? { body } : {}),
1338
1529
  ...(shorthands ? { shorthands } : {}),
1339
1530
  ...(typography.size ? { typography } : {}),
1340
1531
  ...(typographyQueries.size ? { typographyQueries } : {}),
@@ -1357,6 +1548,7 @@ export function extract(options: extract.Options): extract.ReturnType {
1357
1548
  if (
1358
1549
  dynamic ||
1359
1550
  recipe ||
1551
+ entries?.length === 0 ||
1360
1552
  (!definition.styles[0]!.declarations.length &&
1361
1553
  !definition.styles[0]!.rules &&
1362
1554
  !definition.styles[0]!.targets)
@@ -1748,6 +1940,38 @@ export class ExtractError extends Error {
1748
1940
  override name = 'Source.ExtractError'
1749
1941
  }
1750
1942
 
1943
+ /** Accepts an entry context that the composing call selects identically. */
1944
+ function context(
1945
+ entry: Call['nativeContext'],
1946
+ composite: Call['nativeContext'],
1947
+ ): boolean {
1948
+ if (!entry || entry === composite) return true
1949
+ if (!composite || entry.defaultVars !== composite.defaultVars) return false
1950
+
1951
+ const names = Object.keys(entry.vars)
1952
+ return (
1953
+ names.length === Object.keys(composite.vars).length &&
1954
+ names.every((name) => {
1955
+ const left = entry.vars[name]?.[Token.definition].contract
1956
+ const right = composite.vars[name]?.[Token.definition].contract
1957
+ return (
1958
+ !!left &&
1959
+ !!right &&
1960
+ (left === right ||
1961
+ (left[Token.identity] !== undefined &&
1962
+ left[Token.identity] === right[Token.identity]))
1963
+ )
1964
+ })
1965
+ )
1966
+ }
1967
+
1968
+ /** Lists declarations in emitted order, through conditions and segments. */
1969
+ function declarations(style: Style.NamedStyle): readonly Style.Declaration[] {
1970
+ return style.rules
1971
+ ? style.rules.flatMap((rule) => declarations(rule.style))
1972
+ : style.declarations
1973
+ }
1974
+
1751
1975
  function identity(value: string): string {
1752
1976
  let first = 2166136261
1753
1977
  let second = 2246822507
@@ -1189,7 +1189,10 @@ export function compile(options: compile.Options): compile.ReturnType {
1189
1189
  ) ?? -1
1190
1190
  if (start < 0) continue
1191
1191
 
1192
- const location = authored[propertyIndex]!
1192
+ // Composed packed entries can expand differently from the composite's shorthands.
1193
+ const location = authored[propertyIndex]
1194
+ if (!location) continue
1195
+
1193
1196
  used.add(propertyIndex)
1194
1197
 
1195
1198
  Mapping.addMapping(cssMap, {
@@ -1,6 +1,7 @@
1
1
  /** Finds nonescaping local static callables whose applications can become props. @module */
2
2
  import type * as Ast from '@oxc-project/types'
3
3
  import * as Walker from 'oxc-walker'
4
+ import * as Expression from './Expression.js'
4
5
  import * as Scope from './Scope.js'
5
6
  import type * as Source from '../Source.js'
6
7
 
@@ -158,6 +159,8 @@ export function create(
158
159
  if (!candidates.length) return undefined
159
160
 
160
161
  const lexical = new Set<Ast.Node>()
162
+ // Extraction resolves these reads and rewrites their definitions, so none run.
163
+ const resolved = new Set<Ast.Node>()
161
164
  // Reads inside functions, classes, or namespaces may run before initialization.
162
165
  const deferred = new Set<Ast.Node>()
163
166
  const terminals = new Set<Ast.Node>()
@@ -200,6 +203,7 @@ export function create(
200
203
  ) {
201
204
  lexical.add(node)
202
205
  if (terminal(ancestors)) terminals.add(node)
206
+ if (extracted(ancestors, definitions)) resolved.add(node)
203
207
  if (
204
208
  ancestors.some((node) =>
205
209
  [
@@ -240,7 +244,7 @@ export function create(
240
244
  const called = new Set<string>()
241
245
 
242
246
  for (const reference of references.get(declaration.id.name) ?? []) {
243
- if (reference === declaration.id) continue
247
+ if (reference === declaration.id || resolved.has(reference)) continue
244
248
 
245
249
  let callee: Ast.Node = reference
246
250
  let call = direct
@@ -492,6 +496,58 @@ export declare namespace create {
492
496
  }
493
497
  }
494
498
 
499
+ // Matches a selector interpolation or composition entry inside a definition, which
500
+ // extraction resolves statically. Computed keys elsewhere in a definition fail extraction.
501
+ function extracted(
502
+ path: readonly Ast.Node[],
503
+ definitions: ReadonlyMap<number, Source.Call>,
504
+ ): boolean {
505
+ let index = path.length - 1
506
+ const member = path[index - 1]
507
+ if (
508
+ member?.type === 'MemberExpression' &&
509
+ member.object === path[index] &&
510
+ !member.computed
511
+ )
512
+ index--
513
+
514
+ // Type assertions around a node keep the same read.
515
+ function parent() {
516
+ const node = path[index]!
517
+ while (index > 0 && Expression.unwrap(path[index - 1]!) === node) index--
518
+ return path[--index]
519
+ }
520
+
521
+ function definition(node: Ast.Node | undefined) {
522
+ return (
523
+ node?.type === 'CallExpression' &&
524
+ definitions.get(node.start)?.end === node.end
525
+ )
526
+ }
527
+
528
+ const container = parent()
529
+
530
+ if (container?.type === 'ArrayExpression') {
531
+ const call = parent()
532
+ return (
533
+ call?.type === 'CallExpression' &&
534
+ definition(call) &&
535
+ !!call.arguments[0] &&
536
+ Expression.unwrap(call.arguments[0]) === container
537
+ )
538
+ }
539
+
540
+ if (container?.type !== 'TemplateLiteral') return false
541
+
542
+ const property = parent()
543
+ return (
544
+ property?.type === 'Property' &&
545
+ property.computed &&
546
+ Expression.unwrap(property.key) === container &&
547
+ path.slice(0, index).some(definition)
548
+ )
549
+ }
550
+
495
551
  // Matches a no-argument application spread onto, or read as the class of, a DOM element.
496
552
  function terminal(path: readonly Ast.Node[]): boolean {
497
553
  let index = path.length - 1
@@ -11,7 +11,7 @@ import * as Expression from './Expression.js'
11
11
  import type * as Scope from './Scope.js'
12
12
  import * as Themes from './Themes.js'
13
13
 
14
- /** Collects scoped selectors, local identities, and exported style references. */
14
+ /** Collects scoped selectors, local identities, exported style references, and composed definitions. */
15
15
  export function scan(
16
16
  program: Ast.Program,
17
17
  scope: Scope.Tracker,
@@ -21,6 +21,7 @@ export function scan(
21
21
  selectorKeys: ReadonlySet<number> = new Set(),
22
22
  cssNamespaces: ReadonlyMap<number, string> = new Map(),
23
23
  moduleId = namespace,
24
+ composed: readonly Ast.Node[] = [],
24
25
  ) {
25
26
  const authoredNames = Names.collect(program)
26
27
  const conditions = new Map<number, string>()
@@ -33,6 +34,7 @@ export function scan(
33
34
  const namespaces = new Map<number, Map<string, Ast.Expression>>()
34
35
  const importedNames = new Map<string, Themes.Link>()
35
36
  const imported = new Map<number, Themes.Link>()
37
+ const compositions = new Map<number, Ast.CallExpression | Themes.Link>()
36
38
 
37
39
  const names = new Map<string, Ast.Expression>()
38
40
  const publicNames = new Map<string, string>()
@@ -122,7 +124,7 @@ export function scan(
122
124
  }
123
125
 
124
126
  if (!calls.length && !imported.size)
125
- return { conditions, exports, identities, localConditions }
127
+ return { compositions, conditions, exports, identities, localConditions }
126
128
 
127
129
  const exposed = new Map<Ast.Expression, readonly (readonly string[])[]>()
128
130
  for (const [name, local] of publicNames) {
@@ -138,8 +140,8 @@ export function scan(
138
140
  for (const [member, value] of values)
139
141
  exposed.set(value, [...(exposed.get(value) ?? []), [name, member]])
140
142
 
141
- if (!selectorKeys.size && !exposed.size)
142
- return { conditions, exports, identities, localConditions }
143
+ if (!selectorKeys.size && !exposed.size && !composed.length)
144
+ return { compositions, conditions, exports, identities, localConditions }
143
145
 
144
146
  type Entry = { node: Ast.Node; parent: Ast.Node | null | undefined }
145
147
  const nodes: Entry[] = []
@@ -179,10 +181,11 @@ export function scan(
179
181
  }
180
182
  }
181
183
 
182
- function resolve(
184
+ // Composition reads the definition itself, so only selector references claim an identity.
185
+ function target(
183
186
  node: Ast.Node,
184
187
  seen = new Set<Ast.Node>(),
185
- ): Themes.Link | undefined {
188
+ ): Ast.CallExpression | Themes.Link | undefined {
186
189
  node = Expression.unwrap(node)
187
190
  if (seen.has(node)) return undefined
188
191
  seen.add(node)
@@ -191,7 +194,7 @@ export function scan(
191
194
  node.type === 'CallExpression' &&
192
195
  definitions.get(node.start)?.end === node.end
193
196
  )
194
- return link(node)
197
+ return node
195
198
 
196
199
  if (node.type === 'Identifier') {
197
200
  const declaration = declarations.get(node.start)
@@ -199,7 +202,7 @@ export function scan(
199
202
  const external = imported.get(declaration)
200
203
  if (external) return external
201
204
  const value = bindings.get(declaration)
202
- return value ? resolve(value, seen) : undefined
205
+ return value ? target(value, seen) : undefined
203
206
  }
204
207
 
205
208
  if (node.type === 'MemberExpression' && !node.optional) {
@@ -221,14 +224,25 @@ export function scan(
221
224
  declaration === undefined
222
225
  ? undefined
223
226
  : namespaces.get(declaration)?.get(name)
224
- if (value) return resolve(value, seen)
227
+ if (value) return target(value, seen)
225
228
  }
226
- return resolve(node.object, seen)?.members?.[name]
229
+ const object = target(node.object, seen)
230
+ return object && !('type' in object) ? object.members?.[name] : undefined
227
231
  }
228
232
 
229
233
  return undefined
230
234
  }
231
235
 
236
+ function resolve(node: Ast.Node): Themes.Link | undefined {
237
+ const value = target(node)
238
+ return value && 'type' in value ? link(value) : value
239
+ }
240
+
241
+ for (const node of composed) {
242
+ const value = target(node)
243
+ if (value) compositions.set(node.start, value)
244
+ }
245
+
232
246
  for (const { node } of nodes) {
233
247
  if (node.type === 'VariableDeclarator' && node.init)
234
248
  for (const [key, member] of exposed.get(node.init) ?? []) {
@@ -335,5 +349,5 @@ export function scan(
335
349
  return [...imported.values()].some(contains)
336
350
  }
337
351
 
338
- return { conditions, exports, identities, localConditions }
352
+ return { compositions, conditions, exports, identities, localConditions }
339
353
  }
@@ -10,6 +10,10 @@ import type * as Theme from './Theme.js'
10
10
  /** Private ownership used by uncompiled composition; never spread onto DOM props. */
11
11
  export const metadata = Symbol.for('zyzz.authoring')
12
12
 
13
+ // Static definitions retain their style data so later compositions can read it,
14
+ // including definitions created by another bundled copy of the runtime.
15
+ const data = Symbol.for('zyzz.style')
16
+
13
17
  /** Identity and replaceable attribute/slot ownership for an applied definition. */
14
18
  export type Owner = {
15
19
  readonly name: string
@@ -77,24 +81,54 @@ export function create(
77
81
  if (!dynamic && !id && Object.keys(input as object).length === 0)
78
82
  Identity.requireId(undefined, 'Empty style')
79
83
 
80
- if (id && !dynamic && options.theme)
81
- Style.define({ style: body(input as Record<string, unknown>) } as never, {
82
- vars: options.theme,
83
- })
84
- const className = id
85
- ? `z-style-${id}`
86
- : Identity.style(
87
- (
88
- Style.define as (
89
- input: Record<string, unknown>,
90
- options: Style.define.Options,
91
- ) => Style.Definition
92
- )(
93
- { style: body(input as Record<string, unknown>) },
94
- { vars: options.theme },
95
- ).styles[0]!,
96
- namespace,
97
- )
84
+ const define = (input: unknown) =>
85
+ (
86
+ Style.define as (
87
+ input: Record<string, unknown>,
88
+ options: Style.define.Options,
89
+ ) => Style.Definition
90
+ )(
91
+ { style: body(input as Record<string, unknown>) },
92
+ { vars: options.theme },
93
+ ).styles[0]!
94
+
95
+ const style = (() => {
96
+ if (dynamic) return undefined
97
+ if (!Array.isArray(input)) return define(input)
98
+
99
+ // Mirrors compiled composition: each entry is one ordered declaration segment.
100
+ const rules: Style.Rule[] = []
101
+ for (const [index, entry] of (input as readonly unknown[]).entries()) {
102
+ if (typeof entry !== 'function') {
103
+ rules.push({ style: define(entry) })
104
+ continue
105
+ }
106
+
107
+ const style = Object.getOwnPropertyDescriptor(entry, data)?.value as
108
+ | Style.NamedStyle
109
+ | undefined
110
+ if (style) {
111
+ rules.push({ style })
112
+ continue
113
+ }
114
+
115
+ // Compiled callables carry only class names, so an explicit id names the composite.
116
+ if (id) return undefined
117
+
118
+ throw new Style.InvalidError([
119
+ {
120
+ code: 'invalid_structure',
121
+ message:
122
+ 'Composition entries require literal objects or static style definitions. Compositions of compiled definitions require an explicit id, and variants and dynamic styles are applied with cx.',
123
+ path: [String(index)],
124
+ },
125
+ ])
126
+ }
127
+
128
+ return { name: 'style', declarations: [], rules } satisfies Style.NamedStyle
129
+ })()
130
+
131
+ const className = id ? `z-style-${id}` : Identity.style(style!, namespace)
98
132
  const props = Props.create({ className })
99
133
  const apply = (values?: style.Options & Record<string, unknown>) => {
100
134
  let result = props(values as style.Options)
@@ -124,6 +158,7 @@ export function create(
124
158
  Object.defineProperty(apply, Symbol.toPrimitive, {
125
159
  value: () => `.${className}`,
126
160
  })
161
+ if (style) Object.defineProperty(apply, data, { value: style })
127
162
  return apply as style.ReturnType
128
163
  }
129
164
 
@@ -625,15 +625,52 @@ export type StyleFactory<
625
625
  ? [options?: style.DefinitionOptions]
626
626
  : [invalid: never, unavailable: never]
627
627
  : [invalid: never, unavailable: never]
628
- ): style.Dynamic<values, output>
628
+ // TypeScript types a call that matches no overload with this signature. Other
629
+ // inputs leave `callback` at its constraint, so their definitions stay static.
630
+ ): ((...args: never[]) => unknown) extends callback
631
+ ? style.ReturnType<output>
632
+ : style.Dynamic<values, output>
629
633
  <const styles extends Record<string, unknown>>(
630
634
  styles: styles &
631
635
  NoInfer<Body<styles, tokens, layers, mappings>> &
632
636
  Completion.Properties<tokens>,
633
637
  options?: style.DefinitionOptions,
634
638
  ): style.ReturnType<output>
639
+ <const entries>(
640
+ styles: entries & Entries<entries, tokens, layers, mappings>,
641
+ ...options: entries extends readonly unknown[]
642
+ ? [options?: style.DefinitionOptions]
643
+ : [invalid: never, unavailable: never]
644
+ ): style.ReturnType<output>
635
645
  }
636
646
 
647
+ // Composition entries are static definitions or checked literal objects.
648
+ type Entries<
649
+ entries,
650
+ tokens extends Theme.Tokens,
651
+ layers extends string,
652
+ mappings extends Shorthands.Map,
653
+ > = {
654
+ readonly [index in keyof entries]: Entry<
655
+ entries[index],
656
+ tokens,
657
+ layers,
658
+ mappings
659
+ >
660
+ }
661
+
662
+ // Distributes over mixed element unions so each member keeps its own contract.
663
+ type Entry<
664
+ entry,
665
+ tokens extends Theme.Tokens,
666
+ layers extends string,
667
+ mappings extends Shorthands.Map,
668
+ > = entry extends (...args: never) => unknown
669
+ ? style.Composable<entry>
670
+ : entry &
671
+ NoInfer<Body<entry, tokens, layers, mappings>> &
672
+ Completion.Properties<tokens>
673
+
637
674
  type Keys<styles> = styles extends unknown ? keyof styles : never
638
675
 
639
676
  /** Checked declaration body shared by configured styles and recipe choices. */