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.
- package/README.md +23 -365
- package/dist/Style.d.ts +2 -0
- package/dist/Style.d.ts.map +1 -1
- package/dist/compiler/Graph.d.ts +3 -0
- package/dist/compiler/Graph.d.ts.map +1 -1
- package/dist/compiler/Graph.js +38 -2
- package/dist/compiler/Graph.js.map +1 -1
- package/dist/compiler/Source.d.ts.map +1 -1
- package/dist/compiler/Source.js +11 -3
- package/dist/compiler/Source.js.map +1 -1
- package/dist/compiler/Transform.d.ts +4 -0
- package/dist/compiler/Transform.d.ts.map +1 -1
- package/dist/compiler/Transform.js +79 -15
- package/dist/compiler/Transform.js.map +1 -1
- package/dist/compiler/internal/Applications.d.ts +20 -0
- package/dist/compiler/internal/Applications.d.ts.map +1 -1
- package/dist/compiler/internal/Applications.js +151 -47
- package/dist/compiler/internal/Applications.js.map +1 -1
- package/dist/compiler/internal/Compositions.d.ts.map +1 -1
- package/dist/compiler/internal/Compositions.js +6 -0
- package/dist/compiler/internal/Compositions.js.map +1 -1
- package/dist/compiler/internal/Configurations.d.ts.map +1 -1
- package/dist/compiler/internal/Configurations.js +4 -0
- package/dist/compiler/internal/Configurations.js.map +1 -1
- package/dist/compiler/internal/Contract.d.ts.map +1 -1
- package/dist/compiler/internal/Contract.js +34 -7
- package/dist/compiler/internal/Contract.js.map +1 -1
- package/dist/compiler/internal/PackedStyles.d.ts.map +1 -1
- package/dist/compiler/internal/PackedStyles.js +7 -0
- package/dist/compiler/internal/PackedStyles.js.map +1 -1
- package/dist/compiler/internal/Themes.d.ts.map +1 -1
- package/dist/compiler/internal/Themes.js +20 -8
- package/dist/compiler/internal/Themes.js.map +1 -1
- package/dist/internal/Configuration.d.ts +10 -0
- package/dist/internal/Configuration.d.ts.map +1 -1
- package/dist/internal/Configuration.js +8 -0
- package/dist/internal/Configuration.js.map +1 -1
- package/dist/internal/Token.d.ts +2 -0
- package/dist/internal/Token.d.ts.map +1 -1
- package/dist/internal/Token.js.map +1 -1
- package/dist/next/internal/Project.d.ts.map +1 -1
- package/dist/next/internal/Project.js +30 -25
- package/dist/next/internal/Project.js.map +1 -1
- package/dist/next/loader.js +2 -1
- package/dist/next/loader.js.map +1 -1
- package/dist/node/Host.d.ts.map +1 -1
- package/dist/node/Host.js +32 -23
- package/dist/node/Host.js.map +1 -1
- package/dist/node/internal/Contracts.d.ts +22 -0
- package/dist/node/internal/Contracts.d.ts.map +1 -0
- package/dist/node/internal/Contracts.js +287 -0
- package/dist/node/internal/Contracts.js.map +1 -0
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +31 -13
- package/dist/unplugin/index.js.map +1 -1
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +30 -37
- package/dist/vite/index.js.map +1 -1
- package/dist/web/Css.d.ts +10 -5
- package/dist/web/Css.d.ts.map +1 -1
- package/dist/web/Css.js +170 -31
- package/dist/web/Css.js.map +1 -1
- package/dist/web/internal/ClassName.d.ts +49 -0
- package/dist/web/internal/ClassName.d.ts.map +1 -1
- package/dist/web/internal/ClassName.js +57 -0
- package/dist/web/internal/ClassName.js.map +1 -1
- package/package.json +1 -1
- package/src/Style.ts +2 -0
- package/src/compiler/Graph.ts +42 -2
- package/src/compiler/Source.ts +11 -3
- package/src/compiler/Transform.ts +130 -29
- package/src/compiler/internal/Applications.ts +239 -63
- package/src/compiler/internal/Compositions.ts +9 -0
- package/src/compiler/internal/Configurations.ts +4 -0
- package/src/compiler/internal/Contract.ts +59 -7
- package/src/compiler/internal/PackedStyles.ts +9 -0
- package/src/compiler/internal/Themes.ts +25 -7
- package/src/internal/Configuration.ts +21 -0
- package/src/internal/Token.ts +2 -0
- package/src/next/internal/Project.ts +28 -23
- package/src/next/loader.ts +2 -1
- package/src/node/Host.ts +31 -24
- package/src/node/internal/Contracts.ts +384 -0
- package/src/unplugin/index.ts +35 -13
- package/src/vite/index.ts +35 -36
- package/src/web/Css.ts +228 -42
- 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
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
}
|
package/src/next/loader.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
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 (
|
|
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
|
+
}
|