zyzz 0.0.8 → 0.0.9

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.
@@ -29,7 +29,10 @@ export type Accepted<style, properties> = {
29
29
  }
30
30
 
31
31
  type AcceptedValue<value, property extends PropertyKey, properties> =
32
- Extract<value, `[${string}]` | `[${string}] !important`> extends never
32
+ Extract<
33
+ value,
34
+ `${string} !custom` | `${string} !custom !important`
35
+ > extends never
33
36
  ? AcceptedMember<Record<property, value>, properties>[property]
34
37
  : value extends unknown
35
38
  ? AcceptedMember<Record<property, value>, properties>[property]
@@ -37,22 +40,15 @@ type AcceptedValue<value, property extends PropertyKey, properties> =
37
40
 
38
41
  type AcceptedMember<style, properties> = {
39
42
  [property in keyof style]: property extends keyof properties
40
- ? style[property] extends `[${infer value}]` | `[${infer value}] !important`
41
- ?
42
- | (property extends 'gridTemplateColumns' | 'gridTemplateRows'
43
- ? style[property] extends LiteralAccepted<
44
- Pick<style, property>,
45
- Literal.Properties
46
- >[property]
47
- ? style[property]
48
- : never
49
- : never)
50
- | (ScalarValue<value, property> extends LiteralAccepted<
51
- Record<property, ScalarValue<value, property>>,
52
- Literal.Properties
53
- >[property]
54
- ? style[property]
55
- : never)
43
+ ? style[property] extends
44
+ | `${infer value} !custom`
45
+ | `${infer value} !custom !important`
46
+ ? ScalarValue<value, property> extends LiteralAccepted<
47
+ Record<property, ScalarValue<value, property>>,
48
+ Literal.Properties
49
+ >[property]
50
+ ? style[property]
51
+ : never
56
52
  : style[property] extends readonly [unknown, ...unknown[]]
57
53
  ? AcceptedArray<style[property], property, properties>
58
54
  : LiteralAccepted<Pick<style, property>, properties>[property]
@@ -135,15 +131,11 @@ type Fold<value> = value extends string
135
131
  /** Refines concrete scalar spellings; already-broad property contracts need no literal refinement. */
136
132
  export type Checked<style, tokens = {}> = {
137
133
  [property in keyof style]: style[property] extends
138
- | `[${infer value}]`
139
- | `[${infer value}] !important`
140
- ?
141
- | (property extends 'gridTemplateColumns' | 'gridTemplateRows'
142
- ? LiteralChecked<Pick<style, property>>[property]
143
- : never)
144
- | LiteralChecked<
145
- Record<property, ScalarValue<value, property>>
146
- >[property]
134
+ | `${infer value} !custom`
135
+ | `${infer value} !custom !important`
136
+ ? value extends `${string} !${'custom' | 'important'}`
137
+ ? never
138
+ : LiteralChecked<Record<property, ScalarValue<value, property>>>[property]
147
139
  : style[property] extends readonly [unknown, ...unknown[]]
148
140
  ? CheckedArray<style[property], property, tokens>
149
141
  : LiteralChecked<Pick<style, property>, tokens>[property]
@@ -214,8 +206,8 @@ export type Tokens<
214
206
  | Token.Reference
215
207
  | Token.Variable
216
208
  | `${Token.Variable} !important`
217
- | `[${string}]`
218
- | `[${string}] !important`
209
+ | `${string} !custom`
210
+ | `${string} !custom !important`
219
211
  ? unknown
220
212
  : value extends Atom<Token.Names<tokens, property>>
221
213
  ? unknown
@@ -361,7 +353,7 @@ export type Fallbacks<atom> = atom | readonly [atom, ...atom[]]
361
353
  /** One declaration or a nonempty ordered sequence of declaration fallbacks. */
362
354
  export type Input<value> = Fallbacks<Atom<Exclude<value, undefined>>>
363
355
 
364
- /** Splits a trailing importance marker without interpreting quoted or escaped text. */
356
+ /** Splits trailing custom and importance markers without interpreting quoted or escaped text. */
365
357
  export function parse(
366
358
  input: unknown,
367
359
  property: keyof Literal.Properties,
@@ -383,16 +375,6 @@ export function parse(
383
375
 
384
376
  const parts = [...input.parts]
385
377
  let remaining = text.length - String(parsed.value).length
386
- if (parsed.custom) {
387
- for (let index = 0; index < parts.length; index++) {
388
- const part = parts[index]
389
- if (part === '') continue
390
- if (typeof part !== 'string') return undefined
391
- parts[index] = part.slice(1)
392
- break
393
- }
394
- remaining--
395
- }
396
378
 
397
379
  for (let index = parts.length - 1; remaining > 0 && index >= 0; index--) {
398
380
  const part = parts[index]
@@ -434,37 +416,38 @@ export function parse(
434
416
  '',
435
417
  )
436
418
 
437
- if (!escaped(marker) && (suffix === '' || suffix === 'important')) break
419
+ if (
420
+ !escaped(marker) &&
421
+ (suffix === '' || suffix === 'important' || suffix === 'custom')
422
+ )
423
+ break
438
424
 
439
425
  marker = marker === 0 ? -1 : input.lastIndexOf('!', marker - 1)
440
426
  }
441
427
 
442
- if (marker < 0) {
443
- if (bracketed(input))
444
- return {
445
- custom: true,
446
- important: false,
447
- value: scalar(input.slice(1, -1)),
448
- }
449
- return undefined
450
- }
451
- if (input.slice(marker - 1) !== ' !important' || escaped(marker - 1))
428
+ if (marker < 0) return undefined
429
+ const suffix = input.slice(marker - 1)
430
+ if (
431
+ (suffix !== ' !important' && suffix !== ' !custom') ||
432
+ escaped(marker - 1)
433
+ )
452
434
  return { invalid: true }
453
435
 
454
436
  let end = marker
455
-
456
437
  while (end > 0 && /[ \t\n\r\f]/.test(input[end - 1]!) && !escaped(end - 1))
457
438
  end--
458
439
 
459
440
  const body = input.slice(0, end)
460
- const custom = bracketed(body)
461
- const text = custom ? body.slice(1, -1) : body
462
- const value = scalar(text)
441
+ if (!body) return { invalid: true }
442
+ const nested = parse(body, property)
443
+ if (nested && 'invalid' in nested) return nested
444
+ const custom = suffix === ' !custom'
445
+ if (nested && (custom || nested.important)) return { invalid: true }
463
446
 
464
447
  return {
465
- ...(custom ? { custom: true } : {}),
466
- important: true as const,
467
- value,
448
+ ...(custom || nested?.custom ? { custom: true } : {}),
449
+ important: !custom,
450
+ value: nested?.value ?? scalar(body),
468
451
  }
469
452
 
470
453
  function scalar(text: string): number | string {
@@ -476,34 +459,3 @@ export function parse(
476
459
  : text
477
460
  }
478
461
  }
479
-
480
- /** Recognizes one enclosing pair without consuming native grid line lists. */
481
- function bracketed(value: string): boolean {
482
- if (!value.startsWith('[') || !value.endsWith(']')) return false
483
- let depth = 0
484
- let quote = ''
485
- for (let index = 0; index < value.length; index++) {
486
- const character = value[index]!
487
- if (character === '\\') {
488
- index++
489
- continue
490
- }
491
- if (quote) {
492
- if (character === quote) quote = ''
493
- continue
494
- }
495
- if (character === '/' && value[index + 1] === '*') {
496
- const end = value.indexOf('*/', index + 2)
497
- if (end < 0) return false
498
- index = end + 1
499
- continue
500
- }
501
- if (character === '"' || character === "'") {
502
- quote = character
503
- continue
504
- }
505
- if (character === '[') depth++
506
- if (character === ']' && --depth === 0) return index === value.length - 1
507
- }
508
- return false
509
- }
@@ -577,9 +577,8 @@ const portable = createUnplugin<Options | undefined, false>(
577
577
  },
578
578
  webpack(compiler) {
579
579
  compiler.hooks.watchRun.tap('zyzz', () => {
580
- // Directory invalidations must also discard cached source reads before the graph and loaders rebuild.
581
- for (const file of compiler.modifiedFiles ?? [])
582
- if (directories.has(file)) compiler.inputFileSystem?.purge?.(file)
580
+ // The graph rereads every source, including files absent from Webpack's invalidation batch.
581
+ for (const file of files) compiler.inputFileSystem?.purge?.(file)
583
582
  })
584
583
  },
585
584
  }
package/src/vite/index.ts CHANGED
@@ -47,6 +47,18 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
47
47
  ReturnType<typeof Snapshot.create>
48
48
  >()
49
49
  const states = new WeakMap<Environment, Map<string, Entry>>()
50
+ const removed = new WeakMap<Environment, Set<string>>()
51
+ const reads = new WeakMap<Environment, Map<string, string>>()
52
+ const compilers = new WeakMap<Environment, Graph.create.ReturnType>()
53
+
54
+ function compiler(environment: Environment) {
55
+ let value = compilers.get(environment)
56
+ if (!value) {
57
+ value = Graph.create()
58
+ compilers.set(environment, value)
59
+ }
60
+ return value
61
+ }
50
62
  const discoveries = new WeakMap<Environment, Promise<Map<string, string>>>()
51
63
  const contributionFiles = new WeakMap<Environment, Set<string>>()
52
64
  const assets = new Map<string, string>()
@@ -55,6 +67,8 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
55
67
  const graphs = new Map<string, ReadonlySet<string>>()
56
68
  const initializers = new WeakMap<Environment, Entry>()
57
69
  const sourceEntrypoints = new Set<string>()
70
+ const identities = new Map<string, string>()
71
+ const lazyImports = new WeakMap<Parser.ParseResult, string[]>()
58
72
  let root: string
59
73
 
60
74
  function snapshot(environment: Environment) {
@@ -78,7 +92,12 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
78
92
  }
79
93
 
80
94
  function sourceId(file: string) {
81
- return `app/${Path.relative(root, file).split(Path.sep).join('/')}`
95
+ let id = identities.get(file)
96
+ if (!id) {
97
+ id = `app/${Path.relative(root, file).split(Path.sep).join('/')}`
98
+ identities.set(file, id)
99
+ }
100
+ return id
82
101
  }
83
102
 
84
103
  function resource(id: string) {
@@ -288,9 +307,19 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
288
307
  file: string,
289
308
  event: string,
290
309
  ) {
310
+ reads.delete(environment)
291
311
  if (event === 'delete') {
312
+ if (entries(environment).has(file)) {
313
+ let files = removed.get(environment)
314
+ if (!files) {
315
+ files = new Set()
316
+ removed.set(environment, files)
317
+ }
318
+ files.add(file)
319
+ }
292
320
  entries(environment).delete(file)
293
321
  snapshot(environment).delete(file, sourceId(file))
322
+ identities.delete(file)
294
323
  }
295
324
 
296
325
  const pending = discoveries.get(environment)
@@ -348,7 +377,17 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
348
377
  host.watch(file)
349
378
 
350
379
  const id = sourceId(file)
351
- const text = source ?? (await snapshot(entry.environment).read(file))
380
+ let cached = reads.get(entry.environment)
381
+ if (!cached) {
382
+ cached = new Map()
383
+ reads.set(entry.environment, cached)
384
+ }
385
+ // Full-project compilation must not reuse reads from concurrent module transforms.
386
+ const text =
387
+ source ??
388
+ (everything ? undefined : cached.get(file)) ??
389
+ (await snapshot(entry.environment).read(file))
390
+ if (!everything) cached.set(file, text)
352
391
 
353
392
  modules[id] = text
354
393
 
@@ -452,18 +491,22 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
452
491
  }
453
492
 
454
493
  // Lazy imports stay outside this compile but scope the documents that load them.
455
- const specifiers: string[] = []
456
-
457
- Walker.walk(parsed.program, {
458
- enter(node) {
459
- if (
460
- node.type === 'ImportExpression' &&
461
- node.source.type === 'Literal' &&
462
- typeof node.source.value === 'string'
463
- )
464
- specifiers.push(node.source.value)
465
- },
466
- })
494
+ let specifiers = lazyImports.get(parsed)
495
+ if (!specifiers) {
496
+ specifiers = []
497
+ const collected = specifiers
498
+ Walker.walk(parsed.program, {
499
+ enter(node) {
500
+ if (
501
+ node.type === 'ImportExpression' &&
502
+ node.source.type === 'Literal' &&
503
+ typeof node.source.value === 'string'
504
+ )
505
+ collected.push(node.source.value)
506
+ },
507
+ })
508
+ lazyImports.set(parsed, specifiers)
509
+ }
467
510
 
468
511
  const targets = new Set<string>()
469
512
 
@@ -490,8 +533,9 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
490
533
  const selected = contributionFiles.get(entry.environment)?.has(file)
491
534
 
492
535
  if (allSources) {
493
- const { program } = Parser.parseSync('source.tsx', source, {
494
- sourceType: 'module',
536
+ const { program } = snapshot(entry.environment).parse({
537
+ moduleId: sourceId(file),
538
+ source,
495
539
  })
496
540
  const dynamic: import('@oxc-project/types').ImportExpression[] = []
497
541
 
@@ -676,6 +720,8 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
676
720
  entry.files = files
677
721
  const map = Mapping.toEncodedMap(new Mapping.GenMapping())
678
722
  return {
723
+ imports,
724
+ modules: result.modules,
679
725
  code: output.code,
680
726
  contractIds: Object.keys(result.contracts),
681
727
  css: '',
@@ -917,6 +963,8 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
917
963
  entry.files = files
918
964
 
919
965
  return {
966
+ imports,
967
+ modules: result.modules,
920
968
  code: output.code,
921
969
  contractIds: Object.keys(compiled),
922
970
  css: styles.join('\n'),
@@ -935,6 +983,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
935
983
  /** Compiles every discovered source once so index.html can inline each configuration's script before modules load. */
936
984
  async function initializations(server: ViteDevServer) {
937
985
  const environment = server.environments.client
986
+ reads.delete(environment)
938
987
  const host: Host = {
939
988
  asset: async (file) => file,
940
989
  resolve: (source, importer) =>
@@ -950,7 +999,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
950
999
  if (file === undefined) return []
951
1000
 
952
1001
  entry = {
953
- compiler: Graph.create(),
1002
+ compiler: compiler(environment),
954
1003
  environment,
955
1004
  file,
956
1005
  files: new Set([file]),
@@ -1117,6 +1166,9 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1117
1166
  }
1118
1167
 
1119
1168
  return {
1169
+ buildStart() {
1170
+ reads.delete(this.environment)
1171
+ },
1120
1172
  config: {
1121
1173
  order: 'post',
1122
1174
  handler(config) {
@@ -1261,27 +1313,73 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1261
1313
  async watchChange(file, change) {
1262
1314
  await updateDiscovery(this.environment, file, change.event)
1263
1315
  },
1264
- async hotUpdate({ file, modules, timestamp, type }) {
1316
+ async hotUpdate({ file, modules, timestamp, type, server }) {
1265
1317
  await updateDiscovery(this.environment, file, type)
1266
1318
 
1267
- const affected = new Set(modules)
1319
+ const state = entries(this.environment)
1320
+ const first = [...state.values()].find(
1321
+ (entry) => entry.files.has(file) || eager(file),
1322
+ )
1323
+ if (!first && !removed.get(this.environment)?.has(file)) return modules
1324
+
1325
+ // Failed edits retain normal invalidation so Vite reports errors on reload.
1326
+ const output =
1327
+ first && type === 'update' && eligible(file)
1328
+ ? await compile(
1329
+ first,
1330
+ {
1331
+ resolve: (source, importer) =>
1332
+ this.environment.pluginContainer.resolveId(source, importer),
1333
+ watch: (file) => {
1334
+ server.watcher.add(file)
1335
+ },
1336
+ asset: async (file) => file,
1337
+ },
1338
+ undefined,
1339
+ true,
1340
+ true,
1341
+ ).catch(() => undefined)
1342
+ : undefined
1343
+ const affected = new Set(
1344
+ modules.filter((module) => {
1345
+ const entry = module.id && state.get(module.id)
1346
+ return (
1347
+ !output ||
1348
+ !entry ||
1349
+ entry.code !== output.modules[sourceId(entry.file)]?.code ||
1350
+ entry.imports !==
1351
+ JSON.stringify(output.imports[sourceId(entry.file)])
1352
+ )
1353
+ }),
1354
+ )
1355
+ const ids = new Set<string>()
1356
+ if (type === 'delete') ids.add(cssId(file))
1268
1357
 
1269
1358
  for (const entry of entries(this.environment).values()) {
1270
1359
  if (!entry.files.has(file) && !eager(file)) continue
1271
1360
 
1272
1361
  // Theme scopes affect CSS even when Vite's JavaScript import was erased.
1273
- for (const id of [entry.file, cssId(entry.file), sharedId]) {
1274
- const module = this.environment.moduleGraph.getModuleById(id)
1275
- if (!module) continue
1276
-
1277
- this.environment.moduleGraph.invalidateModule(
1278
- module,
1279
- new Set(),
1280
- timestamp,
1281
- true,
1282
- )
1283
- affected.add(module)
1284
- }
1362
+ if (
1363
+ !output ||
1364
+ entry.code !== output.modules[sourceId(entry.file)]?.code ||
1365
+ entry.imports !== JSON.stringify(output.imports[sourceId(entry.file)])
1366
+ )
1367
+ ids.add(entry.file)
1368
+ ids.add(cssId(entry.file))
1369
+ ids.add(sharedId)
1370
+ }
1371
+
1372
+ for (const id of ids) {
1373
+ const module = this.environment.moduleGraph.getModuleById(id)
1374
+ if (!module) continue
1375
+
1376
+ this.environment.moduleGraph.invalidateModule(
1377
+ module,
1378
+ new Set(),
1379
+ timestamp,
1380
+ true,
1381
+ )
1382
+ affected.add(module)
1285
1383
  }
1286
1384
 
1287
1385
  return [...affected]
@@ -1298,6 +1396,8 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1298
1396
  file === undefined
1299
1397
  ? entries(this.environment).values().next().value
1300
1398
  : entries(this.environment).get(file)
1399
+ // A pruned JavaScript module can leave its stylesheet loaded in the browser.
1400
+ if (!entry && file && removed.get(this.environment)?.has(file)) return ''
1301
1401
  if (!entry) throw new Error(`Unknown Zyzz stylesheet: ${file}`)
1302
1402
 
1303
1403
  const output = await compile(
@@ -1353,12 +1453,13 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1353
1453
 
1354
1454
  if (!entry) {
1355
1455
  entry = {
1356
- compiler: Graph.create(),
1456
+ compiler: compiler(this.environment),
1357
1457
  environment: this.environment,
1358
1458
  file: id,
1359
1459
  files: new Set([id]),
1360
1460
  }
1361
1461
  state.set(id, entry)
1462
+ removed.get(this.environment)?.delete(id)
1362
1463
  }
1363
1464
 
1364
1465
  const output = await compile(
@@ -1380,6 +1481,9 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1380
1481
  code,
1381
1482
  )
1382
1483
 
1484
+ entry.imports = JSON.stringify(output.imports[sourceId(entry.file)])
1485
+ entry.code = output.code
1486
+
1383
1487
  if (native) return { code: output.code, map: JSON.stringify(output.map) }
1384
1488
 
1385
1489
  // Keep the CSS dependency even when the current graph has no live rules.
@@ -1479,6 +1583,8 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1479
1583
  }
1480
1584
 
1481
1585
  type Entry = {
1586
+ code?: string
1587
+ imports?: string
1482
1588
  environment: Environment
1483
1589
  compiler: Graph.create.ReturnType
1484
1590
  file: string