zyzz 0.0.24 → 0.0.25

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 (87) hide show
  1. package/README.md +23 -365
  2. package/dist/Style.d.ts +2 -0
  3. package/dist/Style.d.ts.map +1 -1
  4. package/dist/compiler/Graph.d.ts +3 -0
  5. package/dist/compiler/Graph.d.ts.map +1 -1
  6. package/dist/compiler/Graph.js +38 -2
  7. package/dist/compiler/Graph.js.map +1 -1
  8. package/dist/compiler/Source.d.ts.map +1 -1
  9. package/dist/compiler/Source.js +11 -3
  10. package/dist/compiler/Source.js.map +1 -1
  11. package/dist/compiler/Transform.d.ts +4 -0
  12. package/dist/compiler/Transform.d.ts.map +1 -1
  13. package/dist/compiler/Transform.js +79 -15
  14. package/dist/compiler/Transform.js.map +1 -1
  15. package/dist/compiler/internal/Applications.d.ts +20 -0
  16. package/dist/compiler/internal/Applications.d.ts.map +1 -1
  17. package/dist/compiler/internal/Applications.js +151 -47
  18. package/dist/compiler/internal/Applications.js.map +1 -1
  19. package/dist/compiler/internal/Compositions.d.ts.map +1 -1
  20. package/dist/compiler/internal/Compositions.js +6 -0
  21. package/dist/compiler/internal/Compositions.js.map +1 -1
  22. package/dist/compiler/internal/Configurations.d.ts.map +1 -1
  23. package/dist/compiler/internal/Configurations.js +4 -0
  24. package/dist/compiler/internal/Configurations.js.map +1 -1
  25. package/dist/compiler/internal/Contract.d.ts.map +1 -1
  26. package/dist/compiler/internal/Contract.js +34 -7
  27. package/dist/compiler/internal/Contract.js.map +1 -1
  28. package/dist/compiler/internal/PackedStyles.d.ts.map +1 -1
  29. package/dist/compiler/internal/PackedStyles.js +7 -0
  30. package/dist/compiler/internal/PackedStyles.js.map +1 -1
  31. package/dist/compiler/internal/Themes.d.ts.map +1 -1
  32. package/dist/compiler/internal/Themes.js +20 -8
  33. package/dist/compiler/internal/Themes.js.map +1 -1
  34. package/dist/internal/Configuration.d.ts +10 -0
  35. package/dist/internal/Configuration.d.ts.map +1 -1
  36. package/dist/internal/Configuration.js +8 -0
  37. package/dist/internal/Configuration.js.map +1 -1
  38. package/dist/internal/Token.d.ts +2 -0
  39. package/dist/internal/Token.d.ts.map +1 -1
  40. package/dist/internal/Token.js.map +1 -1
  41. package/dist/next/internal/Project.d.ts.map +1 -1
  42. package/dist/next/internal/Project.js +30 -25
  43. package/dist/next/internal/Project.js.map +1 -1
  44. package/dist/next/loader.js +2 -1
  45. package/dist/next/loader.js.map +1 -1
  46. package/dist/node/Host.d.ts.map +1 -1
  47. package/dist/node/Host.js +32 -23
  48. package/dist/node/Host.js.map +1 -1
  49. package/dist/node/internal/Contracts.d.ts +22 -0
  50. package/dist/node/internal/Contracts.d.ts.map +1 -0
  51. package/dist/node/internal/Contracts.js +287 -0
  52. package/dist/node/internal/Contracts.js.map +1 -0
  53. package/dist/unplugin/index.d.ts.map +1 -1
  54. package/dist/unplugin/index.js +31 -13
  55. package/dist/unplugin/index.js.map +1 -1
  56. package/dist/vite/index.d.ts.map +1 -1
  57. package/dist/vite/index.js +30 -37
  58. package/dist/vite/index.js.map +1 -1
  59. package/dist/web/Css.d.ts +10 -5
  60. package/dist/web/Css.d.ts.map +1 -1
  61. package/dist/web/Css.js +170 -31
  62. package/dist/web/Css.js.map +1 -1
  63. package/dist/web/internal/ClassName.d.ts +49 -0
  64. package/dist/web/internal/ClassName.d.ts.map +1 -1
  65. package/dist/web/internal/ClassName.js +57 -0
  66. package/dist/web/internal/ClassName.js.map +1 -1
  67. package/package.json +1 -1
  68. package/src/Style.ts +2 -0
  69. package/src/compiler/Graph.ts +42 -2
  70. package/src/compiler/Source.ts +11 -3
  71. package/src/compiler/Transform.ts +130 -29
  72. package/src/compiler/internal/Applications.ts +239 -63
  73. package/src/compiler/internal/Compositions.ts +9 -0
  74. package/src/compiler/internal/Configurations.ts +4 -0
  75. package/src/compiler/internal/Contract.ts +59 -7
  76. package/src/compiler/internal/PackedStyles.ts +9 -0
  77. package/src/compiler/internal/Themes.ts +25 -7
  78. package/src/internal/Configuration.ts +21 -0
  79. package/src/internal/Token.ts +2 -0
  80. package/src/next/internal/Project.ts +28 -23
  81. package/src/next/loader.ts +2 -1
  82. package/src/node/Host.ts +31 -24
  83. package/src/node/internal/Contracts.ts +384 -0
  84. package/src/unplugin/index.ts +35 -13
  85. package/src/vite/index.ts +35 -36
  86. package/src/web/Css.ts +228 -42
  87. package/src/web/internal/ClassName.ts +114 -0
