zyzz 0.0.5 → 0.0.7

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 (91) hide show
  1. package/README.md +26 -1
  2. package/dist/Style.d.ts +3 -3
  3. package/dist/Style.d.ts.map +1 -1
  4. package/dist/Style.js +12 -1
  5. package/dist/Style.js.map +1 -1
  6. package/dist/babel/index.d.ts +2 -0
  7. package/dist/babel/index.d.ts.map +1 -1
  8. package/dist/babel/index.js +10 -1
  9. package/dist/babel/index.js.map +1 -1
  10. package/dist/compiler/Graph.d.ts +2 -0
  11. package/dist/compiler/Graph.d.ts.map +1 -1
  12. package/dist/compiler/Graph.js +19 -1
  13. package/dist/compiler/Graph.js.map +1 -1
  14. package/dist/compiler/Source.d.ts.map +1 -1
  15. package/dist/compiler/Source.js +5 -2
  16. package/dist/compiler/Source.js.map +1 -1
  17. package/dist/compiler/internal/Contributions.d.ts +1 -1
  18. package/dist/compiler/internal/Contributions.d.ts.map +1 -1
  19. package/dist/compiler/internal/Contributions.js +5 -3
  20. package/dist/compiler/internal/Contributions.js.map +1 -1
  21. package/dist/compiler/internal/Static.d.ts.map +1 -1
  22. package/dist/compiler/internal/Static.js +2 -0
  23. package/dist/compiler/internal/Static.js.map +1 -1
  24. package/dist/internal/Authoring.d.ts.map +1 -1
  25. package/dist/internal/Authoring.js +14 -0
  26. package/dist/internal/Authoring.js.map +1 -1
  27. package/dist/internal/Binding.d.ts +7 -0
  28. package/dist/internal/Binding.d.ts.map +1 -1
  29. package/dist/internal/Binding.js.map +1 -1
  30. package/dist/internal/Configuration.d.ts +1 -1
  31. package/dist/internal/Configuration.d.ts.map +1 -1
  32. package/dist/internal/Configuration.js +7 -6
  33. package/dist/internal/Configuration.js.map +1 -1
  34. package/dist/internal/Theme.d.ts +1 -1
  35. package/dist/internal/Theme.d.ts.map +1 -1
  36. package/dist/internal/Theme.js.map +1 -1
  37. package/dist/internal/Token.d.ts +3 -1
  38. package/dist/internal/Token.d.ts.map +1 -1
  39. package/dist/internal/Token.js +28 -3
  40. package/dist/internal/Token.js.map +1 -1
  41. package/dist/internal/Value.d.ts +22 -0
  42. package/dist/internal/Value.d.ts.map +1 -1
  43. package/dist/internal/Value.js +76 -9
  44. package/dist/internal/Value.js.map +1 -1
  45. package/dist/next/index.d.ts +8 -3
  46. package/dist/next/index.d.ts.map +1 -1
  47. package/dist/next/index.js +21 -10
  48. package/dist/next/index.js.map +1 -1
  49. package/dist/next/internal/Project.d.ts +28 -0
  50. package/dist/next/internal/Project.d.ts.map +1 -0
  51. package/dist/next/internal/Project.js +249 -0
  52. package/dist/next/internal/Project.js.map +1 -0
  53. package/dist/next/loader.d.ts +3 -5
  54. package/dist/next/loader.d.ts.map +1 -1
  55. package/dist/next/loader.js +14 -144
  56. package/dist/next/loader.js.map +1 -1
  57. package/dist/node/Reset.d.ts +3 -0
  58. package/dist/node/Reset.d.ts.map +1 -0
  59. package/dist/node/Reset.js +7 -0
  60. package/dist/node/Reset.js.map +1 -0
  61. package/dist/unplugin/index.d.ts +2 -0
  62. package/dist/unplugin/index.d.ts.map +1 -1
  63. package/dist/unplugin/index.js +2 -0
  64. package/dist/unplugin/index.js.map +1 -1
  65. package/dist/variants.d.ts +1 -1
  66. package/dist/variants.d.ts.map +1 -1
  67. package/dist/variants.js.map +1 -1
  68. package/dist/vite/index.d.ts +2 -0
  69. package/dist/vite/index.d.ts.map +1 -1
  70. package/dist/vite/index.js +38 -9
  71. package/dist/vite/index.js.map +1 -1
  72. package/package.json +1 -1
  73. package/src/Style.ts +42 -8
  74. package/src/babel/index.ts +23 -1
  75. package/src/compiler/Graph.ts +22 -0
  76. package/src/compiler/Source.ts +5 -1
  77. package/src/compiler/internal/Contributions.ts +5 -2
  78. package/src/compiler/internal/Static.ts +1 -0
  79. package/src/internal/Authoring.ts +21 -0
  80. package/src/internal/Binding.ts +30 -0
  81. package/src/internal/Configuration.ts +19 -13
  82. package/src/internal/Theme.ts +5 -1
  83. package/src/internal/Token.ts +34 -3
  84. package/src/internal/Value.ts +180 -10
  85. package/src/next/index.ts +34 -12
  86. package/src/next/internal/Project.ts +337 -0
  87. package/src/next/loader.ts +28 -190
  88. package/src/node/Reset.ts +7 -0
  89. package/src/unplugin/index.ts +4 -0
  90. package/src/variants.ts +1 -0
  91. package/src/vite/index.ts +43 -14
