zyzz 0.0.16 → 0.0.17

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.
@@ -238,8 +238,7 @@ async function compile(context: Context, source: string) {
238
238
  const hash = Crypto.createHash('sha256')
239
239
  .update(
240
240
  options.development
241
- ? (stylesheet.id ??
242
- JSON.stringify([context.resourcePath, index, graph.dependencies]))
241
+ ? (stylesheet.id ?? JSON.stringify([context.resourcePath, index]))
243
242
  : css,
244
243
  )
245
244
  .digest('hex')
@@ -265,33 +264,66 @@ async function compile(context: Context, source: string) {
265
264
  }
266
265
  let request = file
267
266
  if (options.development) {
268
- // Server-component CSS needs a client graph edge for Next's native CSS HMR.
269
- request = `${file}.js`
270
- const stylesheet =
267
+ request =
271
268
  options.bundler === 'turbopack'
272
- ? `./style.css?zyzz=${hash}`
273
- : `./${hash}.css`
274
- const code = `'use client';import ${JSON.stringify(stylesheet)};`
275
- const existing = await Fs.readFile(request, 'utf8').catch(
276
- (error: NodeJS.ErrnoException) => {
277
- if (error.code !== 'ENOENT') throw error
278
- return undefined
279
- },
280
- )
281
- if (existing !== code) await Fs.writeFile(request, code)
269
+ ? Path.join(directory, 'style.css') + `?zyzz=${hash}`
270
+ : file
282
271
  } else if (options.bundler === 'turbopack') {
283
272
  // Persist CSS with the cached transform, independently of generated filesystem artifacts.
284
273
  request =
285
274
  Path.join(directory, 'style.css') +
286
275
  `?zyzz=${hash}&css=${Buffer.from(css).toString('base64url')}`
287
276
  }
288
- const relative = Path.relative(Path.dirname(context.resourcePath), request)
277
+ const relative = Path.relative(
278
+ options.development ? directory : Path.dirname(context.resourcePath),
279
+ request,
280
+ )
289
281
  .split(Path.sep)
290
282
  .join('/')
291
283
  requests.push(
292
- `import ${JSON.stringify(relative.startsWith('../') ? relative : `./${relative}`)};`,
284
+ `${options.development ? '@import' : 'import'} ${JSON.stringify(relative.startsWith('../') ? relative : `./${relative}`)};`,
293
285
  )
294
286
  }
295
287
 
288
+ if (options.development && requests.length) {
289
+ // Keep the client boundary stable when a dependency changes the stylesheet imports.
290
+ const hash = Crypto.createHash('sha256')
291
+ .update(context.resourcePath)
292
+ .digest('hex')
293
+ const stylesheet = Path.join(directory, `${hash}.css`)
294
+ const css = requests.join('\n')
295
+ const previous = await Fs.readFile(stylesheet, 'utf8').catch(
296
+ (error: NodeJS.ErrnoException) => {
297
+ if (error.code !== 'ENOENT') throw error
298
+ return undefined
299
+ },
300
+ )
301
+ if (previous !== css) {
302
+ const temporary = `${stylesheet}.${Crypto.randomUUID()}.tmp`
303
+ await Fs.writeFile(temporary, css)
304
+ await Fs.rename(temporary, stylesheet)
305
+ }
306
+ const file = Path.join(directory, `${hash}.js`)
307
+ const request =
308
+ options.bundler === 'turbopack'
309
+ ? `./style.css?zyzz=${hash}`
310
+ : `./${hash}.css`
311
+ try {
312
+ await Fs.writeFile(
313
+ file,
314
+ `'use client';import ${JSON.stringify(request)};`,
315
+ { flag: 'wx' },
316
+ )
317
+ } catch (error) {
318
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
319
+ }
320
+ const relative = Path.relative(Path.dirname(context.resourcePath), file)
321
+ .split(Path.sep)
322
+ .join('/')
323
+ return {
324
+ code: `${output.code}\nimport ${JSON.stringify(relative.startsWith('../') ? relative : `./${relative}`)};`,
325
+ map,
326
+ }
327
+ }
296
328
  return { code: `${output.code}\n${requests.join('\n')}`, map }
297
329
  }
package/src/vite/index.ts CHANGED
@@ -724,6 +724,8 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
724
724
  modules: result.modules,
