zyzz 0.0.25 → 0.0.27

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 (36) hide show
  1. package/dist/Vars.d.ts +18 -2
  2. package/dist/Vars.d.ts.map +1 -1
  3. package/dist/Vars.js +21 -3
  4. package/dist/Vars.js.map +1 -1
  5. package/dist/compiler/internal/Configurations.d.ts +3 -0
  6. package/dist/compiler/internal/Configurations.d.ts.map +1 -1
  7. package/dist/compiler/internal/Configurations.js +2 -1
  8. package/dist/compiler/internal/Configurations.js.map +1 -1
  9. package/dist/compiler/internal/Themes.d.ts.map +1 -1
  10. package/dist/compiler/internal/Themes.js +103 -27
  11. package/dist/compiler/internal/Themes.js.map +1 -1
  12. package/dist/internal/Configuration.d.ts.map +1 -1
  13. package/dist/internal/Configuration.js +5 -1
  14. package/dist/internal/Configuration.js.map +1 -1
  15. package/dist/internal/VariableSets.d.ts +20 -4
  16. package/dist/internal/VariableSets.d.ts.map +1 -1
  17. package/dist/internal/VariableSets.js +70 -19
  18. package/dist/internal/VariableSets.js.map +1 -1
  19. package/dist/react-native/internal/Tokens.d.ts.map +1 -1
  20. package/dist/react-native/internal/Tokens.js +3 -6
  21. package/dist/react-native/internal/Tokens.js.map +1 -1
  22. package/dist/vite/index.d.ts.map +1 -1
  23. package/dist/vite/index.js +3 -7
  24. package/dist/vite/index.js.map +1 -1
  25. package/dist/web/internal/Themes.d.ts.map +1 -1
  26. package/dist/web/internal/Themes.js +42 -4
  27. package/dist/web/internal/Themes.js.map +1 -1
  28. package/package.json +13 -1
  29. package/src/Vars.ts +104 -12
  30. package/src/compiler/internal/Configurations.ts +4 -1
  31. package/src/compiler/internal/Themes.ts +149 -45
  32. package/src/internal/Configuration.ts +5 -4
  33. package/src/internal/VariableSets.ts +104 -24
  34. package/src/react-native/internal/Tokens.ts +5 -6
  35. package/src/vite/index.ts +3 -9
  36. package/src/web/internal/Themes.ts +59 -13
@@ -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,
@@ -32,12 +32,11 @@ export function resolve(
32
32
  (metadata.contract[Token.identity] !== undefined &&
33
33
  metadata.contract[Token.identity] ===
34
34
  value.contract[Token.identity]))
35
- const next = shared
36
- ? Object.hasOwn(metadata.values, value.path)
37
- ? metadata.values[value.path]
38
- : undefined
39
- : value.value
40
- if (next === undefined) throw new Error('Theme is missing a live token.')
35
+ // A set sharing its contract with an extension lacks the extension's added paths.
36
+ const next =
37
+ shared && Object.hasOwn(metadata.values, value.path)
38
+ ? metadata.values[value.path]!
39
+ : value.value
41
40
  active.add(value)
42
41
  const result = scalar(next)
43
42
  active.delete(value)
package/src/vite/index.ts CHANGED
@@ -907,15 +907,9 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
907
907
  url += target.slice(raw.length)
908
908
  }
909
909
  } else {
910
- const pathname = file
911
- .replaceAll('\\', '/')
912
- .split('/')
913
- .map((part, index) =>
914
- index === 0 && /^[a-z]:$/i.test(part)
915
- ? part
916
- : encodeURIComponent(part),
917
- )
918
- .join('/')
910
+ // Vite reads `/@fs/` URLs with `decodeURI`, which keeps reserved escapes
911
+ // such as `%40` in scoped package paths, so those characters stay raw.
912
+ const pathname = encodeURI(file.replaceAll('\\', '/'))
919
913
 
920
914
  url = `/@fs/${pathname}${target.slice(raw.length)}`
921
915
  }