@@ -21,6 +21,60 @@ export type Atom<value> =
21
21
  * Checks authored properties and retains validated input types to avoid expanding whole-property fallback unions.
22
22
  */
23
23
  export type Accepted<style, properties> = {
24
+ [property in keyof style]: AcceptedValue<
25
+ style[property],
26
+ property,
27
+ properties
28
+ >
29
+ }
30
+
31
+ type AcceptedValue<value, property extends PropertyKey, properties> =
32
+ Extract<value, `[${string}]` | `[${string}] !important`> extends never
33
+ ? AcceptedMember<Record<property, value>, properties>[property]
34
+ : value extends unknown
35
+ ? AcceptedMember<Record<property, value>, properties>[property]
36
+ : never
37
+
38
+ type AcceptedMember<style, properties> = {
39
+ [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)
56
+ : style[property] extends readonly [unknown, ...unknown[]]
57
+ ? AcceptedArray<style[property], property, properties>
58
+ : LiteralAccepted<Pick<style, property>, properties>[property]
59
+ : never
60
+ }
61
+
62
+ type ScalarValue<value, property> =
63
+ value extends `${infer numeric extends number}`
64
+ ? property extends keyof Literal.Properties
65
+ ? number extends Literal.Properties[property]
66
+ ? numeric
67
+ : value
68
+ : value
69
+ : value
70
+
71
+ type AcceptedArray<values, property extends PropertyKey, properties> = {
72
+ [index in keyof values]: values[index] extends readonly unknown[]
73
+ ? never
74
+ : Accepted<Record<property, values[index]>, properties>[property]
75
+ }
76
+
77
+ type LiteralAccepted<style, properties> = {
24
78
  [property in keyof style]: property extends keyof properties
25
79
  ? style[property] extends { readonly [Token.scalar]: infer scalar }
26
80
  ? property extends keyof Literal.Properties
@@ -80,6 +134,22 @@ type Fold<value> = value extends string
80
134
 
81
135
  /** Refines concrete scalar spellings; already-broad property contracts need no literal refinement. */
82
136
  export type Checked<style, tokens = {}> = {
137
+ [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]
147
+ : style[property] extends readonly [unknown, ...unknown[]]
148
+ ? CheckedArray<style[property], property, tokens>
149
+ : LiteralChecked<Pick<style, property>, tokens>[property]
150
+ }
151
+
152
+ type LiteralChecked<style, tokens = {}> = {
83
153
  [property in keyof style]: style[property] extends string & Binding.Reference
84
154
  ? style[property] extends Binding.Reference<'*'>
85
155
  ? unknown
@@ -121,6 +191,40 @@ export type Checked<style, tokens = {}> = {
121
191
  : unknown)
122
192
  }
123
193
 
194
+ type CheckedArray<values, property extends PropertyKey, tokens> = {
195
+ [index in keyof values]: Checked<
196
+ Record<property, values[index]>,
197
+ tokens
198
+ >[property]
199
+ }
200
+
201
+ /** Requires tokens for properties with configured values. */
202
+ export type Tokens<
203
+ value,
204
+ tokens,
205
+ property extends keyof Literal.Properties,
206
+ > = property extends `--${string}`
207
+ ? unknown
208
+ : [Token.Names<tokens, property>] extends [never]
209
+ ? unknown
210
+ : value extends readonly unknown[]
211
+ ? { [index in keyof value]: Tokens<value[index], tokens, property> }
212
+ : value extends
213
+ | Binding.Reference
214
+ | Token.Reference
215
+ | Token.Variable
216
+ | `${Token.Variable} !important`
217
+ | `[${string}]`
218
+ | `[${string}] !important`
219
+ ? unknown
220
+ : value extends Atom<Token.Names<tokens, property>>
221
+ ? unknown
222
+ : value extends number
223
+ ? `${value}` extends Token.Names<tokens, property>
224
+ ? unknown
225
+ : never
226
+ : never
227
+
124
228
  type Importance<value> = value extends readonly unknown[]
125
229
  ? { [key in keyof value]: Importance<value[key]> }
126
230
  : value extends `${infer body}!${infer suffix}`
@@ -263,7 +367,11 @@ export function parse(
263
367
  property: keyof Literal.Properties,
264
368
  ):
265
369
  | { invalid: true }
266
- | { important: boolean; value: number | string | Token.Expression }
370
+ | {
371
+ custom?: true
372
+ important: boolean
373
+ value: number | string | Token.Expression
374
+ }
267
375
  | undefined {
268
376
  if (Token.isExpression(input)) {
269
377
  const text = input.parts
@@ -275,6 +383,16 @@ export function parse(
275
383
 
276
384
  const parts = [...input.parts]
277
385
  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
+ }
278
396
 
279
397
  for (let index = parts.length - 1; remaining > 0 && index >= 0; index--) {
280
398
  const part = parts[index]
@@ -286,7 +404,11 @@ export function parse(
286
404
  remaining -= count
287
405
  }
288
406
 
289
- return { important: parsed.important, value: Token.compose(parts) }
407
+ return {
408
+ ...(parsed.custom ? { custom: true } : {}),
409
+ important: parsed.important,
410
+ value: Token.compose(parts.filter((part) => part !== '')),
411
+ }
290
412
  }
291
413
 
292
414
  if (typeof input !== 'string') return undefined
@@ -317,7 +439,15 @@ export function parse(
317
439
  marker = marker === 0 ? -1 : input.lastIndexOf('!', marker - 1)
318
440
  }
319
441
 
320
- if (marker < 0) return undefined
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
+ }
321
451
  if (input.slice(marker - 1) !== ' !important' || escaped(marker - 1))
322
452
  return { invalid: true }
323
453
 
@@ -326,14 +456,54 @@ export function parse(
326
456
  while (end > 0 && /[ \t\n\r\f]/.test(input[end - 1]!) && !escaped(end - 1))
327
457
  end--
328
458
 
329
- const text = input.slice(0, end)
330
- const numeric =
331
- Literal.rule(property)?.kind === 'number' ||
332
- Literal.rule(property)?.kind === 'grid-line'
333
- const value =
334
- numeric && /^[+-]?(?:\d*\.\d+|\d+)(?:[eE][+-]?\d+)?$/.test(text)
459
+ 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)
463
+
464
+ return {
465
+ ...(custom ? { custom: true } : {}),
466
+ important: true as const,
467
+ value,
468
+ }
469
+
470
+ function scalar(text: string): number | string {
471
+ const numeric =
472
+ Literal.rule(property)?.kind === 'number' ||
473
+ Literal.rule(property)?.kind === 'grid-line'
474
+ return numeric && /^[+-]?(?:\d*\.\d+|\d+)(?:[eE][+-]?\d+)?$/.test(text)
335
475
  ? Number(text)
336
476
  : text
477
+ }
478
+ }
337
479
 
338
- return { important: true as const, value }
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
339
509
  }