725
725
  code: output.code,
726
726
  contractIds: Object.keys(result.contracts),
727
+ configurations: new Map<string, Catalogs.Catalog>(),
728
+ connections: staged,
727
729
  css: '',
728
730
  sharedCss: '',
729
731
  cssMap: map,
@@ -758,9 +760,13 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
758
760
  for (const key of catalogs.keys())
759
761
  if (key.startsWith(`${id}\0`)) catalogs.delete(key)
760
762
 
763
+ const configurations = new Map<string, Catalogs.Catalog>()
761
764
  for (const [id, contract] of Object.entries(compiled))
762
- for (const configuration of Catalogs.read(contract))
763
- catalogs.set(`${id}\0${configuration.identity}`, configuration)
765
+ for (const configuration of Catalogs.read(contract)) {
766
+ const key = `${id}\0${configuration.identity}`
767
+ catalogs.set(key, configuration)
768
+ configurations.set(key, configuration)
769
+ }
764
770
 
765
771
  const map = new Mapping.GenMapping()
766
772
  const styles: string[] = []
@@ -967,6 +973,8 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
967
973
  modules: result.modules,
968
974
  code: output.code,
969
975
  contractIds: Object.keys(compiled),
976
+ configurations,
977
+ connections: staged,
970
978
  css: styles.join('\n'),
971
979
  sharedCss: new TextDecoder().decode(shared.code),
972
980
  sharedCssMap: sharedMap,
@@ -996,7 +1004,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
996
1004
  // The seed only anchors the compile, which visits every discovered source.
997
1005
  if (!entry || !sources.has(entry.file)) {
998
1006
  const file = sources.keys().next().value
999
- if (file === undefined) return []
1007
+ if (file === undefined) return { catalogs: new Map(), edges: new Map() }
1000
1008
 
1001
1009
  entry = {
1002
1010
  compiler: compiler(environment),
@@ -1019,7 +1027,16 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1019
1027
  if (!output.contractIds.some((id) => key.startsWith(`${id}\0`)))
1020
1028
  catalogs.delete(key)
1021
1029
 
1022
- return [...catalogs.values()]
1030
+ // Concurrent module transforms must not replace this request's compiled catalogs.
1031
+ return {
1032
+ catalogs: output?.configurations ?? new Map(catalogs),
1033
+ edges: output?.connections ?? new Map(edges),
1034
+ }
1035
+ }
1036
+
1037
+ type DocumentState = {
1038
+ catalogs: ReadonlyMap<string, Catalogs.Catalog>
1039
+ edges: ReadonlyMap<string, ReadonlySet<string>>
1023
1040
  }
1024
1041
 
1025
1042
  /** Catalogs reachable from a served document's module scripts; every catalog when none resolve. */
@@ -1027,6 +1044,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1027
1044
  html: string,
1028
1045
  filename: string,
1029
1046
  resolve: Host['resolve'],
1047
+ { catalogs, edges }: DocumentState,
1030
1048
  ) {
1031
1049
  const reachable = new Set<string>()
1032
1050
 
@@ -1560,7 +1578,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1560
1578
  if (!context.server)
1561
1579
  return entryCatalogs(context.bundle, context.chunk)
1562
1580
 
1563
- await initializations(context.server)
1581
+ const state = await initializations(context.server)
1564
1582
 
1565
1583
  const environment = context.server.environments.client
1566
1584
 
@@ -1569,6 +1587,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1569
1587
  context.filename,
1570
1588
  (source, importer) =>
1571
1589
  environment.pluginContainer.resolveId(source, importer),
1590
+ state,
1572
1591
  )
1573
1592
  })()
1574
1593
  // Saved preferences apply before any other script or visible content.
package/src/web/Css.ts CHANGED
@@ -94,7 +94,7 @@ export function compile<
94
94
  // Logical dimensions may alias either physical axis in inherited writing modes.
95
95
  // Preserve physical-only factoring when no logical dimension is authored.
96
96
  const logicalSizing = [...properties].some((property) =>
97
- /^(min|max)?(blockSize|inlineSize)$/i.test(property),
97
+ /^(min|max)?(blockSize|inlineSize)$/i.test(canonical(property)),
98
98
  )
