zyzz 0.0.27 → 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 (45) 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/package.json +1 -1
  36. package/src/compiler/Source.ts +237 -13
  37. package/src/compiler/Transform.ts +4 -1
  38. package/src/compiler/internal/Applications.ts +57 -1
  39. package/src/compiler/internal/Selectors.ts +25 -11
  40. package/src/internal/Authoring.ts +53 -18
  41. package/src/internal/Configuration.ts +38 -1
  42. package/src/internal/Literal.ts +3 -0
  43. package/src/internal/Theme.ts +15 -0
  44. package/src/styleFunction.ts +31 -2
  45. package/src/vite/index.ts +242 -27
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. */