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
@@ -494,13 +494,31 @@ export function collect(program: Ast.Program, options: collect.Options) {
494
494
  root = root.object
495
495
  }
496
496
  if (root.type !== 'Identifier') return undefined
497
- const local = derived?.name === root.name
498
- const call = names.get(root.name)
499
- if (!local && !call) return undefined
500
- let value: unknown = local ? derived.vars : themes[call!.name]?.tokens
501
- if (!local && !call!.variableSet && !call!.directVariables) {
502
- if (!['vars', 'tokens'].includes(path.shift()!)) return undefined
503
- }
497
+ const name = root.name
498
+ let value: unknown = (() => {
499
+ if (derived?.name === name) return derived.vars
500
+
501
+ const call = names.get(name)
502
+ if (call) {
503
+ if (
504
+ !call.variableSet &&
505
+ !call.directVariables &&
506
+ !['vars', 'tokens'].includes(path.shift()!)
507
+ )
508
+ return undefined
509
+ return themes[call.name]?.tokens
510
+ }
511
+
512
+ // Config-bound vars read the configured variables, so they follow set scoping.
513
+ const config = configs.get(name)
514
+ if (
515
+ !config?.call.variableConfig ||
516
+ (!config.call.selection && path.shift() !== 'vars')
517
+ )
518
+ return undefined
519
+ return themes[config.call.name]?.tokens
520
+ })()
521
+ if (value === undefined) return undefined
504
522
  for (const key of path)
505
523
  value =
506
524
  value && typeof value === 'object'
@@ -615,6 +633,68 @@ export function collect(program: Ast.Program, options: collect.Options) {
615
633
  return result
616
634
  }
617
635
 
636
+ /** Reads a derived-variables callback as one parameter name and a literal body. */
637
+ function derivation(argument: Ast.Node | undefined) {
638
+ const callback = argument && Expression.unwrap(argument)
639
+ if (
640
+ callback?.type !== 'ArrowFunctionExpression' &&
641
+ callback?.type !== 'FunctionExpression'
642
+ )
643
+ return undefined
644
+
645
+ const parameter = callback.params[0]
646
+ const body = (() => {
647
+ if (callback.body?.type !== 'BlockStatement') return callback.body
648
+ if (
649
+ callback.body.body.length === 1 &&
650
+ callback.body.body[0]?.type === 'ReturnStatement'
651
+ )
652
+ return callback.body.body[0].argument
653
+ return undefined
654
+ })()
655
+ if (
656
+ callback.async ||
657
+ callback.generator ||
658
+ callback.params.length !== 1 ||
659
+ parameter?.type !== 'Identifier' ||
660
+ !body
661
+ )
662
+ fail(
663
+ 'Derived variables require one named parameter and a literal return value.',
664
+ callback,
665
+ )
666
+
667
+ return { body, name: parameter.name }
668
+ }
669
+
670
+ /** Authored values of an extension, keeping base values at existing paths as `Vars.extend` types do. */
671
+ function extendedValues(base: Theme.Definition, extension: Theme.Definition) {
672
+ const metadata = extension[Token.definition]
673
+ const known = base[Token.definition].values
674
+ const tree: Record<string, unknown> = Object.create(null)
675
+
676
+ for (const path of Object.keys(metadata.values)) {
677
+ const parts = metadata.paths?.[path] ?? path.split('.')
678
+ const source = Object.hasOwn(known, path) ? base : extension
679
+ let target = tree
680
+ let token: unknown = source.tokens
681
+
682
+ for (const part of parts.slice(0, -1)) {
683
+ target = (target[part] ??= Object.create(null)) as Record<
684
+ string,
685
+ unknown
686
+ >
687
+ token = (token as Record<string, unknown>)[part]
688
+ }
689
+
690
+ const name = parts.at(-1)!
691
+ // Every metadata path has a reference in its own token tree.
692
+ target[name] = (token as Record<string, Token.Reference>)[name]!.value
693
+ }
694
+
695
+ return tree
696
+ }
697
+
618
698
  function fail(message: string, node: Pick<Ast.Node, 'end' | 'start'>): never {
619
699
  throw new InvalidError(message, node)
620
700
  }