@@ -70,6 +70,7 @@ export function create() {
70
70
  const completeDefaults = new Set<Token.Contract>()
71
71
  const empty: string[] = []
72
72
  const rules: Rule[] = []
73
+ const varied = variedPaths(themes)
73
74
 
74
75
  for (const [name, theme] of Object.entries(themes)) {
75
76
  if (!name) throw new Error('Theme names must be nonempty.')
@@ -130,8 +131,14 @@ export function create() {
130
131
  const paths = [...contract.paths]
131
132
  for (const [path, name] of paths) {
132
133
  const value = data.values[path]
133
- if (value === undefined)
134
- throw new Error('Theme scope is missing a live token.')
134
+ // A set sharing its contract with an extension lacks the extension's added paths.
135
+ if (value === undefined) continue
136
+ // An inherited base reference resolves through its fallback, unless a sibling set overrides it.
137
+ if (
138
+ inherited(value, path, data.contract) &&
139
+ !varied.get(data.contract)?.has(path)
140
+ )
141
+ continue
135
142
 
136
143
  const label = path
137
144
  declarations.push({
@@ -144,15 +151,16 @@ export function create() {
144
151
 
145
152
  rules.push(...declarations)
146
153
  for (const [path, name] of contract.paths)
147
- conditional(
148
- data.values[path]!,
149
- name,
150
- `.${className}`,
151
- rules,
152
- path,
153
- [],
154
- data.contract,
155
- )
154
+ if (Object.hasOwn(data.values, path))
155
+ conditional(
156
+ data.values[path]!,
157
+ name,
158
+ `.${className}`,
159
+ rules,
160
+ path,
161
+ [],
162
+ data.contract,
163
+ )
156
164
  }
157
165
 
158
166
  for (const rule of rules) {
@@ -257,9 +265,9 @@ export function create() {
257
265
  if ('default' in value) {
258
266
  // Separate fallback properties preserve extensions and resolve references within each scope.
259
267
  const base = literal(value.default, label, owner)
260
- // Rule consolidation must not change existing variable identities.
268
+ // Rule consolidation must not change existing variable identities, so the name hashes the fallback's CSS.
261
269
  const css = `:where(*){--fallback:${base};}${conditionalCss(value, '--fallback', ':where(*)', label, owner)}`
262
- const name = `${owner ? property(owner, label ?? 'value') : variable(undefined, label ?? 'value')}-fallback-${Identity.name(css)}`
270
+ const name = `${owner ? property(owner, label ?? 'value') : variable(undefined, label ?? 'value')}-fallback-${Identity.hash(css)}`
263
271
  const emitted: Rule[] = [
264
272
  { conditions: [], property: name, selector: ':where(*)', value: base },
265
273
  ]
@@ -409,6 +417,44 @@ export function scope(value: string, identity?: string): string {
409
417
  return `z-theme-${namespace}${Identity.name(value)}`
410
418
  }
411
419
 
420
+ /** Whether a set's value is its base's own reference at the same path, as named extensions inherit. */
421
+ function inherited(value: Token.Value, path: string, contract: Token.Contract) {
422
+ return Token.is(value) && value.path === path && value.contract !== contract
423
+ }
424
+
425
+ /** Paths whose values differ between sets that share a contract. */
426
+ function variedPaths(
427
+ themes: Readonly<Record<string, Theme.Definition | Vars.Definition>>,
428
+ ) {
429
+ const seen = new Map<Token.Contract, Map<string, string>>()
430
+ const varied = new Map<Token.Contract, Set<string>>()
431
+
432
+ for (const theme of Object.values(themes)) {
433
+ const data = Object.getOwnPropertyDescriptor(theme, Token.definition)
434
+ ?.value as Token.Metadata | undefined
435
+ if (!data) continue
436
+
437
+ const values = seen.get(data.contract) ?? new Map<string, string>()
438
+ seen.set(data.contract, values)
439
+
440
+ for (const [path, value] of Object.entries(data.values)) {
441
+ const key = Token.is(value)
442
+ ? JSON.stringify([value.contract[Token.identity], value.path])
443
+ : JSON.stringify(value)
444
+ const previous = values.get(path)
445
+
446
+ if (previous === undefined) values.set(path, key)
447
+ else if (previous !== key) {
448
+ const paths = varied.get(data.contract) ?? new Set<string>()
449
+ paths.add(path)
450
+ varied.set(data.contract, paths)
451
+ }
452
+ }
453
+ }
454
+
455
+ return varied
456
+ }
457
+
412
458
  function variable(identity: string | undefined, path: string): string {
413
459
  const namespace = identity?.startsWith('id-') ? `${identity.slice(3)}-` : ''
414
460
  return `--z-${namespace}${path.split('.').map(Identity.name).join('-')}`