@@ -1,5 +1,6 @@
1
1
  /** Retains filesystem snapshots and an incremental graph within a Next.js loader worker. @module */
2
2
  import * as Contract from '../../compiler/internal/Contract.js'
3
+ import * as Contracts from '../../node/internal/Contracts.js'
3
4
  import * as Fs from 'node:fs/promises'
4
5
  import * as Graph from '../../compiler/Graph.js'
5
6
  import * as Path from 'node:path'
@@ -26,6 +27,7 @@ export type Context = {
26
27
  /** Creates project state owned by a loader configuration in one worker process. */
27
28
  export function create(options: create.Options) {
28
29
  let compiler = Graph.create()
30
+ const packages = Contracts.create()
29
31
  const files = new Map<string, { source: string; version: string }>()
30
32
  const libraries = new Map<
31
33
  string,
@@ -122,6 +124,20 @@ export function create(options: create.Options) {
122
124
  }
123
125
  return pending
124
126
  }
127
+ // Files behind package contracts stay dependencies even when the module is unchanged.
128
+ const inputs = new Set<string>()
129
+ const host: Contracts.read.Options = {
130
+ read: async (file) => {
131
+ const source = await read(file)
132
+ // Next ignores node_modules edits, and tracked manifests stalled webpack rebuilds after a failed compile.
133
+ if (Path.basename(file) === 'package.json') return source
134
+ track(file)
135
+ inputs.add(file)
136
+ return source
137
+ },
138
+ resolve: async (specifier, importer) =>
139
+ (await resolve(Path.dirname(importer), specifier)) || undefined,
140
+ }
125
141
  const id = (file: string) =>
126
142
  `app/${Path.relative(root, file).split(Path.sep).join('/')}`
127
143
  const eligible = (file: string) =>
@@ -322,14 +338,10 @@ export function create(options: create.Options) {
322
338
  await visit(resolved)
323
339
  continue
324
340
  }
325
- const sidecar = `${resolved}.zyzz.json`
326
- try {
327
- contracts[resolved] = await read(sidecar)
328
- track(sidecar)
329
- links[specifier] = resolved
330
- } catch (error) {
331
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
332
- }
341
+ const contract = await packages.read(resolved, host)
342
+ if (contract === undefined) continue
343
+ contracts[resolved] = contract
344
+ links[specifier] = resolved
333
345
  }
334
346
  }
335
347
 
@@ -360,11 +372,10 @@ export function create(options: create.Options) {
360
372
 
361
373
  const links = (imports[owner] ??= Object.create(null))
362
374
  links[specifier] = target
363
- if (!Object.hasOwn(contracts, target)) {
364
- const sidecar = `${target}.zyzz.json`
365
- contracts[target] = await read(sidecar)
366
- track(sidecar)
367
- }
375
+ contracts[target] ??= (await packages.read(target, {
376
+ ...host,
377
+ required: true,
378
+ }))!
368
379
  await dependencies(target)
369
380
  owner = target
370
381
  }