package/src/next/index.ts CHANGED
@@ -9,21 +9,26 @@ import * as Path from 'node:path'
9
9
  * Preserves existing configuration hooks and creates `.zyzz/next` beneath the working directory.
10
10
  * Throws file-system errors during setup; compilation errors are reported by the bundler.
11
11
  */
12
- export function zyzz(config: NextConfig): NextConfig
13
- export function zyzz(config: zyzz.Factory): zyzz.Factory
14
- export function zyzz(config: Promise<NextConfig>): Promise<NextConfig>
12
+ export function zyzz(config: NextConfig, options?: zyzz.Options): NextConfig
13
+ export function zyzz(config: zyzz.Factory, options?: zyzz.Options): zyzz.Factory
14
+ export function zyzz(
15
+ config: Promise<NextConfig>,
16
+ options?: zyzz.Options,
17
+ ): Promise<NextConfig>
15
18
  export function zyzz(
16
19
  config: NextConfig | zyzz.Factory | Promise<NextConfig>,
20
+ options: zyzz.Options = {},
17
21
  ): NextConfig | zyzz.Factory | Promise<NextConfig> {
18
22
  if (typeof config === 'function')
19
- return async (phase, context) => zyzz(await config(phase, context))
20
- if (config instanceof Promise) return config.then(zyzz)
23
+ return async (phase, context) => zyzz(await config(phase, context), options)
24
+ if (config instanceof Promise)
25
+ return config.then((value) => zyzz(value, options))
21
26
 
22
27
  const loader = Module.createRequire(import.meta.url).resolve(
23
28
  'zyzz/next/loader',
24
29
  )
25
- const options = { root: process.cwd() }
26
- const shared = Path.join(options.root, '.zyzz', 'next', 'shared.css')
30
+ const loaderOptions = { reset: options.reset ?? false, root: process.cwd() }
31
+ const shared = Path.join(loaderOptions.root, '.zyzz', 'next', 'shared.css')
27
32
  Fs.mkdirSync(Path.dirname(shared), { recursive: true })
28
33
  try {
29
34
  Fs.writeFileSync(shared, '', { flag: 'wx' })
@@ -37,7 +42,10 @@ export function zyzz(
37
42
  all: [{ not: 'foreign' as const }, { not: { query: /zyzz-style/ } }],
38
43
  },
39
44
  loaders: [
40
- { loader, options: { ...options, bundler: 'turbopack', mode: 'source' } },
45
+ {
46
+ loader,
47
+ options: { ...loaderOptions, bundler: 'turbopack', mode: 'source' },
48
+ },
41
49
  ],
42
50
  }
43
51
 
@@ -67,7 +75,11 @@ export function zyzz(
67
75
  loaders: [
68
76
  {
69
77
  loader,
70
- options: { ...options, bundler: 'turbopack', mode: 'style' },
78
+ options: {
79
+ ...loaderOptions,
80
+ bundler: 'turbopack',
81
+ mode: 'style',
82
+ },
71
83
  },
72
84
  ],
73
85
  as: '*.css',
@@ -77,7 +89,11 @@ export function zyzz(
77
89
  loaders: [
78
90
  {
79
91
  loader,
80
- options: { ...options, bundler: 'turbopack', mode: 'shared' },
92
+ options: {
93
+ ...loaderOptions,
94
+ bundler: 'turbopack',
95
+ mode: 'shared',
96
+ },
81
97
  },
82
98
  ],
83
99
  },
@@ -90,12 +106,12 @@ export function zyzz(
90
106
  result.module.rules.push({
91
107
  enforce: 'pre',
92
108
  exclude: /node_modules/,
93
- include: options.root,
109
+ include: loaderOptions.root,
94
110
  test: /\.[cm]?[jt]sx?$/,
95
111
  use: [
96
112
  {
97
113
  loader,
98
- options: { ...options, bundler: 'webpack', mode: 'source' },
114
+ options: { ...loaderOptions, bundler: 'webpack', mode: 'source' },
99
115
  },
100
116
  ],
101
117
  })
@@ -111,4 +127,10 @@ export declare namespace zyzz {
111
127
  phase: string,
112
128
  context: { defaultConfig: NextConfig },
113
129
  ) => NextConfig | Promise<NextConfig>
130
+
131
+ /** Web stylesheet delivery settings. */
132
+ type Options = {
133
+ /** Include the CSS reset. Defaults to false. */
134
+ readonly reset?: boolean | undefined
135
+ }
114
136
  }
@@ -0,0 +1,337 @@
1
+ /** Retains filesystem snapshots and an incremental graph within a Next.js loader worker. @module */
2
+ import * as Contract from '../../compiler/internal/Contract.js'
3
+ import * as Fs from 'node:fs/promises'
4
+ import * as Graph from '../../compiler/Graph.js'
5
+ import type * as NativeFs from 'node:fs'
6
+ import * as Path from 'node:path'
7
+ import * as Reset from '../../node/Reset.js'
8
+ import * as Syntax from '../../compiler/internal/Syntax.js'
9
+
10
+ /** Resolution and dependency notifications supplied by the active loader invocation. */
11
+ export type Context = {
12
+ /** Track graph membership changes. */
13
+ addContextDependency(directory: string): void
14
+ /** Track source and contract edits. */
15
+ addDependency(file: string): void
16
+ /** Resolve with the active bundler's aliases and conditions. */
17
+ getResolve(
18
+ options: object,
19
+ ): (
20
+ directory: string,
21
+ specifier: string,
22
+ callback: (error: Error | null, file?: string | false) => void,
23
+ ) => void
24
+ /** Physical loader resource. */
25
+ resourcePath: string
26
+ }
27
+
28
+ /** Creates project state owned by a loader configuration in one worker process. */
29
+ export function create(options: create.Options) {
30
+ let compiler = Graph.create()
31
+ const directories = new Map<
32
+ string,
33
+ { items: NativeFs.Dirent[]; track: boolean; version: string }
34
+ >()
35
+ const files = new Map<string, { source: string; version: string }>()
36
+ const libraries = new Map<
37
+ string,
38
+ { dependencies: readonly (readonly string[])[]; source: string }
39
+ >()
40
+ const reset = options.reset ? Reset.read() : undefined
41
+ const syntax = new Map<
42
+ string,
43
+ { parsed: ReturnType<typeof Syntax.parse>; source: string }
44
+ >()
45
+ let pending = Promise.resolve()
46
+
47
+ return {
48
+ /** Compiles a fresh snapshot while retaining unchanged syntax and graph results. */
49
+ compile(context: Context, source?: string) {
50
+ // Loader calls may overlap, but only one immutable snapshot updates the compiler at a time.
51
+ const result = pending.then(async () => {
52
+ try {
53
+ return await compile(context, source)
54
+ } catch (error) {
55
+ // Failed discovery or extraction may leave a partial filesystem snapshot.
56
+ compiler = Graph.create()
57
+ directories.clear()
58
+ files.clear()
59
+ libraries.clear()
60
+ syntax.clear()
61
+ throw error
62
+ }
63
+ })
64
+
65
+ // A failed generation must not prevent recovery after the source is corrected.
66
+ pending = result.then(
67
+ () => {},
68
+ () => {},
69
+ )
70
+
71
+ return result
72
+ },
73
+ }
74
+
75
+ async function compile(context: Context, source?: string) {
76
+ const root = options.root
77
+ const programs = new Map<string, ReturnType<typeof Syntax.parse>>()
78
+ const seenDirectories = new Set<string>()
79
+ const seenFiles = new Set<string>()
80
+ async function read(file: string) {
81
+ seenFiles.add(file)
82
+ const version = await stamp(file)
83
+ let entry = files.get(file)
84
+ if (entry?.version !== version) {
85
+ entry = { source: await Fs.readFile(file, 'utf8'), version }
86
+ files.set(file, entry)
87
+ }
88
+ return entry.source
89
+ }
90
+
91
+ const modules: Record<string, string> = Object.create(null)
92
+ const imports: Record<
93
+ string,
94
+ Record<string, string | null>
95
+ > = Object.create(null)
96
+ const contracts: Record<string, string> = Object.create(null)
97
+ const resolve = context.getResolve({})
98
+ const id = (file: string) =>
99
+ `app/${Path.relative(root, file).split(Path.sep).join('/')}`
100
+ const eligible = (file: string) =>
101
+ /\.[cm]?[jt]sx?$/.test(file) &&
102
+ !/\.(?:d|test|test-d|bench|bench-d)\.[cm]?[jt]sx?$/.test(file) &&
103
+ !Path.relative(root, file)
104
+ .split(Path.sep)
105
+ .some(
106
+ (part) =>
107
+ part === '..' || part === 'node_modules' || part.startsWith('.'),
108
+ )
109
+
110
+ async function visit(file: string, text?: string): Promise<void> {
111
+ const name = id(file)
112
+ if (Object.hasOwn(modules, name)) return
113
+
114
+ context.addDependency(file)
115
+ seenFiles.add(file)
116
+ modules[name] = text ?? (await read(file))
117
+ const links: Record<string, string | null> = Object.create(null)
118
+ imports[name] = links
119
+ let entry = syntax.get(name)
120
+ if (entry?.source !== modules[name]) {
121
+ entry = {
122
+ source: modules[name]!,
123
+ parsed: Syntax.parse({ moduleId: name, source: modules[name]! }),
124
+ }
125
+ syntax.set(name, entry)
126
+ }
127
+ programs.set(name, entry.parsed)
128
+ const parsed = entry.parsed
129
+
130
+ for (const node of parsed.program.body) {
131
+ if (
132
+ (node.type !== 'ImportDeclaration' &&
133
+ node.type !== 'ExportNamedDeclaration' &&
134
+ node.type !== 'ExportAllDeclaration') ||
135
+ !node.source
136
+ )
137
+ continue
138
+ if (
139
+ node.type === 'ImportDeclaration'
140
+ ? node.importKind === 'type'
141
+ : node.exportKind === 'type'
142
+ )
143
+ continue
144
+
145
+ const specifier = node.source.value
146
+ links[specifier] = null
147
+ // Next owns this virtual module, which has no physical stylesheet contract.
148
+ if (specifier === 'next/root-params') continue
149
+ if (
150
+ specifier === 'zyzz' ||
151
+ (specifier.startsWith('zyzz/') && specifier !== 'zyzz/default') ||
152
+ specifier.startsWith('node:')
153
+ )
154
+ continue
155
+
156
+ const resolved = await new Promise<string | false | undefined>(
157
+ (accept, reject) => {
158
+ resolve(Path.dirname(file), specifier, (error, value) =>
159
+ error ? reject(error) : accept(value),
160
+ )
161
+ },
162
+ )
163
+ if (!resolved || !/\.[cm]?[jt]sx?$/.test(resolved)) continue
164
+ if (eligible(resolved)) {
165
+ links[specifier] = id(resolved)
166
+ await visit(resolved)
167
+ continue
168
+ }
169
+
170
+ const sidecar = `${resolved}.zyzz.json`
171
+ try {
172
+ contracts[resolved] = await read(sidecar)
173
+ context.addDependency(sidecar)
174
+ links[specifier] = resolved
175
+ } catch (error) {
176
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
177
+ }
178
+ }
179
+ }
180
+
181
+ async function discover(directory: string): Promise<void> {
182
+ const version = await stamp(directory)
183
+ let cached = directories.get(directory)
184
+ if (cached?.version !== version) {
185
+ const items = await Fs.readdir(directory, { withFileTypes: true })
186
+ cached = {
187
+ items,
188
+ track:
189
+ !items.some((item) => item.name === 'node_modules') ||
190
+ !(await loops(directory)),
191
+ version,
192
+ }
193
+ directories.set(directory, cached)
194
+ }
195
+ seenDirectories.add(directory)
196
+ const items = cached.items
197
+
198
+ // Linked package roots are tracked through children to avoid recursive dependency loops.
199
+ if (cached.track) context.addContextDependency(directory)
200
+
201
+ for (const item of items) {
202
+ if (
203
+ item.name.startsWith('.') ||
204
+ [
205
+ 'node_modules',
206
+ 'dist',
207
+ 'build',
208
+ 'coverage',
209
+ 'test',
210
+ 'tests',
211
+ '__tests__',
212
+ ].includes(item.name)
213
+ )
214
+ continue
215
+ const file = Path.join(directory, item.name)
216
+ if (item.isDirectory()) await discover(file)
217
+ else if (item.isFile() && eligible(file)) await visit(file)
218
+ }
219
+ }
220
+
221
+ if (source !== undefined) await visit(context.resourcePath, source)
222
+ await discover(root)
223
+ const loaded = new Set<string>()
224
+ async function dependencies(file: string): Promise<void> {
225
+ if (loaded.has(file)) return
226
+ loaded.add(file)
227
+
228
+ let library = libraries.get(file)
229
+ if (!library || library.source !== contracts[file]) {
230
+ library = {
231
+ source: contracts[file]!,
232
+ dependencies: Contract.read(
233
+ contracts[file]!,
234
+ new Map(),
235
+ file,
236
+ ).stylesheets.map((section) => section.dependency ?? []),
237
+ }
238
+ libraries.set(file, library)
239
+ }
240
+ for (const dependency of library.dependencies) {
241
+ let owner = file
242
+ for (const specifier of dependency) {
243
+ const target = await new Promise<string | false | undefined>(
244
+ (accept, reject) => {
245
+ resolve(Path.dirname(owner), specifier, (error, value) =>
246
+ error ? reject(error) : accept(value),
247
+ )
248
+ },
249
+ )
250
+ if (!target || !Path.isAbsolute(target))
251
+ throw new Error('Unable to resolve packed stylesheet dependency.')
252
+
253
+ const links = (imports[owner] ??= Object.create(null))
254
+ links[specifier] = target
255
+ if (!Object.hasOwn(contracts, target)) {
256
+ const sidecar = `${target}.zyzz.json`
257
+ contracts[target] = await read(sidecar)
258
+ context.addDependency(sidecar)
259
+ }
260
+ await dependencies(target)
261
+ owner = target
262
+ }
263
+ }
264
+ }
265
+ for (const file of Object.keys(contracts)) await dependencies(file)
266
+
267
+ const graph = compiler.compile({
268
+ [Syntax.cache]: programs,
269
+ contracts,
270
+ imports,
271
+ modules,
272
+ reset,
273
+ })
274
+
275
+ for (const file of files.keys())
276
+ if (!seenFiles.has(file)) files.delete(file)
277
+ for (const file of directories.keys())
278
+ if (!seenDirectories.has(file)) directories.delete(file)
279
+ for (const file of libraries.keys())
280
+ if (!Object.hasOwn(contracts, file)) libraries.delete(file)
281
+ for (const name of syntax.keys())
282
+ if (!programs.has(name)) syntax.delete(name)
283
+
284
+ return graph
285
+ }
286
+ }
287
+
288
+ /** Project cache settings. */
289
+ export declare namespace create {
290
+ /** Isolates source discovery and shared stylesheet options. */
291
+ type Options = {
292
+ /** Include the packaged reset in shared CSS. */
293
+ readonly reset?: boolean | undefined
294
+ /** Absolute application directory. */
295
+ readonly root: string
296
+ }
297
+ }
298
+
299
+ async function stamp(file: string) {
300
+ const stat = await Fs.stat(file, { bigint: true })
301
+ return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`
302
+ }
303
+
304
+ /** Whether an installed dependency links back to the directory or one of its ancestors. */
305
+ async function loops(directory: string): Promise<boolean> {
306
+ const installed = Path.join(directory, 'node_modules')
307
+ const entries = await Fs.readdir(installed, { withFileTypes: true }).catch(
308
+ () => [],
309
+ )
310
+ const candidates = (
311
+ await Promise.all(
312
+ entries.map(async (entry) => {
313
+ if (!entry.name.startsWith('@'))
314
+ return [Path.join(installed, entry.name)]
315
+
316
+ const scoped = Path.join(installed, entry.name)
317
+
318
+ return (await Fs.readdir(scoped).catch(() => [])).map((name) =>
319
+ Path.join(scoped, name),
320
+ )
321
+ }),
322
+ )
323
+ ).flat()
324
+
325
+ for (const candidate of candidates) {
326
+ const stat = await Fs.lstat(candidate).catch(() => undefined)
327
+ if (!stat?.isSymbolicLink()) continue
328
+
329
+ const target = await Fs.realpath(candidate).catch(() => undefined)
330
+ if (target === undefined) continue
331
+
332
+ const relative = Path.relative(target, directory)
333
+ if (!relative.startsWith('..') && !Path.isAbsolute(relative)) return true
334
+ }
335
+
336
+ return false
337
+ }