@@ -750,6 +830,18 @@ export function collect(program: Ast.Program, options: collect.Options) {
750
830
  )
751
831
  if (id !== undefined) name = Identity.requireId(id, 'Vars.define')
752
832
  }
833
+ if (!config && method === 'extend') {
834
+ const argument = expression.arguments[2]
835
+ const callback = argument && Expression.unwrap(argument)
836
+ const id = Identifiers.explicit(
837
+ expression,
838
+ callback?.type === 'ArrowFunctionExpression' ||
839
+ callback?.type === 'FunctionExpression'
840
+ ? 3
841
+ : 2,
842
+ )
843
+ if (id !== undefined) name = Identity.requireId(id, 'Vars.extend')
844
+ }
753
845
 
754
846
  if (config) {
755
847
  const nativeProvider =
@@ -931,42 +1023,17 @@ export function collect(program: Ast.Program, options: collect.Options) {
931
1023
 
932
1024
  tokenType = type(expression.arguments[0]!)
933
1025
 
934
- const argument = expression.arguments[1]
935
- const callback = argument && Expression.unwrap(argument)
936
- if (
937
- variableSet &&
938
- (callback?.type === 'ArrowFunctionExpression' ||
939
- callback?.type === 'FunctionExpression')
940
- ) {
941
- const parameter = callback.params[0]
942
- const body = (() => {
943
- if (callback.body?.type !== 'BlockStatement') return callback.body
944
- if (
945
- callback.body.body.length === 1 &&
946
- callback.body.body[0]?.type === 'ReturnStatement'
947
- )
948
- return callback.body.body[0].argument
949
- return undefined
950
- })()
951
- if (
952
- callback.async ||
953
- callback.generator ||
954
- callback.params.length !== 1 ||
955
- parameter?.type !== 'Identifier' ||
956
- !body
957
- )
958
- fail(
959
- 'Derived variables require one named parameter and a literal return value.',
960
- callback,
961
- )
962
-
1026
+ const derive = variableSet
1027
+ ? derivation(expression.arguments[1])
1028
+ : undefined
1029
+ if (derive) {
963
1030
  const base = VariableSets.build(
964
1031
  input,
965
1032
  Object.freeze({ variableSet: true, [Token.identity]: name }),
966
1033
  )
967
1034
  input = VariableSets.merge(
968
1035
  input,
969
- data(body, { name: parameter.name, vars: base }),
1036
+ data(derive.body, { name: derive.name, vars: base }),
970
1037
  )
971
1038
  tokenType = Configurations.type(input)
972
1039
  } else if (expression.arguments.length > 2)
@@ -994,33 +1061,88 @@ export function collect(program: Ast.Program, options: collect.Options) {
994
1061
  } else {
995
1062
  const base = expression.arguments[0]
996
1063
  const parent = base ? resolve(base)?.call : undefined
1064
+ const derive = variableSet
1065
+ ? derivation(expression.arguments[2])
1066
+ : undefined
997
1067
 
998
- if (expression.arguments.length !== 2 || !parent)
1068
+ const arity = derive ? 3 : 2
1069
+ // A trailing options object names a new set over the base's variables.
1070
+ const id =
1071
+ expression.arguments.length === arity + 1
1072
+ ? Identifiers.explicit(expression, arity)
1073
+ : undefined
1074
+ const named = id !== undefined
1075
+ if (
1076
+ (expression.arguments.length !== arity && !named) ||
1077
+ !parent
1078
+ )
999
1079
  fail(
1000
1080
  'Vars.extend requires a preceding variable set and literal overrides.',
1001
1081
  expression,
1002
1082
  )
1003
1083
 
1004
1084
  factoryReferences.add(base!.start)
1005
- definition = variableSet
1006
- ? VariableSets.theme(
1007
- Vars.extend(
1008
- VariableSets.from(themes[parent.name]![Token.definition]),
1009
- data(expression.arguments[1]!) as Vars.Overrides<Vars.Values>,
1010
- ),
1011
- )
1012
- : Theme.extend(
1013
- themes[parent.name]!,
1014
- data(expression.arguments[1]!) as Theme.Overrides<Theme.Tokens>,
1015
- )
1085
+ tokenType = parent.tokenType
1086
+ output = parent.output
1087
+
1088
+ if (variableSet) {
1089
+ const theme = themes[parent.name]!
1090
+ const original = VariableSets.from(theme[Token.definition])
1091
+ const extend = Vars.extend as (
1092
+ variables: Vars.Definition,
1093
+ values: Vars.Values,
1094
+ derive: (vars: Vars.References<Vars.Values>) => Vars.Values,
1095
+ options: { readonly id?: string | undefined },
1096
+ ) => Vars.Definition
1097
+ const extended = extend(
1098
+ original,
1099
+ data(expression.arguments[1]!) as Vars.Values,
1100
+ (vars) =>
1101
+ derive
1102
+ ? (data(derive.body, {
1103
+ name: derive.name,
1104
+ vars: vars as Vars.Definition,
1105
+ }) as Vars.Values)
1106
+ : {},
1107
+ { id },
1108
+ )
1109
+ definition = named
1110
+ ? Token.bind(
1111
+ VariableSets.theme(extended),
1112
+ Object.freeze({
1113
+ ...extended[Token.definition].contract,
1114
+ [Token.identity]: name,
1115
+ }),
1116
+ )
1117
+ : VariableSets.theme(extended)
1118
+
1119
+ const metadata = definition[Token.definition]
1120
+ // Additions grow the token contract; overridden leaves keep their base types, as in `Vars.extend`.
1121
+ if (
1122
+ named ||
1123
+ Object.keys(metadata.values).length !==
1124
+ Object.keys(theme[Token.definition].values).length ||
1125
+ JSON.stringify(metadata.queries) !==
1126
+ JSON.stringify(theme[Token.definition].queries)
1127
+ )
1128
+ tokenType = Configurations.type({
1129
+ ...extendedValues(theme, definition),
1130
+ ...metadata.queries,
1131
+ })
1132
+ } else
1133
+ definition = Theme.extend(
1134
+ themes[parent.name]!,
1135
+ data(expression.arguments[1]!) as Theme.Overrides<Theme.Tokens>,
1136
+ )
1137
+
1138
+ // Extensions share their base identity, so the binding keeps sibling sets distinct.
1016
1139
  if (
1140
+ !named &&
1017
1141
  definition[Token.definition].contract[Token.identity]?.startsWith(
1018
1142
  'id-',
1019
1143
  )
1020
1144
  )
1021
- name = definition.className.slice('z-theme-'.length)
1022
- tokenType = parent.tokenType
1023
- output = parent.output
1145
+ name = `${definition.className.slice('z-theme-'.length)}-${binding}`
1024
1146
  }
1025
1147
  } catch (error) {
1026
1148
  if (error instanceof InvalidError) throw error
@@ -137,11 +137,12 @@ export function create(
137
137
  appearance: result.appearance,
138
138
  script: result.script,
139
139
  style: result.style,
140
+ // The default set's metadata lets Vars.extend grow configured vars.
140
141
  vars: Object.freeze(
141
- Object.defineProperties(
142
- vars,
143
- Object.getOwnPropertyDescriptors(theme.tokens),
144
- ),
142
+ Object.defineProperties(vars, {
143
+ ...Object.getOwnPropertyDescriptors(theme.tokens),
144
+ [Token.definition]: { value: theme[Token.definition] },
145
+ }),
145
146
  ),
146
147
  variants: result.variants,
147
148
  })
@@ -150,6 +151,7 @@ export function create(
150
151
  for (const key of Object.keys(input))
151
152
  if (
152
153
  ![
154
+ 'composition',
153
155
  'cssOutput',
154
156
  'id',
155
157
  'defaultLayer',
@@ -190,6 +192,13 @@ export function create(
190
192
  )
191
193
  throw new InvalidError('cssOutput must be atomic or grouped.')
192
194
 
195
+ if (
196
+ input.composition !== undefined &&
197
+ input.composition !== 'independent' &&
198
+ input.composition !== 'ordered'
199
+ )
200
+ throw new InvalidError('composition must be independent or ordered.')
201
+
193
202
  if (input.theme !== undefined && input.themes !== undefined)
194
203
  throw new InvalidError('Use either theme or themes, not both.')
195
204
 
@@ -253,6 +262,9 @@ export function create(
253
262
 
254
263
  const contract = Object.freeze({
255
264
  ...(variableMode ? { variableSet: true, mappings: variableMappings } : {}),
265
+ ...(input.composition !== undefined
266
+ ? { composition: input.composition as 'independent' | 'ordered' }
267
+ : {}),
256
268
  ...(input.defaultLayer !== undefined
257
269
  ? { defaultLayer: input.defaultLayer as string }
258
270
  : {}),
@@ -452,6 +464,11 @@ export function create(
452
464
  export declare namespace create {
453
465
  /** Optional layer names and mutually exclusive theme modes. */
454
466
  type Options = {
467
+ /**
468
+ * Whether bound styles' class lists may combine. Independent atomic styles
469
+ * share rules for repeated declarations, so combine them only with `cx`.
470
+ */
471
+ readonly composition?: 'independent' | 'ordered' | undefined
455
472
  /** CSS representation inherited by bound helpers; atomic by default. */
456
473
  readonly cssOutput?: 'atomic' | 'grouped' | undefined
457
474
  /** Fallback CSS layer for bound styles and variants; unlayered when omitted. */
@@ -837,6 +854,11 @@ type Validated<options> = Record<
837
854
 
838
855
  /** Vars, optional named alternatives, and category-to-property mappings. */
839
856
  export type VariableOptions = {
857
+ /**
858
+ * Whether bound styles' class lists may combine. Independent atomic styles
859
+ * share rules for repeated declarations, so combine them only with `cx`.
860
+ */
861
+ readonly composition?: 'independent' | 'ordered' | undefined
840
862
  /** CSS representation inherited by bound helpers; atomic by default. */
841
863
  readonly cssOutput?: 'atomic' | 'grouped' | undefined
842
864
  /** Fallback CSS layer for bound styles and variants; unlayered when omitted. */
@@ -393,6 +393,8 @@ export type Contract = {
393
393
  readonly variableSet?: boolean | undefined
394
394
  /** Configuration-local category-to-property mappings. */
395
395
  readonly mappings?: VariableSets.Mappings | false | undefined
396
+ /** Whether bound styles' class lists combine, retained by configuration-bound handles. */
397
+ readonly composition?: 'independent' | 'ordered' | undefined
396
398
  /** Web emission mode retained by configuration-bound theme handles. */
397
399
  readonly cssOutput?: 'atomic' | 'grouped' | undefined
398
400
  /** Fallback layer for declarations without an explicit layer. */
@@ -55,19 +55,30 @@ export function from(metadata: Token.Metadata): Vars.Definition {
55
55
  ) as Vars.Definition
56
56
  }
57
57
 
58
- /** Validates and freezes a complete set or compatible partial override. */
58
+ /**
59
+ * Validates and freezes a complete set, or grows a base set with compatible
60
+ * overrides and new leaves.
61
+ */
59
62
  export function build(
60
63
  input: unknown,
61
64
  contract: Token.Contract,
62
- base?: Token.Metadata['values'],
63
- baseQueries?: Query.Metadata,
64
- basePaths?: Token.Metadata['paths'],
65
+ options: build.Options = {},
65
66
  ): Vars.Definition {
67
+ const base = options.base
66
68
  const values: Record<string, Token.Value> = Object.assign(
67
69
  Object.create(null),
68
- base,
70
+ base?.values,
69
71
  )
70
- const paths = { ...basePaths }
72
+ const paths = { ...base?.paths }
73
+ // Segment keys keep dotted typography conditions distinct from nested paths.
74
+ const leaves = new Set<string>()
75
+ const categories = new Set<string>()
76
+ for (const path of Object.keys(base?.values ?? {})) {
77
+ const parts = base!.paths?.[path] ?? path.split('.')
78
+ leaves.add(JSON.stringify(parts))
79
+ for (let index = 1; index < parts.length; index++)
80
+ categories.add(JSON.stringify(parts.slice(0, index)))
81
+ }
71
82
  const conditions: string[][] = []
72
83
  const active = new Set<object>()
73
84
  function visit(input: unknown, path: string[]) {
@@ -86,14 +97,31 @@ export function build(
86
97
  ) {
87
98
  if (!path.length)
88
99
  throw new Vars.InvalidError(path, 'Expected a variable record.')
89
- const value = read(input, path, queryData)
100
+ if (categories.has(JSON.stringify(path)))
101
+ throw new Vars.InvalidError(
102
+ path,
103
+ 'Extensions cannot replace a variable category with a leaf.',
104
+ )
90
105
  const name = path.join('.')
91
- if (base && !Object.hasOwn(base, name))
106
+ const previous =
107
+ base && Object.hasOwn(base.values, name) ? base.values[name] : undefined
108
+ if (previous !== undefined && options.derived)
92
109
  throw new Vars.InvalidError(
93
110
  path,
94
- 'Extensions cannot add variable paths.',
111
+ 'Derived variables cannot replace existing paths.',
95
112
  )
96
- if (base && domain(base[name]!) !== domain(value))
113
+ // Restating the base reference would make the variable reference itself.
114
+ if (
115
+ previous !== undefined &&
116
+ Token.is(input) &&
117
+ input.path === name &&
118
+ (input.contract === contract ||
119
+ (contract[Token.identity] !== undefined &&
120
+ input.contract[Token.identity] === contract[Token.identity]))
121
+ )
122
+ return
123
+ const value = read(input, path, queryData)
124
+ if (previous !== undefined && domain(previous) !== domain(value))
97
125
  throw new Vars.InvalidError(
98
126
  path,
99
127
  'Variable overrides must preserve their domain.',
@@ -103,11 +131,19 @@ export function build(
103
131
  values[name] = value
104
132
  return
105
133
  }
134
+ if (leaves.has(JSON.stringify(path)))
135
+ throw new Vars.InvalidError(
136
+ path,
137
+ 'Extensions cannot replace a variable leaf with a category.',
138
+ )
106
139
  const entries = record(input, path)
107
140
  if (active.has(input as object))
108
141
  throw new Vars.InvalidError(path, 'Cyclic variables are not supported.')
109
142
  active.add(input as object)
110
- if (!entries.length && !base)
143
+ if (
144
+ !entries.length &&
145
+ (!base || (path.length > 0 && !categories.has(JSON.stringify(path))))
146
+ )
111
147
  throw new Vars.InvalidError(path, 'Variable records cannot be empty.')
112
148
  for (const [key, value] of entries) {
113
149
  const query = path[0] === 'typography' && Typography.condition(key)
@@ -122,34 +158,29 @@ export function build(
122
158
  active.delete(input as object)
123
159
  }
124
160
  const fields = Object.fromEntries(record(input, []))
125
- const queries = { ...baseQueries }
161
+ const queries = { ...base?.queries }
126
162
  for (const key of ['breakpoint', 'container', 'containerNames'] as const) {
127
163
  if (Object.hasOwn(fields, key)) {
128
164
  const value = fields[key]
129
165
  if (base) {
130
166
  if (key === 'containerNames') {
131
167
  if (
132
- JSON.stringify(value) !==
133
- JSON.stringify(baseQueries?.containerNames ?? [])
168
+ !Array.isArray(value) ||
169
+ (base.queries?.containerNames ?? []).some(
170
+ (name) => !value.includes(name),
171
+ )
134
172
  )
135
173
  throw new Vars.InvalidError(
136
174
  [key],
137
- 'Extensions cannot change container identities.',
175
+ 'Extensions cannot remove container identities.',
138
176
  )
139
- } else {
140
- for (const [name] of record(value, [key]))
141
- if (!Object.hasOwn(baseQueries?.[key] ?? {}, name))
142
- throw new Vars.InvalidError(
143
- [key, name],
144
- 'Extensions cannot add query thresholds.',
145
- )
146
- }
177
+ } else record(value, [key])
147
178
  }
148
179
  Object.assign(queries, {
149
180
  [key]:
150
181
  key === 'containerNames'
151
182
  ? value
152
- : { ...baseQueries?.[key], ...value },
183
+ : { ...base?.queries?.[key], ...value },
153
184
  })
154
185
  if (key !== 'container' || Object.keys(value as object).length === 0)
155
186
  delete fields[key]
@@ -178,6 +209,55 @@ export function build(
178
209
  ) as Vars.Definition
179
210
  }
180
211
 
212
+ /** Set construction inputs. */
213
+ export declare namespace build {
214
+ /** Base set metadata and how its existing paths may change. */
215
+ type Options = {
216
+ /** Set whose leaves the input overrides or extends. */
217
+ readonly base?:
218
+ | Pick<Token.Metadata, 'paths' | 'queries' | 'values'>
219
+ | undefined
220
+ /** Rejects existing paths, as derived leaves only add to the set. */
221
+ readonly derived?: boolean | undefined
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Reads a set's references as values for a named extension. Typography presets
227
+ * keep their base values, since presets expand at each use.
228
+ */
229
+ export function references(
230
+ variables: object,
231
+ metadata: Pick<Token.Metadata, 'paths' | 'queries' | 'values'>,
232
+ ): Vars.Values {
233
+ const tree: Record<string, unknown> = Object.create(null)
234
+
235
+ for (const [path, value] of Object.entries(metadata.values)) {
236
+ const parts = metadata.paths?.[path] ?? path.split('.')
237
+ let target = tree
238
+ let source: unknown = variables
239
+
240
+ for (const part of parts.slice(0, -1)) {
241
+ target = (target[part] ??= Object.create(null)) as Record<string, unknown>
242
+ source = (source as Record<string, unknown>)[part]
243
+ }
244
+
245
+ const name = parts.at(-1)!
246
+ target[name] =
247
+ parts[0] === 'typography'
248
+ ? value
249
+ : (source as Record<string, unknown>)[name]
250
+ }
251
+
252
+ for (const key of ['breakpoint', 'container'] as const)
253
+ if (Object.keys(metadata.queries?.[key] ?? {}).length)
254
+ tree[key] = metadata.queries![key]
255
+ if (metadata.queries?.containerNames?.length)
256
+ tree.containerNames = metadata.queries.containerNames
257
+
258
+ return tree as Vars.Values
259
+ }
260
+
181
261
  function buildTree(
182
262
  values: Token.Metadata['values'],
183
263
  contract: Token.Contract,
@@ -1,5 +1,6 @@
1
1
  /** Retains filesystem snapshots and an incremental graph within a Next.js loader worker. @module */
2
2
  import * as Contract from '../../compiler/internal/Contract.js'
3
+ import * as Contracts from '../../node/internal/Contracts.js'
3
4
  import * as Fs from 'node:fs/promises'
4
5
  import * as Graph from '../../compiler/Graph.js'
5
6
  import * as Path from 'node:path'
@@ -26,6 +27,7 @@ export type Context = {
26
27
  /** Creates project state owned by a loader configuration in one worker process. */
27
28
  export function create(options: create.Options) {
28
29
  let compiler = Graph.create()
30
+ const packages = Contracts.create()
29
31
  const files = new Map<string, { source: string; version: string }>()
30
32
  const libraries = new Map<
31
33
  string,
@@ -122,6 +124,20 @@ export function create(options: create.Options) {
122
124
  }
123
125
  return pending
124
126
  }
127
+ // Files behind package contracts stay dependencies even when the module is unchanged.
128
+ const inputs = new Set<string>()
129
+ const host: Contracts.read.Options = {
130
+ read: async (file) => {
131
+ const source = await read(file)
132
+ // Next ignores node_modules edits, and tracked manifests stalled webpack rebuilds after a failed compile.
133
+ if (Path.basename(file) === 'package.json') return source
134
+ track(file)
135
+ inputs.add(file)
136
+ return source
137
+ },
138
+ resolve: async (specifier, importer) =>
139
+ (await resolve(Path.dirname(importer), specifier)) || undefined,
140
+ }
125
141
  const id = (file: string) =>
126
142
  `app/${Path.relative(root, file).split(Path.sep).join('/')}`
127
143
  const eligible = (file: string) =>
@@ -322,14 +338,10 @@ export function create(options: create.Options) {
322
338
  await visit(resolved)
323
339
  continue
324
340
  }
325
- const sidecar = `${resolved}.zyzz.json`
326
- try {
327
- contracts[resolved] = await read(sidecar)
328
- track(sidecar)
329
- links[specifier] = resolved
330
- } catch (error) {
331
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
332
- }
341
+ const contract = await packages.read(resolved, host)
342
+ if (contract === undefined) continue
343
+ contracts[resolved] = contract
344
+ links[specifier] = resolved
333
345
  }
334
346
  }
335
347
 
@@ -360,11 +372,10 @@ export function create(options: create.Options) {
360
372
 
361
373
  const links = (imports[owner] ??= Object.create(null))
362
374
  links[specifier] = target
363
- if (!Object.hasOwn(contracts, target)) {
364
- const sidecar = `${target}.zyzz.json`
365
- contracts[target] = await read(sidecar)
366
- track(sidecar)
367
- }
375
+ contracts[target] ??= (await packages.read(target, {
376
+ ...host,
377
+ required: true,
378
+ }))!
368
379
  await dependencies(target)
369
380
  owner = target
370
381
  }
@@ -388,16 +399,10 @@ export function create(options: create.Options) {
388
399
  if (output.code === source && !output.css) {
389
400
  context.addDependency(context.resourcePath)
390
401
  for (const file of runtime) context.addDependency(file)
391
- for (const file of tracked)
392
- if (file.endsWith('.zyzz.json')) context.addDependency(file)
393
- for (const target of Object.values(imports[entry] ?? {})) {
394
- if (!target) continue
395
- context.addDependency(
396
- target.startsWith('app/')
397
- ? Path.join(root, target.slice(4))
398
- : `${target}.zyzz.json`,
399
- )
400
- }
402
+ for (const file of inputs) context.addDependency(file)
403
+ for (const target of Object.values(imports[entry] ?? {}))
404
+ if (target?.startsWith('app/'))
405
+ context.addDependency(Path.join(root, target.slice(4)))
401
406
  } else {
402
407
  for (const file of tracked) context.addDependency(file)
403
408
  }
@@ -161,7 +161,8 @@ async function compile(context: Context, source: string) {
161
161
  placeholder,
162
162
  Path.relative(directory, file)
163
163
  .split(Path.sep)
164
- .map(encodeURIComponent)
164
+ // Bundlers resolve url() paths without decoding, so escape only characters that change URL parsing.
165
+ .map((part) => part.replace(/[%#?]/g, encodeURIComponent))
165
166
  .join('/') + target.slice(raw.length),
166
167
  )
167
168
  }