@@ -388,16 +399,10 @@ export function create(options: create.Options) {
388
399
  if (output.code === source && !output.css) {
389
400
  context.addDependency(context.resourcePath)
390
401
  for (const file of runtime) context.addDependency(file)
391
- for (const file of tracked)
392
- if (file.endsWith('.zyzz.json')) context.addDependency(file)
393
- for (const target of Object.values(imports[entry] ?? {})) {
394
- if (!target) continue
395
- context.addDependency(
396
- target.startsWith('app/')
397
- ? Path.join(root, target.slice(4))
398
- : `${target}.zyzz.json`,
399
- )
400
- }
402
+ for (const file of inputs) context.addDependency(file)
403
+ for (const target of Object.values(imports[entry] ?? {}))
404
+ if (target?.startsWith('app/'))
405
+ context.addDependency(Path.join(root, target.slice(4)))
401
406
  } else {
402
407
  for (const file of tracked) context.addDependency(file)
403
408
  }
@@ -161,7 +161,8 @@ async function compile(context: Context, source: string) {
161
161
  placeholder,
162
162
  Path.relative(directory, file)
163
163
  .split(Path.sep)
164
- .map(encodeURIComponent)
164
+ // Bundlers resolve url() paths without decoding, so escape only characters that change URL parsing.
165
+ .map((part) => part.replace(/[%#?]/g, encodeURIComponent))
165
166
  .join('/') + target.slice(raw.length),
166
167
  )
167
168
  }
package/src/node/Host.ts CHANGED
@@ -12,6 +12,8 @@ import { ResolverFactory } from 'oxc-resolver'
12
12
  import * as Graph from '../compiler/Graph.js'
13
13
  import * as AtRules from '../compiler/internal/AtRules.js'
14
14
  import * as Catalogs from '../compiler/internal/Catalogs.js'
15
+ import * as ClassName from '../web/internal/ClassName.js'
16
+ import * as Contracts from './internal/Contracts.js'
15
17
  import * as Relative from '../compiler/internal/Relative.js'
16
18
  import * as Scheme from '../internal/Scheme.js'
17
19
  import * as Snapshot from './internal/Snapshot.js'
@@ -135,6 +137,7 @@ export async function create(options: create.Options): Promise<Runtime> {
135
137
  const manifestPath = Path.join(outDir, '.zyzz.json')
136
138
 
137
139
  const compiler = Graph.create()
140
+ const packages = Contracts.create()
138
141
  const snapshot = Snapshot.create()
139
142
  const stylesheets = new WeakMap<Transform.compile.ReturnType, Stylesheet>()
140
143
  let closed = false
@@ -316,18 +319,16 @@ export async function create(options: create.Options): Promise<Runtime> {
316
319
  async function contract(file: string, required = false): Promise<boolean> {
317
320
  if (Object.hasOwn(contracts, file)) return true
318
321
 
319
- observe(`${file}.zyzz.json`)
320
- try {
321
- contracts[file] = await snapshot.read(`${file}.zyzz.json`)
322
- } catch (error) {
323
- if (
324
- !required &&
325
- !previousContracts.has(file) &&
326
- (error as NodeJS.ErrnoException).code === 'ENOENT'
327
- )
328
- return false
329
- throw error
330
- }
322
+ const source = await packages.read(file, {
323
+ read(path) {
324
+ observe(path)
325
+ return snapshot.read(path)
326
+ },
327
+ required: required || previousContracts.has(file),
328
+ resolve: async (specifier, importer) => resolve(specifier, importer),
329
+ })
330
+ if (source === undefined) return false
331
+ contracts[file] = source
331
332
 
332
333
  const metadata = (() => {
333
334
  try {
@@ -460,6 +461,7 @@ export async function create(options: create.Options): Promise<Runtime> {
460
461
  }
461
462
 
462
463
  const graph = compiler.compile({
464
+ [ClassName.complete]: true,
463
465
  [ThemeRules.shared]:
464
466
  options.modules === false && !native ? 'all' : undefined,
465
467
  [Syntax.cache]: snapshot.programs(modules),
@@ -470,13 +472,7 @@ export async function create(options: create.Options): Promise<Runtime> {
470
472
  imports,
471
473
  })
472
474
 
473
- snapshot.retain(
474
- new Set([
475
- ...inputs,
476
- ...Object.keys(contracts).map((file) => `${file}.zyzz.json`),
477
- ]),
478
- modules,
479
- )
475
+ snapshot.retain(new Set([...inputs, ...observed]), modules)
480
476
 
481
477
  const artifactNames = [
482
478
  'zyzz.css',
@@ -675,10 +671,11 @@ export async function create(options: create.Options): Promise<Runtime> {
675
671
  // cascade over the rules of the modules it imports.
676
672
  if (shared || moduleStylesheets.size) {
677
673
  const parts = shared ? [shared] : []
674
+ const units = new Set<string>()
678
675
  let schemes = false
679
676
 
680
- // Each module stylesheet declares the scheme classes to load alone. The
681
- // complete stylesheet keeps the first declaration.
677
+ // Each module stylesheet declares the scheme classes and shared rules it
678
+ // uses to load alone. The complete stylesheet keeps the first declaration.
682
679
  for (const id of order(graph.dependencies, [
683
680
  ...moduleStylesheets.keys(),
684
681
  ])) {
@@ -686,6 +683,9 @@ export async function create(options: create.Options): Promise<Runtime> {
686
683
  const declares = Object.values(Scheme.classes).some((className) =>
687
684
  stylesheet.code.includes(`.${className}`),
688
685
  )
686
+ const names = [
687
+ ...(graph.modules[id]![ClassName.units]?.values() ?? []),
688
+ ].map((decision) => decision.name)
689
689
 
690
690
  parts.push(
691
691
  rebase(
@@ -693,9 +693,11 @@ export async function create(options: create.Options): Promise<Runtime> {
693
693
  name,
694
694
  css === false ? false : css.minify,
695
695
  schemes && declares,
696
+ new Set(names.filter((name) => units.has(name))),
696
697
  ),
697
698
  )
698
699
  schemes ||= declares
700
+ for (const name of names) units.add(name)
699
701
  }
700
702
 
701
703
  const complete = concatenate(parts)
@@ -1199,10 +1201,15 @@ function rebase(
1199
1201
  name: string,
1200
1202
  minify: boolean,
1201
1203
  schemes: boolean,
1204
+ emitted: ReadonlySet<string>,
1202
1205
  ) {
1203
1206
  const directory = Path.posix.dirname(name)
1204
1207
 
1205
- if (!schemes && (directory === '.' || !stylesheet.code.includes('url(')))
1208
+ if (
1209
+ !schemes &&
1210
+ !emitted.size &&
1211
+ (directory === '.' || !stylesheet.code.includes('url('))
1212
+ )
1206
1213
  return stylesheet
1207
1214
 
1208
1215
  const classes = Object.values(Scheme.classes)
@@ -1220,11 +1227,11 @@ function rebase(
1220
1227
  const [component, ...rest] = selector ?? []
1221
1228
 
1222
1229
  if (
1223
- schemes &&
1224
1230
  !others.length &&
1225
1231
  !rest.length &&
1226
1232
  component?.type === 'class' &&
1227
- classes.includes(component.name)
1233
+ ((schemes && classes.includes(component.name)) ||
1234
+ emitted.has(component.name))
1228
1235
  )
1229
1236
  return []
1230
1237
  },
@@ -0,0 +1,384 @@
1
+ /** Reads package contracts, deriving a missing sidecar from the package's JavaScript without executing it. @module */
2
+ import * as Crypto from 'node:crypto'
3
+ import * as Path from 'node:path'
4
+ import * as Graph from '../../compiler/Graph.js'
5
+ import * as Contract from '../../compiler/internal/Contract.js'
6
+ import * as Syntax from '../../compiler/internal/Syntax.js'
7
+
8
+ /** Creates a reader whose derived contracts persist across the builds of one adapter instance. */
9
+ export function create() {
10
+ const derived = new Map<
11
+ string,
12
+ { contract: string | undefined; inputs: string }
13
+ >()
14
+ const syntax = new Map<
15
+ string,
16
+ { parsed: ReturnType<typeof Syntax.parse>; source: string }
17
+ >()
18
+
19
+ async function lookup(
20
+ file: string,
21
+ options: read.Options,
22
+ active: ReadonlySet<string>,
23
+ ): Promise<string | undefined> {
24
+ try {
25
+ return await options.read(`${file}.zyzz.json`)
26
+ } catch (error) {
27
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
28
+
29
+ const contract = await derive(file, options, active)
30
+ if (contract === undefined && options.required) throw error
31
+ return contract
32
+ }
33
+ }
34
+
35
+ async function derive(
36
+ file: string,
37
+ options: read.Options,
38
+ active: ReadonlySet<string>,
39
+ ): Promise<string | undefined> {
40
+ const root = await manifest(file, options)
41
+ if (!root) return undefined
42
+
43
+ const { directory, name } = root
44
+ const id = (path: string) =>
45
+ `${name}/${Path.relative(directory, path).split(Path.sep).join('/')}`
46
+ const contracts: Record<string, string> = Object.create(null)
47
+ const external = new Map<string, string | undefined>()
48
+ const imports: Record<
49
+ string,
50
+ Record<string, string | null>
51
+ > = Object.create(null)
52
+ const modules: Record<string, string> = Object.create(null)
53
+ const programs = new Map<string, ReturnType<typeof Syntax.parse>>()
54
+ const next = new Set(active).add(file)
55
+ let authored = false
56
+
57
+ async function link(target: string, required = false) {
58
+ const known = external.get(target)
59
+ if (known !== undefined || (external.has(target) && !required))
60
+ return known
61
+
62
+ // A package cycle stays a runtime import.
63
+ const contract = next.has(target)
64
+ ? undefined
65
+ : await lookup(target, { ...options, required }, next)
66
+ external.set(target, contract)
67
+ if (contract === undefined) return undefined
68
+
69
+ contracts[target] = contract
70
+ for (const section of Contract.read(contract, new Map(), target)
71
+ .stylesheets) {
72
+ let owner = target
73
+ for (const specifier of section.dependency ?? []) {
74
+ const resolved = await options.resolve(specifier, owner)
75
+ if (!resolved)
76
+ throw new Error(
77
+ `Unable to resolve packed stylesheet dependency: ${specifier}`,
78
+ )
79
+
80
+ ;(imports[owner] ??= Object.create(null))[specifier] = resolved
81
+ await link(resolved, true)
82
+ owner = resolved
83
+ }
84
+ }
85
+ return contract
86
+ }
87
+
88
+ async function parse(path: string) {
89
+ const source = await options.read(path)
90
+ let entry = syntax.get(path)
91
+ if (entry?.source !== source) {
92
+ entry = { parsed: Syntax.parse({ moduleId: id(path), source }), source }
93
+ syntax.set(path, entry)
94
+ }
95
+ return entry
96
+ }
97
+
98
+ // Mirrors the Next.js loader: component exports stay runtime code, and the package ships their CSS.
99
+ async function callable(
100
+ path: string,
101
+ name: string,
102
+ seen = new Set<string>(),
103
+ ): Promise<boolean> {
104
+ const key = JSON.stringify([path, name])
105
+ if (seen.has(key)) return false
106
+ seen.add(key)
107
+
108
+ const { program } = (await parse(path)).parsed
109
+ const functions = new Set<string>()
110
+
111
+ for (const statement of program.body) {
112
+ const declaration =
113
+ statement.type === 'ExportNamedDeclaration' ||
114
+ statement.type === 'ExportDefaultDeclaration'
115
+ ? statement.declaration
116
+ : undefined
117
+ const exported =
118
+ statement.type === 'ExportDefaultDeclaration'
119
+ ? 'default'
120
+ : declaration &&
121
+ 'id' in declaration &&
122
+ declaration.id?.type === 'Identifier'
123
+ ? declaration.id.name
124
+ : undefined
125
+
126
+ if (
127
+ exported &&
128
+ (declaration?.type === 'FunctionDeclaration' ||
129
+ declaration?.type === 'ClassDeclaration' ||
130
+ declaration?.type === 'ArrowFunctionExpression' ||
131
+ declaration?.type === 'FunctionExpression')
132
+ )
133
+ functions.add(exported)
134
+ if (declaration?.type === 'VariableDeclaration')
135
+ for (const item of declaration.declarations)
136
+ if (
137
+ item.id.type === 'Identifier' &&
138
+ (item.init?.type === 'ArrowFunctionExpression' ||
139
+ item.init?.type === 'FunctionExpression' ||
140
+ item.init?.type === 'ClassExpression')
141
+ )
142
+ functions.add(item.id.name)
143
+ }
144
+
145
+ // A merged namespace can carry style definitions beside the function.
146
+ for (const statement of program.body) {
147
+ const declaration =
148
+ statement.type === 'ExportNamedDeclaration'
149
+ ? statement.declaration
150
+ : statement
151
+ if (
152
+ declaration?.type === 'TSModuleDeclaration' &&
153
+ declaration.id.type === 'Identifier'
154
+ )
155
+ functions.delete(declaration.id.name)
156
+ }
157
+ if (functions.has(name)) return true
158
+
159
+ for (const statement of program.body) {
160
+ if (
161
+ statement.type !== 'ExportNamedDeclaration' ||
162
+ !statement.source ||
163
+ statement.exportKind === 'type'
164
+ )
165
+ continue
166
+
167
+ for (const specifier of statement.specifiers) {
168
+ const exported =
169
+ specifier.exported.type === 'Identifier'
170
+ ? specifier.exported.name
171
+ : specifier.exported.value
172
+ if (specifier.exportKind === 'type' || exported !== name) continue
173
+
174
+ const target = await options.resolve(statement.source.value, path)
175
+ if (!target || !inside(directory, target)) return false
176
+ return callable(
177
+ target,
178
+ specifier.local.type === 'Identifier'
179
+ ? specifier.local.name
180
+ : specifier.local.value,
181
+ seen,
182
+ )
183
+ }
184
+ }
185
+ return false
186
+ }
187
+
188
+ async function visit(path: string) {
189
+ const moduleId = id(path)
190
+ if (Object.hasOwn(modules, moduleId)) return
191
+
192
+ const entry = await parse(path)
193
+ modules[moduleId] = entry.source
194
+ programs.set(moduleId, entry.parsed)
195
+ const links: Record<string, string | null> = Object.create(null)
196
+ imports[moduleId] = links
197
+
198
+ for (const node of entry.parsed.program.body) {
199
+ if (
200
+ (node.type !== 'ImportDeclaration' &&
201
+ node.type !== 'ExportNamedDeclaration' &&
202
+ node.type !== 'ExportAllDeclaration') ||
203
+ !node.source
204
+ )
205
+ continue
206
+
207
+ const specifier = node.source.value
208
+ links[specifier] = null
209
+ if (
210
+ (node.type === 'ImportDeclaration'
211
+ ? node.importKind === 'type'
212
+ : node.exportKind === 'type') ||
213
+ ('specifiers' in node &&
214
+ node.specifiers.length &&
215
+ node.specifiers.every((specifier) =>
216
+ specifier.type === 'ImportSpecifier'
217
+ ? specifier.importKind === 'type'
218
+ : specifier.type === 'ExportSpecifier' &&
219
+ specifier.exportKind === 'type',
220
+ ))
221
+ )
222
+ continue
223
+ if (
224
+ specifier === 'zyzz' ||
225
+ (specifier.startsWith('zyzz/') && specifier !== 'zyzz/default')
226
+ ) {
227
+ authored = true
228
+ continue
229
+ }
230
+ if (
231
+ /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(specifier) ||
232
+ specifier.includes('?')
233
+ )
234
+ continue
235
+
236
+ const target = await options.resolve(specifier, path)
237
+ if (
238
+ !target ||
239
+ !Path.isAbsolute(target) ||
240
+ !/\.[cm]?[jt]sx?$/.test(target)
241
+ )
242
+ continue
243
+
244
+ if (!inside(directory, target)) {
245
+ if (await link(target)) links[specifier] = target
246
+ continue
247
+ }
248
+
249
+ if ('specifiers' in node && node.specifiers.length) {
250
+ const names = node.specifiers.map((item) => {
251
+ if (item.type === 'ImportDefaultSpecifier') return 'default'
252
+ if (item.type === 'ImportNamespaceSpecifier') return undefined
253
+ if (item.type === 'ImportSpecifier')
254
+ return item.importKind === 'type'
255
+ ? null
256
+ : item.imported.type === 'Identifier'
257
+ ? item.imported.name
258
+ : item.imported.value
259
+ if (item.exportKind === 'type') return null
260
+ return item.local.type === 'Identifier'
261
+ ? item.local.name
262
+ : item.local.value
263
+ })
264
+ const runtime = await Promise.all(
265
+ names.map((name) =>
266
+ name === null
267
+ ? true
268
+ : name === undefined
269
+ ? false
270
+ : callable(target, name),
271
+ ),
272
+ )
273
+ if (runtime.every(Boolean)) continue
274
+ }
275
+
276
+ links[specifier] = id(target)
277
+ await visit(target)
278
+ }
279
+ }
280
+
281
+ await visit(file)
282
+ if (!authored && !Object.keys(contracts).length) return undefined
283
+
284
+ const inputs = Crypto.createHash('sha256')
285
+ .update(JSON.stringify([modules, contracts, imports]))
286
+ .digest('hex')
287
+ const cached = derived.get(file)
288
+ if (cached?.inputs === inputs) return cached.contract
289
+
290
+ const graph = (() => {
291
+ try {
292
+ return Graph.compile({
293
+ [Syntax.cache]: programs,
294
+ compiler: false,
295
+ contracts,
296
+ imports,
297
+ modules,
298
+ })
299
+ } catch (error) {
300
+ throw new Error(
301
+ `Unable to derive the Zyzz contract of ${file}: ${(error as Error).message}`,
302
+ { cause: error },
303
+ )
304
+ }
305
+ })()
306
+
307
+ const contract = graph.contracts[id(file)]
308
+ derived.set(file, { contract, inputs })
309
+ return contract
310
+ }
311
+
312
+ return {
313
+ /**
314
+ * Returns the shipped `<file>.zyzz.json` contract, or derives it when the
315
+ * package depends on Zyzz and the module graph reaches its authoring APIs.
316
+ * Undefined marks a plain runtime import.
317
+ */
318
+ read(file: string, options: read.Options) {
319
+ return lookup(file, options, new Set())
320
+ },
321
+ }
322
+ }
323
+
324
+ /** Contract lookup contracts. */
325
+ export declare namespace read {
326
+ /** Capabilities of the requesting bundler. */
327
+ type Options = {
328
+ /** Reads a file, rejecting with `ENOENT` when it is missing. Every file whose content or absence affects the contract passes through it. */
329
+ readonly read: (file: string) => Promise<string>
330
+ /** Rethrows the missing sidecar error when no contract can be derived. */
331
+ readonly required?: boolean | undefined
332
+ /** Resolves a specifier with the bundler's conditions. Undefined marks an external. */
333
+ readonly resolve: (
334
+ specifier: string,
335
+ importer: string,
336
+ ) => Promise<string | undefined>
337
+ }
338
+ }
339
+
340
+ function inside(directory: string, file: string) {
341
+ const relative = Path.relative(directory, file)
342
+
343
+ return (
344
+ !relative.startsWith(`..${Path.sep}`) &&
345
+ relative !== '..' &&
346
+ !Path.isAbsolute(relative) &&
347
+ !relative.split(Path.sep).includes('node_modules')
348
+ )
349
+ }
350
+
351
+ /** Finds the named package owning a file, when its manifest declares Zyzz. */
352
+ async function manifest(file: string, options: read.Options) {
353
+ for (let directory = Path.dirname(file); ; ) {
354
+ const data = await options.read(Path.join(directory, 'package.json')).then(
355
+ (source) => JSON.parse(source) as Record<string, unknown>,
356
+ (error: NodeJS.ErrnoException) => {
357
+ if (error.code !== 'ENOENT') throw error
358
+ return undefined
359
+ },
360
+ )
361
+
362
+ // Nested manifests without a name, such as `{"type":"module"}`, belong to the enclosing package.
363
+ if (typeof data?.name === 'string')
364
+ return [
365
+ 'dependencies',
366
+ 'devDependencies',
367
+ 'optionalDependencies',
368
+ 'peerDependencies',
369
+ ].some((key) => {
370
+ const dependencies = data[key]
371
+ return (
372
+ !!dependencies &&
373
+ typeof dependencies === 'object' &&
374
+ Object.hasOwn(dependencies, 'zyzz')
375
+ )
376
+ })
377
+ ? { directory, name: data.name }
378
+ : undefined
379
+
380
+ const parent = Path.dirname(directory)
381
+ if (parent === directory) return undefined
382
+ directory = parent
383
+ }
384
+ }