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
@@ -1588,7 +1588,10 @@ const properties = {
1588
1588
  direction: { kind: 'enum', values: ['ltr', 'rtl'] },
1589
1589
  display: {
1590
1590
  kind: 'enum',
1591
+ // The Compat Standard keeps the legacy box keywords that -webkit-line-clamp requires.
1591
1592
  values: [
1593
+ '-webkit-box',
1594
+ '-webkit-inline-box',
1592
1595
  'block',
1593
1596
  'block flex',
1594
1597
  'block flow',
@@ -38,6 +38,14 @@ export type StyleFactory<tokens extends Tokens> = {
38
38
  (values extends Binding.Inputs<values> ? unknown : never),
39
39
  options?: style.DefinitionOptions,
40
40
  ): style.Dynamic<values>
41
+ <const entries>(
42
+ styles: entries & {
43
+ readonly [index in keyof entries]: Entry<entries[index], tokens>
44
+ },
45
+ ...options: entries extends readonly unknown[]
46
+ ? [options?: style.DefinitionOptions]
47
+ : [invalid: never, unavailable: never]
48
+ ): style.ReturnType
41
49
  <const styles extends Record<string, unknown>>(
42
50
  styles: styles & NoInfer<Style.Accepted<styles, tokens>>,
43
51
  options?: style.DefinitionOptions,
@@ -56,6 +64,13 @@ export type StyleFactory<tokens extends Tokens> = {
56
64
  ): style.ReturnType
57
65
  }
58
66
 
67
+ // Distributes over mixed element unions so each member keeps its own contract.
68
+ type Entry<entry, tokens extends Tokens> = entry extends (
69
+ ...args: never
70
+ ) => unknown
71
+ ? style.Composable<entry>
72
+ : entry & NoInfer<Style.Accepted<entry, tokens>>
73
+
59
74
  /**
60
75
  * Defines scalar tokens and typography sets without defaults or environment access.
61
76
  * Colors currently use the supported literal color grammar; spacing and radius
@@ -29,9 +29,21 @@ type VariableOptions<input> = input extends {
29
29
  }
30
30
  : unknown
31
31
 
32
+ // Composition entries are static definitions or exact literal objects.
33
+ type Entries<entries> = {
34
+ readonly [index in keyof entries]: Entry<entries[index]>
35
+ }
36
+
37
+ // Distributes over mixed element unions so each member keeps its own contract.
38
+ type Entry<entry> = entry extends (...args: never) => unknown
39
+ ? style.Composable<entry>
40
+ : entry & NoInfer<Style.Accepted<entry, {}, true>> & Completion.Properties
41
+
32
42
  /**
33
43
  * Declares literal styles for source extraction. Requires a compile-time transform.
34
- * @param styles - Token-free literal CSS properties. Omit for an empty definition.
44
+ * An array composes static definitions and literal objects in order, so later entries
45
+ * win as later declarations do.
46
+ * @param styles - Token-free literal CSS properties, or an array of static definitions and literal objects. Omit for an empty definition.
35
47
  * @returns A callable style definition after source rewriting.
36
48
  * @throws {Error} When a definition needs an explicit identity and none is supplied.
37
49
  */
@@ -53,16 +65,29 @@ export function style<
53
65
  ? [options?: style.DefinitionOptions]
54
66
  : [invalid: never, unavailable: never]
55
67
  : [invalid: never, unavailable: never]
56
- ): style.Dynamic<values>
68
+ // TypeScript types a call that matches no overload with this signature. Other
69
+ // inputs leave `callback` at its constraint, so their definitions stay static.
70
+ ): ((...args: never[]) => unknown) extends callback
71
+ ? style.ReturnType
72
+ : style.Dynamic<values>
57
73
  export function style<const styles extends Record<string, unknown>>(
58
74
  styles: styles &
59
75
  NoInfer<Style.Accepted<styles, {}, true>> &
60
76
  Completion.Properties,
61
77
  options?: style.DefinitionOptions,
62
78
  ): style.ReturnType
79
+ export function style<const entries>(
80
+ styles: entries & Entries<entries>,
81
+ // Exclude non-array inputs by arity so they cannot mask object-property diagnostics.
82
+ ...options: entries extends readonly unknown[]
83
+ ? [options?: style.DefinitionOptions]
84
+ : [invalid: never, unavailable: never]
85
+ ): style.ReturnType
63
86
  export function style(
64
87
  styles?: unknown,
65
88
  options: style.DefinitionOptions = {},
89
+ // Matches the arity-gated overloads, whose invalid branches add parameters.
90
+ ..._: readonly never[]
66
91
  ): style.ReturnType {
67
92
  return Authoring.create(styles, options)
68
93
  }
@@ -73,6 +98,10 @@ type Reference = { readonly [identity]: true }
73
98
 
74
99
  /** Contracts for the literal authoring boundary. */
75
100
  export declare namespace style {
101
+ /** Accepts a static definition as a composition entry. Dynamic definitions and variants require cx. */
102
+ type Composable<entry> = entry extends Reference & (() => unknown)
103
+ ? entry
104
+ : ReturnType<Output>
76
105
  /** Stable identity for selectors and definitions that cannot be content-addressed. */
77
106
  type DefinitionOptions = { readonly id?: string | undefined }
78
107
  /** Callable compiled bindings with required scalar inputs and styling overrides. */
package/src/vite/index.ts CHANGED
@@ -9,16 +9,24 @@ import * as Contracts from '../node/internal/Contracts.js'
9
9
  import * as Mapping from '@jridgewell/gen-mapping'
10
10
  import * as Lightning from 'lightningcss'
11
11
  import * as Crypto from 'node:crypto'
12
+ import { existsSync } from 'node:fs'
12
13
  import * as Fs from 'node:fs/promises'
13
14
  import * as Path from 'node:path'
14
15
  import * as Reset from '../node/Reset.js'
15
- import { isBuiltin } from 'node:module'
16
+ import { createRequire, isBuiltin } from 'node:module'
16
17
  import * as Parser from 'oxc-parser'
17
18
  import * as Walker from 'oxc-walker'
18
19
  import * as Snapshot from '../node/internal/Snapshot.js'
19
20
  import * as Syntax from '../compiler/internal/Syntax.js'
20
21
  import * as Scope from '../compiler/internal/Scope.js'
21
- import type { Environment, Plugin, Rollup, ViteDevServer } from 'vite'
22
+ import type {
23
+ DevEnvironment,
24
+ Environment,
25
+ Plugin,
26
+ Rollup,
27
+ UserConfig,
28
+ ViteDevServer,
29
+ } from 'vite'
22
30
  import * as Graph from '../compiler/Graph.js'
23
31
  import * as Source from '../compiler/Source.js'
24
32
  import * as ThemeRules from '../web/internal/Themes.js'
@@ -51,6 +59,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
51
59
  const states = new WeakMap<Environment, Map<string, Entry>>()
52
60
  const removed = new WeakMap<Environment, Set<string>>()
53
61
  const reads = new WeakMap<Environment, Map<string, string>>()
62
+ const lookups = new WeakMap<Environment, Lookups>()
54
63
  const compilers = new WeakMap<Environment, Graph.create.ReturnType>()
55
64
  const packages = Contracts.create()
56
65
 
@@ -71,8 +80,11 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
71
80
  const initializers = new WeakMap<Environment, Entry>()
72
81
  const sourceEntrypoints = new Set<string>()
73
82
  const identities = new Map<string, string>()
83
+ const exclusions = new Map<string, boolean>()
74
84
  const lazyImports = new WeakMap<Parser.ParseResult, string[]>()
75
85
  let directories: readonly string[]
86
+ let filter: (id: unknown) => boolean
87
+ let optimizeRuntime = false
76
88
  let root: string
77
89
 
78
90
  function snapshot(environment: Environment) {
@@ -149,10 +161,26 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
149
161
  Path.isAbsolute(id) &&
150
162
  sourceRoot(id) !== undefined &&
151
163
  /\.[cm]?[jt]sx?$/.test(id) &&
152
- !/\.(?:d|test|test-d|bench|bench-d)\.[cm]?[jt]sx?$/.test(id)
164
+ !/\.(?:d|test|test-d|bench|bench-d)\.[cm]?[jt]sx?$/.test(id) &&
165
+ !excluded(id)
153
166
  )
154
167
  }
155
168
 
169
+ /** Matches excluded files inside source directories. Package files never match, so packed contracts stay readable. */
170
+ function excluded(id: string) {
171
+ const file = id.split(/[?#]/)[0]!
172
+ let value = exclusions.get(file)
173
+
174
+ // Vite's filter recompiles each glob per call, and every compile checks each import.
175
+ if (value === undefined) {
176
+ value =
177
+ Path.isAbsolute(file) && sourceRoot(file) !== undefined && !filter(file)
178
+ exclusions.set(file, value)
179
+ }
180
+
181
+ return value
182
+ }
183
+
156
184
  function sourceRoot(file: string) {
157
185
  return directories.find((directory) => {
158
186
  const relative = Path.relative(directory, file)
@@ -355,6 +383,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
355
383
  event: string,
356
384
  ) {
357
385
  reads.delete(environment)
386
+ lookups.delete(environment)
358
387
  if (event === 'delete') {
359
388
  if (entries(environment).has(file)) {
360
389
  let files = removed.get(environment)
@@ -392,9 +421,32 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
392
421
  allSources = false,
393
422
  everything = false,
394
423
  ) {
424
+ // Every compile visits the discovered project, so modules share lookups
425
+ // until the watcher reports a change. Full-project compiles look up afresh.
426
+ const cache = (() => {
427
+ const fresh: Lookups = { contracts: new Map(), resolutions: new Map() }
428
+ if (everything) return fresh
429
+
430
+ let value = lookups.get(entry.environment)
431
+ if (!value) {
432
+ value = fresh
433
+ lookups.set(entry.environment, value)
434
+ }
435
+ return value
436
+ })()
437
+
395
438
  async function resolve(source: string, importer: string) {
396
439
  if (isBuiltin(source)) return { id: source, external: true }
397
- const resolved = await host.resolve(source, importer)
440
+
441
+ const key = `${importer}\0${source}`
442
+ let pending = cache.resolutions.get(key)
443
+ if (!pending) {
444
+ pending = host.resolve(source, importer)
445
+ cache.resolutions.set(key, pending)
446
+ pending.catch(() => cache.resolutions.delete(key))
447
+ }
448
+
449
+ const resolved = await pending
398
450
  if (!resolved) return resolved
399
451
 
400
452
  if (entry.environment.mode === 'dev') {
@@ -416,15 +468,41 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
416
468
  const lazy: Record<string, ReadonlySet<string>> = Object.create(null)
417
469
  const modules: Record<string, string> = Object.create(null)
418
470
  const files = new Set<string>()
419
- const contractHost: Contracts.read.Options = {
420
- read: async (file) => {
421
- const source = await snapshot(entry.environment).read(file)
422
- host.watch(file)
423
- files.add(file)
424
- return source
425
- },
426
- resolve: async (specifier, importer) =>
427
- (await resolve(specifier, importer))?.id.split(/[?#]/)[0],
471
+
472
+ // A package contract derives from the files it reads and the resolutions
473
+ // above. A cached contract still makes this compile watch those files.
474
+ async function readContract(file: string, required = false) {
475
+ const key = `${required}\0${file}`
476
+ let pending = cache.contracts.get(key)
477
+ if (!pending) {
478
+ const read = new Set<string>()
479
+
480
+ pending = packages
481
+ .read(file, {
482
+ read: async (path) => {
483
+ const source = await snapshot(entry.environment).read(path)
484
+ read.add(path)
485
+ return source
486
+ },
487
+ required,
488
+ resolve: async (specifier, importer) =>
489
+ (await resolve(specifier, importer))?.id.split(/[?#]/)[0],
490
+ })
491
+ .then((contract) => ({ contract, files: read }))
492
+ cache.contracts.set(key, pending)
493
+ pending.catch(() => cache.contracts.delete(key))
494
+ }
495
+
496
+ const derived = await pending
497
+
498
+ for (const path of derived.files) {
499
+ if (files.has(path)) continue
500
+
501
+ files.add(path)
502
+ host.watch(path)
503
+ }
504
+
505
+ return derived.contract
428
506
  }
429
507
 
430
508
  async function visit(file: string, source?: string) {
@@ -522,11 +600,18 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
522
600
  await visit(resolved.id)
523
601
  continue
524
602
  }
603
+
604
+ // An excluded source stays a runtime import instead of deriving a package contract.
605
+ if (excluded(resolved.id)) {
606
+ resolutions[specifier] = null
607
+ continue
608
+ }
609
+
525
610
  if (resolved.external || !eligible(resolved.id)) {
526
611
  const physical = resolved.id.split('?')[0]!.split('#')[0]!
527
612
  const contract =
528
613
  Path.isAbsolute(physical) && /\.[cm]?[jt]sx?$/.test(physical)
529
- ? await packages.read(physical, contractHost)
614
+ ? await readContract(physical)
530
615
  : undefined
531
616
 
532
617
  if (contract !== undefined) {
@@ -648,7 +733,11 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
648
733
  continue
649
734
 
650
735
  const resolved = await resolve(specifier, file)
651
- if (!resolved || (!resolved.external && eligible(resolved.id)))
736
+ if (
737
+ !resolved ||
738
+ (!resolved.external && eligible(resolved.id)) ||
739
+ excluded(resolved.id)
740
+ )
652
741
  continue
653
742
 
654
743
  if (resource(specifier) || resource(resolved.id)) continue
@@ -657,7 +746,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
657
746
  if (!Path.isAbsolute(physical) || !/\.[cm]?[jt]sx?$/.test(physical))
658
747
  continue
659
748
 
660
- const contract = await packages.read(physical, contractHost)
749
+ const contract = await readContract(physical)
661
750
  if (contract === undefined) continue
662
751
 
663
752
  contracts[resolved.id] = contract
@@ -715,9 +804,9 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
715
804
  imports[owner] ??= Object.create(null)
716
805
  imports[owner]![specifier] = target.id
717
806
 
718
- contracts[target.id] ??= (await packages.read(
807
+ contracts[target.id] ??= (await readContract(
719
808
  target.id.split(/[?#]/)[0]!,
720
- { ...contractHost, required: true },
809
+ true,
721
810
  ))!
722
811
 
723
812
  await dependencies(target.id)
@@ -995,10 +1084,10 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
995
1084
  async function initializations(server: ViteDevServer) {
996
1085
  const environment = server.environments.client
997
1086
  reads.delete(environment)
1087
+ lookups.delete(environment)
998
1088
  const host: Host = {
999
1089
  asset: async (file) => file,
1000
- resolve: (source, importer) =>
1001
- environment.pluginContainer.resolveId(source, importer),
1090
+ resolve: resolver(environment),
1002
1091
  watch: (file) => server.watcher.add(file),
1003
1092
  }
1004
1093
  const sources = await discover(environment, host)
@@ -1189,10 +1278,17 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1189
1278
  return {
1190
1279
  buildStart() {
1191
1280
  reads.delete(this.environment)
1281
+ lookups.delete(this.environment)
1192
1282
  },
1193
1283
  config: {
1194
1284
  order: 'post',
1195
- handler(config) {
1285
+ handler(config, env) {
1286
+ // Ordered after other config hooks, so plugin-provided aliases are visible.
1287
+ optimizeRuntime =
1288
+ env.command === 'serve' &&
1289
+ options.compiler !== false &&
1290
+ installed(config)
1291
+
1196
1292
  if (native) return
1197
1293
  // Inline color-scheme changes cannot initialize Lightning's lowered helpers.
1198
1294
  return {
@@ -1221,6 +1317,32 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1221
1317
  }
1222
1318
  },
1223
1319
  },
1320
+ configEnvironment: {
1321
+ // Plugins without enforce settle their environment options first.
1322
+ order: 'post',
1323
+ handler(name, config) {
1324
+ if (!optimizeRuntime) return
1325
+
1326
+ const optimizeDeps = config.optimizeDeps ?? {}
1327
+ const consumer =
1328
+ config.consumer ?? (name === 'client' ? 'client' : 'server')
1329
+
1330
+ // Vite defaults server environments to noDiscovery. An include list there
1331
+ // would create an explicit optimizer that otherwise never exists.
1332
+ if (optimizeDeps.noDiscovery ?? consumer === 'server') return
1333
+ if (
1334
+ optimizeDeps.include?.includes(runtime) ||
1335
+ optimizeDeps.exclude?.some((id) => id === 'zyzz' || id === runtime)
1336
+ )
1337
+ return
1338
+
1339
+ // Vite scans authored source, so the runtime import that compilation
1340
+ // adds first appears while a request transforms a module. Discovering
1341
+ // it then re-runs the optimizer, and modules loaded before the re-run
1342
+ // can keep a separate copy of a shared dependency such as React.
1343
+ return { optimizeDeps: { include: [runtime] } }
1344
+ },
1345
+ },
1224
1346
  configureServer(server) {
1225
1347
  // Directory watches belong to the server, not module import dependencies.
1226
1348
  server.watcher.add(directories.slice(1))
@@ -1257,6 +1379,14 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1257
1379
  }),
1258
1380
  )),
1259
1381
  ]
1382
+
1383
+ // Portable build adapters share this module and do not require Vite.
1384
+ const { createFilter } = await import('vite')
1385
+ const patterns = [...configFiles, ...(options.exclude ?? [])]
1386
+
1387
+ filter = createFilter(null, patterns, { resolve: root })
1388
+ exclusions.clear()
1389
+
1260
1390
  if (native) return
1261
1391
  const lightning = (config.css.lightningcss ??= {})
1262
1392
  lightning.include =
@@ -1370,8 +1500,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1370
1500
  ? await compile(
1371
1501
  first,
1372
1502
  {
1373
- resolve: (source, importer) =>
1374
- this.environment.pluginContainer.resolveId(source, importer),
1503
+ resolve: resolver(this.environment),
1375
1504
  watch: (file) => {
1376
1505
  server.watcher.add(file)
1377
1506
  },
@@ -1465,7 +1594,10 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1465
1594
  const output = await compile(
1466
1595
  entry,
1467
1596
  {
1468
- resolve: (source, importer) => this.resolve(source, importer),
1597
+ resolve:
1598
+ this.environment.mode === 'dev'
1599
+ ? resolver(this.environment)
1600
+ : (source, importer) => this.resolve(source, importer),
1469
1601
  watch: (file) => this.addWatchFile(file),
1470
1602
  asset: async (file) => {
1471
1603
  const reference = this.emitFile({
@@ -1527,7 +1659,10 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1527
1659
  const output = await compile(
1528
1660
  entry,
1529
1661
  {
1530
- resolve: (source, importer) => this.resolve(source, importer),
1662
+ resolve:
1663
+ this.environment.mode === 'dev'
1664
+ ? resolver(this.environment)
1665
+ : (source, importer) => this.resolve(source, importer),
1531
1666
  watch: (file) => this.addWatchFile(file),
1532
1667
  asset: async (file) => {
1533
1668
  const reference = this.emitFile({
@@ -1629,8 +1764,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
1629
1764
  return await documentCatalogs(
1630
1765
  html,
1631
1766
  context.filename,
1632
- (source, importer) =>
1633
- environment.pluginContainer.resolveId(source, importer),
1767
+ resolver(environment),
1634
1768
  state,
1635
1769
  )
1636
1770
  })()
@@ -1666,7 +1800,81 @@ type Host = {
1666
1800
  watch: (file: string) => void
1667
1801
  }
1668
1802
 
1803
+ type Lookups = {
1804
+ contracts: Map<
1805
+ string,
1806
+ Promise<{ contract: string | undefined; files: ReadonlySet<string> }>
1807
+ >
1808
+ resolutions: Map<string, ReturnType<Host['resolve']>>
1809
+ }
1810
+
1811
+ /**
1812
+ * Whether `zyzz/runtime` resolves inside node_modules from the Vite root. Vite
1813
+ * optimizes a discovered import only there, so linked and aliased copies stay
1814
+ * source modules beside the other entrypoints.
1815
+ */
1816
+ function installed(config: UserConfig) {
1817
+ const aliases = config.resolve?.alias ?? {}
1818
+ const entries = Array.isArray(aliases)
1819
+ ? aliases
1820
+ : Object.entries(aliases).map(([find, replacement]) => ({
1821
+ find,
1822
+ replacement,
1823
+ }))
1824
+
1825
+ // Matches Vite's alias semantics: a string entry covers its subpaths.
1826
+ if (
1827
+ entries.some((entry) =>
1828
+ typeof entry.find === 'string'
1829
+ ? runtime === entry.find || runtime.startsWith(`${entry.find}/`)
1830
+ : entry.find.test(runtime),
1831
+ )
1832
+ )
1833
+ return false
1834
+
1835
+ try {
1836
+ const require = createRequire(
1837
+ Path.join(Path.resolve(config.root ?? ''), 'package.json'),
1838
+ )
1839
+
1840
+ // Vite keeps a symlinked package at its node_modules path, which Node dereferences.
1841
+ if (config.resolve?.preserveSymlinks)
1842
+ return (require.resolve.paths('zyzz') ?? []).some((directory) =>
1843
+ existsSync(Path.join(directory, 'zyzz/package.json')),
1844
+ )
1845
+
1846
+ return require.resolve(runtime).split(Path.sep).includes('node_modules')
1847
+ } catch {
1848
+ return false
1849
+ }
1850
+ }
1851
+
1852
+ // Vite forwards `scan` to `resolveId` hooks but leaves it out of its public types.
1853
+ type ScanOptions = NonNullable<
1854
+ Parameters<DevEnvironment['pluginContainer']['resolveId']>[2]
1855
+ > & { scan: boolean }
1856
+
1857
+ const scan: ScanOptions = { scan: true }
1858
+
1859
+ /**
1860
+ * Resolves imports read from source before other plugins transform their
1861
+ * importers. Marking them as scans, as Vite's dependency scanner does, keeps the
1862
+ * optimizer from discovering their dependencies and lets plugins tell them apart
1863
+ * from imports that reach the module graph.
1864
+ */
1865
+ function resolver(environment: DevEnvironment): Host['resolve'] {
1866
+ return (source, importer) =>
1867
+ environment.pluginContainer.resolveId(source, importer, scan)
1868
+ }
1869
+
1870
+ // Vite and Vitest load their own configuration modules, which application code never imports.
1871
+ const configFiles = [
1872
+ '**/vite.config.*',
1873
+ '**/vitest.config.*',
1874
+ '**/vitest.workspace.*',
1875
+ ]
1669
1876
  const prefix = '\0zyzz:'
1877
+ const runtime = 'zyzz/runtime'
1670
1878
  const sharedId = `${prefix}shared.css`
1671
1879
 
1672
1880
  function cssId(file: string) {
@@ -1679,6 +1887,13 @@ export declare namespace zyzz {
1679
1887
  type Options = {
1680
1888
  /** False retains authored calls and requires explicit identities where needed. */
1681
1889
  readonly compiler?: boolean | undefined
1890
+ /**
1891
+ * Source modules left out of discovery and transforms. Globs resolve against the
1892
+ * Vite root unless they start with `**`, and regular expressions test absolute paths.
1893
+ * Imports of excluded modules stay runtime imports. Vite and Vitest configuration
1894
+ * modules are always excluded.
1895
+ */
1896
+ readonly exclude?: readonly (RegExp | string)[] | undefined
1682
1897
  /** Additional source directories, absolute or relative to the Vite root. */
1683
1898
  readonly include?: readonly string[] | undefined
1684
1899
  /** Native context captured at creation. Disables CSS delivery and initialization scripts. */
@@ -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.')
@@ -132,6 +133,12 @@ export function create() {
132
133
  const value = data.values[path]
133
134
  // A set sharing its contract with an extension lacks the extension's added paths.
134
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({
@@ -258,9 +265,9 @@ export function create() {
258
265
  if ('default' in value) {
259
266
  // Separate fallback properties preserve extensions and resolve references within each scope.
260
267
  const base = literal(value.default, label, owner)
261
- // 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.
262
269
  const css = `:where(*){--fallback:${base};}${conditionalCss(value, '--fallback', ':where(*)', label, owner)}`
263
- 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)}`
264
271
  const emitted: Rule[] = [
265
272
  { conditions: [], property: name, selector: ':where(*)', value: base },
266
273
  ]
@@ -410,6 +417,44 @@ export function scope(value: string, identity?: string): string {
410
417
  return `z-theme-${namespace}${Identity.name(value)}`
411
418
  }
412
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
+
413
458
  function variable(identity: string | undefined, path: string): string {
414
459
  const namespace = identity?.startsWith('id-') ? `${identity.slice(3)}-` : ''
415
460
  return `--z-${namespace}${path.split('.').map(Identity.name).join('-')}`