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.
- package/README.md +26 -1
- package/dist/Style.d.ts +3 -3
- package/dist/Style.d.ts.map +1 -1
- package/dist/Style.js +12 -1
- package/dist/Style.js.map +1 -1
- package/dist/babel/index.d.ts +2 -0
- package/dist/babel/index.d.ts.map +1 -1
- package/dist/babel/index.js +10 -1
- package/dist/babel/index.js.map +1 -1
- package/dist/compiler/Graph.d.ts +2 -0
- package/dist/compiler/Graph.d.ts.map +1 -1
- package/dist/compiler/Graph.js +19 -1
- package/dist/compiler/Graph.js.map +1 -1
- package/dist/compiler/Source.d.ts.map +1 -1
- package/dist/compiler/Source.js +5 -2
- package/dist/compiler/Source.js.map +1 -1
- package/dist/compiler/internal/Contributions.d.ts +1 -1
- package/dist/compiler/internal/Contributions.d.ts.map +1 -1
- package/dist/compiler/internal/Contributions.js +5 -3
- package/dist/compiler/internal/Contributions.js.map +1 -1
- package/dist/compiler/internal/Static.d.ts.map +1 -1
- package/dist/compiler/internal/Static.js +2 -0
- package/dist/compiler/internal/Static.js.map +1 -1
- package/dist/internal/Authoring.d.ts.map +1 -1
- package/dist/internal/Authoring.js +14 -0
- package/dist/internal/Authoring.js.map +1 -1
- package/dist/internal/Binding.d.ts +7 -0
- package/dist/internal/Binding.d.ts.map +1 -1
- package/dist/internal/Binding.js.map +1 -1
- package/dist/internal/Configuration.d.ts +1 -1
- package/dist/internal/Configuration.d.ts.map +1 -1
- package/dist/internal/Configuration.js +7 -6
- package/dist/internal/Configuration.js.map +1 -1
- package/dist/internal/Theme.d.ts +1 -1
- package/dist/internal/Theme.d.ts.map +1 -1
- package/dist/internal/Theme.js.map +1 -1
- package/dist/internal/Token.d.ts +3 -1
- package/dist/internal/Token.d.ts.map +1 -1
- package/dist/internal/Token.js +28 -3
- package/dist/internal/Token.js.map +1 -1
- package/dist/internal/Value.d.ts +22 -0
- package/dist/internal/Value.d.ts.map +1 -1
- package/dist/internal/Value.js +76 -9
- package/dist/internal/Value.js.map +1 -1
- package/dist/next/index.d.ts +8 -3
- package/dist/next/index.d.ts.map +1 -1
- package/dist/next/index.js +21 -10
- package/dist/next/index.js.map +1 -1
- package/dist/next/internal/Project.d.ts +28 -0
- package/dist/next/internal/Project.d.ts.map +1 -0
- package/dist/next/internal/Project.js +249 -0
- package/dist/next/internal/Project.js.map +1 -0
- package/dist/next/loader.d.ts +3 -5
- package/dist/next/loader.d.ts.map +1 -1
- package/dist/next/loader.js +14 -144
- package/dist/next/loader.js.map +1 -1
- package/dist/node/Reset.d.ts +3 -0
- package/dist/node/Reset.d.ts.map +1 -0
- package/dist/node/Reset.js +7 -0
- package/dist/node/Reset.js.map +1 -0
- package/dist/unplugin/index.d.ts +2 -0
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +2 -0
- package/dist/unplugin/index.js.map +1 -1
- package/dist/variants.d.ts +1 -1
- package/dist/variants.d.ts.map +1 -1
- package/dist/variants.js.map +1 -1
- package/dist/vite/index.d.ts +2 -0
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +38 -9
- package/dist/vite/index.js.map +1 -1
- package/package.json +1 -1
- package/src/Style.ts +42 -8
- package/src/babel/index.ts +23 -1
- package/src/compiler/Graph.ts +22 -0
- package/src/compiler/Source.ts +5 -1
- package/src/compiler/internal/Contributions.ts +5 -2
- package/src/compiler/internal/Static.ts +1 -0
- package/src/internal/Authoring.ts +21 -0
- package/src/internal/Binding.ts +30 -0
- package/src/internal/Configuration.ts +19 -13
- package/src/internal/Theme.ts +5 -1
- package/src/internal/Token.ts +34 -3
- package/src/internal/Value.ts +180 -10
- package/src/next/index.ts +34 -12
- package/src/next/internal/Project.ts +337 -0
- package/src/next/loader.ts +28 -190
- package/src/node/Reset.ts +7 -0
- package/src/unplugin/index.ts +4 -0
- package/src/variants.ts +1 -0
- package/src/vite/index.ts +43 -14
package/src/internal/Value.ts
CHANGED
|
@@ -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
|
-
| {
|
|
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 {
|
|
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)
|
|
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
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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(
|
|
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)
|
|
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
|
|
26
|
-
const shared = Path.join(
|
|
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
|
-
{
|
|
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: {
|
|
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: {
|
|
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:
|
|
109
|
+
include: loaderOptions.root,
|
|
94
110
|
test: /\.[cm]?[jt]sx?$/,
|
|
95
111
|
use: [
|
|
96
112
|
{
|
|
97
113
|
loader,
|
|
98
|
-
options: { ...
|
|
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
|
+
}
|