99
99
  const resets = properties.has('all')
100
100
  const combinedLines = new Set<string>()
@@ -165,6 +165,7 @@ export function compile<
165
165
  [
166
166
  'whiteSpace',
167
167
  'whiteSpaceCollapse',
168
+ 'whiteSpaceTrim',
168
169
  'textWrap',
169
170
  'textWrapMode',
170
171
  'textWrapStyle',
@@ -18,6 +18,7 @@ export function create() {
18
18
  >()
19
19
 
20
20
  const defaults = new Map<string, readonly Rule[]>()
21
+ const defaultGroups = new Map<Token.Contract, Set<string>>()
21
22
 
22
23
  function serialize(token: Token.Reference): string {
23
24
  const value = token.value
@@ -42,7 +43,7 @@ export function create() {
42
43
  }
43
44
 
44
45
  const label = contract.identity?.startsWith('src-') ? token.path : undefined
45
- return `var(${name},${literal(value, label)})`
46
+ return `var(${name},${literal(value, label, token.contract)})`
46
47
  }
47
48
 
48
49
  function emit(
@@ -51,6 +52,7 @@ export function create() {
51
52
  separate?: 'all' | 'defaults',
52
53
  ) {
53
54
  const classes: Record<string, string> = Object.create(null)
55
+ const completeDefaults = new Set<Token.Contract>()
54
56
  const empty: string[] = []
55
57
  const rules: Rule[] = []
56
58
 
@@ -97,6 +99,16 @@ export function create() {
97
99
  }
98
100
 
99
101
  if (!contract) continue
102
+ // Independently compiled consumers must import identical fallback contents.
103
+ if (separate === 'all' && contract.identity) {
104
+ completeDefaults.add(data.contract)
105
+ for (const [path, value] of Object.entries(data.values))
106
+ literal(
107
+ value,
108
+ contract.identity.startsWith('src-') ? path : undefined,
109
+ data.contract,
110
+ )
111
+ }
100
112
  if (!contract.paths.size) empty.push(`.${className}{}`)
101
113
 
102
114
  const declarations: Rule[] = []
@@ -112,7 +124,7 @@ export function create() {
112
124
  conditions: [],
113
125
  property: name,
114
126
  selector: `.${className}`,
115
- value: literal(value, label),
127
+ value: literal(value, label, data.contract),
116
128
  })
117
129
  }
118
130
 
@@ -124,6 +136,8 @@ export function create() {
124
136
  `.${className}`,
125
137
  rules,
126
138
  contract.identity?.startsWith('src-') ? path : undefined,
139
+ [],
140
+ data.contract,
127
141
  )
128
142
  }
129
143
 
@@ -134,6 +148,31 @@ export function create() {
134
148
  scopes.set(rule.property, entries)
135
149
  }
136
150
 
151
+ const grouped = new Set<string>()
152
+ const resources: Resource[] = []
153
+ if (separate === 'all')
154
+ for (const [contract, names] of defaultGroups) {
155
+ if (!completeDefaults.has(contract)) continue
156
+
157
+ // Sort independent properties only. Each property retains its authored conditional order.
158
+ const rules = [...names].sort().flatMap((name) => defaults.get(name)!)
159
+ resources.push({
160
+ css: render(rules),
161
+ id: JSON.stringify([
162
+ contract[Token.identity],
163
+ 'fallbacks',
164
+ Object.entries(themes)
165
+ .filter(
166
+ ([, theme]) => theme[Token.definition].contract === contract,
167
+ )
168
+ .map(([name]) => name)
169
+ .sort(),
170
+ ]),
171
+ rules,
172
+ })
173
+ for (const name of names) grouped.add(name)
174
+ }
175
+
137
176
  return {
138
177
  classes: Object.freeze(classes),
139
178
  css:
@@ -151,11 +190,14 @@ export function create() {
151
190
  .join('\n'),
152
191
  resources: separate
153
192
  ? [
154
- ...[...defaults].map(([id, rules]) => ({
155
- css: render(rules),
156
- id,
157
- rules,
158
- })),
193
+ ...resources,
194
+ ...[...defaults]
195
+ .filter(([id]) => !grouped.has(id))
196
+ .map(([id, rules]) => ({
197
+ css: render(rules),
198
+ id,
199
+ rules,
200
+ })),
159
201
  // Keep every scope of a property together so overlapping themes retain precedence.
160
202
  ...(separate === 'all'
161
203
  ? [...scopes].map(([property, rules]) => ({
@@ -172,25 +214,34 @@ export function create() {
172
214
  }
173
215
  }
174
216
 
175
- function literal(value: Token.Value, label?: string): string {
217
+ function literal(
218
+ value: Token.Value,
219
+ label?: string,
220
+ owner?: Token.Contract,
221
+ ): string {
176
222
  if (Token.is(value)) return serialize(value)
177
223
  if (typeof value !== 'object') return String(value)
178
224
  if ('default' in value) {
179
225
  // Separate fallback properties preserve extensions and resolve references within each scope.
180
- const base = literal(value.default, label)
226
+ const base = literal(value.default, label, owner)
181
227
  // Rule consolidation must not change existing variable identities.
182
- const css = `:where(*){--fallback:${base};}${conditionalCss(value, '--fallback', ':where(*)', label)}`
228
+ const css = `:where(*){--fallback:${base};}${conditionalCss(value, '--fallback', ':where(*)', label, owner)}`
183
229
  const name = label
184
230
  ? `--z-${Identity.label(label)}-fallback-${Identity.compact(css)}`
185
231
  : `--z-f${Identity.hash(css)}`
186
232
  const emitted: Rule[] = [
187
233
  { conditions: [], property: name, selector: ':where(*)', value: base },
188
234
  ]
189
- conditional(value, name, ':where(*)', emitted, label)
235
+ conditional(value, name, ':where(*)', emitted, label, [], owner)
190
236
  defaults.set(name, emitted)
237
+ if (owner) {
238
+ const names = defaultGroups.get(owner) ?? new Set<string>()
239
+ names.add(name)
240
+ defaultGroups.set(owner, names)
241
+ }
191
242
  return `var(${name})`
192
243
  }
193
- return `light-dark(${literal(value.light, label)},${literal(value.dark, label)})`
244
+ return `light-dark(${literal(value.light, label, owner)},${literal(value.dark, label, owner)})`
194
245
  }
195
246
 
196
247
  function conditionalCss(
@@ -198,6 +249,7 @@ export function create() {
198
249
  property: string,
199
250
  selector: string,
200
251
  label?: string,
252
+ owner?: Token.Contract,
201
253
  ): string {
202
254
  if (
203
255
  !value ||
@@ -207,12 +259,12 @@ export function create() {
207
259
  )
208
260
  return ''
209
261
  return (
210
- conditionalCss(value.default, property, selector, label) +
262
+ conditionalCss(value.default, property, selector, label, owner) +
211
263
  Object.entries(value)
212
264
  .filter(([query]) => query !== 'default')
213
265
  .map(
214
266
  ([query, entry]) =>
215
- `${query}{${selector}{${property}:${literal(entry, label)};}${conditionalCss(entry, property, selector, label)}}`,
267
+ `${query}{${selector}{${property}:${literal(entry, label, owner)};}${conditionalCss(entry, property, selector, label, owner)}}`,
216
268
  )
217
269
  .join('')
218
270
  )
@@ -225,6 +277,7 @@ export function create() {
225
277
  rules: Rule[],
226
278
  label?: string,
227
279
  conditions: readonly string[] = [],
280
+ owner?: Token.Contract,
228
281
  ) {
229
282
  if (
230
283
  !value ||
@@ -233,7 +286,7 @@ export function create() {
233
286
  !('default' in value)
234
287
  )
235
288
  return
236
- conditional(value.default, name, selector, rules, label, conditions)
289
+ conditional(value.default, name, selector, rules, label, conditions, owner)
237
290
  for (const [query, entry] of Object.entries(value)) {
238
291
  if (query === 'default') continue
239
292
  const nested = [...conditions, query]
@@ -241,9 +294,9 @@ export function create() {
241
294
  conditions: nested,
242
295
  property: name,
243
296
  selector,
244
- value: literal(entry, label),
297
+ value: literal(entry, label, owner),
245
298
  })
246
- conditional(entry, name, selector, rules, label, nested)
299
+ conditional(entry, name, selector, rules, label, nested, owner)
247
300
  }
248
301
  }
249
302