zyzz 0.0.24 → 0.0.26
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/Vars.d.ts +18 -2
- package/dist/Vars.d.ts.map +1 -1
- package/dist/Vars.js +21 -3
- package/dist/Vars.js.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 +3 -0
- package/dist/compiler/internal/Configurations.d.ts.map +1 -1
- package/dist/compiler/internal/Configurations.js +6 -1
- 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 +123 -35
- 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 +13 -1
- 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/internal/VariableSets.d.ts +20 -4
- package/dist/internal/VariableSets.d.ts.map +1 -1
- package/dist/internal/VariableSets.js +70 -19
- package/dist/internal/VariableSets.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/react-native/internal/Tokens.d.ts.map +1 -1
- package/dist/react-native/internal/Tokens.js +3 -6
- package/dist/react-native/internal/Tokens.js.map +1 -1
- 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 +33 -44
- 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/dist/web/internal/Themes.d.ts.map +1 -1
- package/dist/web/internal/Themes.js +4 -2
- package/dist/web/internal/Themes.js.map +1 -1
- package/package.json +1 -1
- package/src/Style.ts +2 -0
- package/src/Vars.ts +104 -12
- 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 +8 -1
- package/src/compiler/internal/Contract.ts +59 -7
- package/src/compiler/internal/PackedStyles.ts +9 -0
- package/src/compiler/internal/Themes.ts +174 -52
- package/src/internal/Configuration.ts +26 -4
- package/src/internal/Token.ts +2 -0
- package/src/internal/VariableSets.ts +104 -24
- 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/react-native/internal/Tokens.ts +5 -6
- package/src/unplugin/index.ts +35 -13
- package/src/vite/index.ts +38 -45
- package/src/web/Css.ts +228 -42
- package/src/web/internal/ClassName.ts +114 -0
- package/src/web/internal/Themes.ts +12 -11
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
|
+
}
|
|
@@ -32,12 +32,11 @@ export function resolve(
|
|
|
32
32
|
(metadata.contract[Token.identity] !== undefined &&
|
|
33
33
|
metadata.contract[Token.identity] ===
|
|
34
34
|
value.contract[Token.identity]))
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (next === undefined) throw new Error('Theme is missing a live token.')
|
|
35
|
+
// A set sharing its contract with an extension lacks the extension's added paths.
|
|
36
|
+
const next =
|
|
37
|
+
shared && Object.hasOwn(metadata.values, value.path)
|
|
38
|
+
? metadata.values[value.path]!
|
|
39
|
+
: value.value
|
|
41
40
|
active.add(value)
|
|
42
41
|
const result = scalar(next)
|
|
43
42
|
active.delete(value)
|
package/src/unplugin/index.ts
CHANGED
|
@@ -11,7 +11,9 @@ import type { UnpluginBuildContext } from 'unplugin'
|
|
|
11
11
|
import * as Graph from '../compiler/Graph.js'
|
|
12
12
|
import * as AtRules from '../compiler/internal/AtRules.js'
|
|
13
13
|
import * as Catalogs from '../compiler/internal/Catalogs.js'
|
|
14
|
+
import * as ClassName from '../web/internal/ClassName.js'
|
|
14
15
|
import * as Contract from '../compiler/internal/Contract.js'
|
|
16
|
+
import * as Contracts from '../node/internal/Contracts.js'
|
|
15
17
|
import * as Vite from '../vite/index.js'
|
|
16
18
|
import * as ThemeRules from '../web/internal/Themes.js'
|
|
17
19
|
|
|
@@ -32,6 +34,7 @@ const portable = createUnplugin<Options | undefined, false>(
|
|
|
32
34
|
(options = {}, meta) => {
|
|
33
35
|
const root = Path.resolve(options.root ?? process.cwd())
|
|
34
36
|
const compiler = Graph.create()
|
|
37
|
+
const packages = Contracts.create()
|
|
35
38
|
const snapshot = Snapshot.create()
|
|
36
39
|
let graph: Graph.compile.ReturnType | undefined
|
|
37
40
|
let pending: Promise<void> | undefined
|
|
@@ -204,20 +207,17 @@ const portable = createUnplugin<Options | undefined, false>(
|
|
|
204
207
|
required = false,
|
|
205
208
|
): Promise<boolean> {
|
|
206
209
|
if (Object.hasOwn(contracts, file)) return true
|
|
207
|
-
const sidecar = `${file}.zyzz.json`
|
|
208
210
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
throw error
|
|
220
|
-
}
|
|
211
|
+
const source = await packages.read(file, {
|
|
212
|
+
read(path) {
|
|
213
|
+
files.add(path)
|
|
214
|
+
return snapshot.read(path)
|
|
215
|
+
},
|
|
216
|
+
required,
|
|
217
|
+
resolve,
|
|
218
|
+
})
|
|
219
|
+
if (source === undefined) return false
|
|
220
|
+
contracts[file] = source
|
|
221
221
|
const metadata = Contract.read(contracts[file]!, new Map(), file)
|
|
222
222
|
for (const section of metadata.stylesheets) {
|
|
223
223
|
let owner = file
|
|
@@ -287,6 +287,7 @@ const portable = createUnplugin<Options | undefined, false>(
|
|
|
287
287
|
|
|
288
288
|
// Only the combined stylesheet is emitted, so generated theme rules are shared once.
|
|
289
289
|
const result = compiler.compile({
|
|
290
|
+
[ClassName.complete]: true,
|
|
290
291
|
[ThemeRules.shared]: 'all',
|
|
291
292
|
[Syntax.cache]: snapshot.programs(modules),
|
|
292
293
|
compiler: options.compiler,
|
|
@@ -354,6 +355,7 @@ const portable = createUnplugin<Options | undefined, false>(
|
|
|
354
355
|
boundary: string,
|
|
355
356
|
outputDirectory = '.',
|
|
356
357
|
targets?: Readonly<Record<string, string>>,
|
|
358
|
+
emitted: ReadonlySet<string> = new Set(),
|
|
357
359
|
) {
|
|
358
360
|
const urls = new Set<string>()
|
|
359
361
|
AtRules.transform({
|
|
@@ -405,6 +407,18 @@ const portable = createUnplugin<Options | undefined, false>(
|
|
|
405
407
|
: { inputSourceMap: map, sourceMap: true }),
|
|
406
408
|
visitor: {
|
|
407
409
|
Rule(rule) {
|
|
410
|
+
// Shared rules repeat in each module that uses them, so keep the first.
|
|
411
|
+
if (rule.type === 'style') {
|
|
412
|
+
const [selector, ...others] = rule.value.selectors
|
|
413
|
+
const [component, ...rest] = selector ?? []
|
|
414
|
+
if (
|
|
415
|
+
!others.length &&
|
|
416
|
+
!rest.length &&
|
|
417
|
+
component?.type === 'class' &&
|
|
418
|
+
emitted.has(component.name)
|
|
419
|
+
)
|
|
420
|
+
return []
|
|
421
|
+
}
|
|
408
422
|
if (rule.type !== 'import') return
|
|
409
423
|
const url = replacements.get(rule.value.url)
|
|
410
424
|
if (url) return AtRules.relocateImport(rule.value, url)
|
|
@@ -435,6 +449,7 @@ const portable = createUnplugin<Options | undefined, false>(
|
|
|
435
449
|
},
|
|
436
450
|
]
|
|
437
451
|
const seen = new Set<string>()
|
|
452
|
+
const units = new Set<string>()
|
|
438
453
|
async function visit(id: string): Promise<void> {
|
|
439
454
|
if (seen.has(id)) return
|
|
440
455
|
seen.add(id)
|
|
@@ -442,12 +457,19 @@ const portable = createUnplugin<Options | undefined, false>(
|
|
|
442
457
|
await visit(dependency)
|
|
443
458
|
const output = result.modules[id]
|
|
444
459
|
if (output) {
|
|
460
|
+
const names = [...(output[ClassName.units]?.values() ?? [])].map(
|
|
461
|
+
(decision) => decision.name,
|
|
462
|
+
)
|
|
445
463
|
const processed = await stylesheet(
|
|
446
464
|
output.css,
|
|
447
465
|
JSON.stringify(output.cssMap),
|
|
448
466
|
Path.join(root, id.slice(4)),
|
|
449
467
|
await Fs.realpath(root),
|
|
468
|
+
'.',
|
|
469
|
+
undefined,
|
|
470
|
+
new Set(names.filter((name) => units.has(name))),
|
|
450
471
|
)
|
|
472
|
+
for (const name of names) units.add(name)
|
|
451
473
|
chunks.push({
|
|
452
474
|
code: Buffer.from(processed.code).toString(),
|
|
453
475
|
map: Buffer.from(processed.map!).toString(),
|