zyzz 0.0.23 → 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 +10 -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/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 +64 -27
- 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/Device.js +90 -37
- package/dist/react-native/internal/Device.js.map +3 -3
- package/dist/react-native/internal/NativeZyzz.js.map +2 -2
- package/dist/react-native/internal/Store.d.ts +4 -0
- package/dist/react-native/internal/Store.d.ts.map +1 -1
- package/dist/react-native/internal/Store.js.map +1 -1
- package/dist/react-native/internal/Viewport.d.ts +6 -0
- package/dist/react-native/internal/Viewport.d.ts.map +1 -1
- package/dist/react-native/internal/Viewport.js +2 -0
- package/dist/react-native/internal/Viewport.js.map +1 -1
- package/dist/react-native/react.d.ts +5 -2
- package/dist/react-native/react.d.ts.map +1 -1
- package/dist/react-native/react.js +60 -8
- package/dist/react-native/react.js.map +1 -1
- package/dist/runtime/NativeContext.d.ts +6 -0
- package/dist/runtime/NativeContext.d.ts.map +1 -1
- package/dist/runtime/NativeContext.js +62 -9
- package/dist/runtime/NativeContext.js.map +1 -1
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +35 -14
- package/dist/unplugin/index.js.map +1 -1
- package/dist/variants.d.ts +3 -2
- package/dist/variants.d.ts.map +1 -1
- package/dist/variants.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/native/NativeZyzz.cpp +52 -65
- package/native/NativeZyzz.h +0 -4
- package/native/ZyzzSpecJSI.h +0 -10
- package/native/android/ZyzzSpec-generated.cpp +0 -6
- 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 +23 -1
- 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 +90 -36
- package/src/node/internal/Contracts.ts +384 -0
- package/src/react-native/internal/Device.ts +129 -48
- package/src/react-native/internal/NativeZyzz.ts +4 -6
- package/src/react-native/internal/Store.ts +4 -0
- package/src/react-native/internal/Viewport.ts +7 -0
- package/src/react-native/react.ts +81 -11
- package/src/runtime/NativeContext.ts +83 -9
- package/src/unplugin/index.ts +39 -14
- package/src/variants.ts +3 -2
- package/src/vite/index.ts +35 -36
- package/src/web/Css.ts +228 -42
- package/src/web/internal/ClassName.ts +114 -0
|
@@ -6,8 +6,12 @@ import * as React from 'react'
|
|
|
6
6
|
export type Patch = {
|
|
7
7
|
/** Runtime-local mounted view identifier. */
|
|
8
8
|
readonly id: number
|
|
9
|
+
/** Mounted Fabric node, sent until the binding's first patch commits. */
|
|
10
|
+
readonly node?: object | undefined
|
|
9
11
|
/** Processed native style properties. */
|
|
10
12
|
readonly props: Record<string, unknown>
|
|
13
|
+
/** Processed style React rendered, sent with `node`. */
|
|
14
|
+
readonly rendered?: Record<string, unknown> | undefined
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
type Selection = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Connects the native device adapter to framework subscriptions. @module */
|
|
2
|
+
import type * as NativeContext from '../../runtime/NativeContext.js'
|
|
2
3
|
import type * as Media from '../../runtime/internal/NativeMedia.js'
|
|
3
4
|
import * as React from 'react'
|
|
4
5
|
|
|
@@ -6,3 +7,9 @@ import * as React from 'react'
|
|
|
6
7
|
export const context = React.createContext<Media.Viewport | undefined>(
|
|
7
8
|
undefined,
|
|
8
9
|
)
|
|
10
|
+
|
|
11
|
+
/** Device appearance for `colorScheme: 'system'`, with the platform's appearance-following color when it has one. */
|
|
12
|
+
export const appearance = React.createContext<{
|
|
13
|
+
readonly adaptive?: NativeContext.Context['adaptive']
|
|
14
|
+
readonly colorScheme: 'dark' | 'light'
|
|
15
|
+
}>({ colorScheme: 'light' })
|
|
@@ -12,7 +12,6 @@ import * as Viewport from './internal/Viewport.js'
|
|
|
12
12
|
export function defineConfig<const options extends Config.create.Options = {}>(
|
|
13
13
|
options: options & Parameters<typeof Config.create<options>>[0] = {} as never,
|
|
14
14
|
): defineConfig.ReturnType<options> {
|
|
15
|
-
const config = Config.create<options>(options)
|
|
16
15
|
const defaultVars = (options as Config.VariableOptions).defaultVars
|
|
17
16
|
const names =
|
|
18
17
|
defaultVars === undefined
|
|
@@ -29,10 +28,71 @@ export function defineConfig<const options extends Config.create.Options = {}>(
|
|
|
29
28
|
})
|
|
30
29
|
}
|
|
31
30
|
|
|
31
|
+
if ((options as Config.VariableOptions).vars === undefined)
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
...Config.create<options>(options),
|
|
34
|
+
Provider: BoundProvider,
|
|
35
|
+
}) as defineConfig.ReturnType<options>
|
|
36
|
+
|
|
37
|
+
// Compiled native styles and `useVars` read precompiled tables, so variable
|
|
38
|
+
// configurations build their authoring helpers on first runtime use.
|
|
39
|
+
let config: Helpers | undefined
|
|
40
|
+
const create = () =>
|
|
41
|
+
(config ??= (Config.create as (options: object) => Helpers)(options))
|
|
32
42
|
return Object.freeze({
|
|
33
|
-
|
|
43
|
+
get appearance() {
|
|
44
|
+
return create().appearance
|
|
45
|
+
},
|
|
34
46
|
Provider: BoundProvider,
|
|
35
|
-
|
|
47
|
+
get script() {
|
|
48
|
+
return create().script
|
|
49
|
+
},
|
|
50
|
+
style: (...args: never[]) => create().style(...args),
|
|
51
|
+
variants: (...args: never[]) => create().variants(...args),
|
|
52
|
+
vars: deferred(() => create().vars),
|
|
53
|
+
}) as unknown as defineConfig.ReturnType<options>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Untyped view of a variable configuration's helpers. */
|
|
57
|
+
type Helpers = {
|
|
58
|
+
readonly appearance: unknown
|
|
59
|
+
readonly script: unknown
|
|
60
|
+
readonly style: (...args: never[]) => unknown
|
|
61
|
+
readonly variants: (...args: never[]) => unknown
|
|
62
|
+
readonly vars: object
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Forwards every operation to a value built on first use, mirroring it onto the proxy target so invariants hold. */
|
|
66
|
+
function deferred<value extends object>(create: () => value): value {
|
|
67
|
+
const target = (() => {}) as unknown as value
|
|
68
|
+
let ready = false
|
|
69
|
+
function resolve() {
|
|
70
|
+
if (ready) return target
|
|
71
|
+
const value = create()
|
|
72
|
+
Object.defineProperties(target, Object.getOwnPropertyDescriptors(value))
|
|
73
|
+
Object.setPrototypeOf(target, Object.getPrototypeOf(value))
|
|
74
|
+
if (Object.isFrozen(value)) Object.freeze(target)
|
|
75
|
+
ready = true
|
|
76
|
+
return target
|
|
77
|
+
}
|
|
78
|
+
return new Proxy(target, {
|
|
79
|
+
apply: (_, self, args) =>
|
|
80
|
+
Reflect.apply(create() as (...args: unknown[]) => unknown, self, args),
|
|
81
|
+
defineProperty: (_, key, descriptor) =>
|
|
82
|
+
Reflect.defineProperty(resolve(), key, descriptor),
|
|
83
|
+
deleteProperty: (_, key) => Reflect.deleteProperty(resolve(), key),
|
|
84
|
+
get: (_, key) => Reflect.get(resolve(), key),
|
|
85
|
+
getOwnPropertyDescriptor: (_, key) =>
|
|
86
|
+
Reflect.getOwnPropertyDescriptor(resolve(), key),
|
|
87
|
+
getPrototypeOf: () => Reflect.getPrototypeOf(resolve()),
|
|
88
|
+
has: (_, key) => Reflect.has(resolve(), key),
|
|
89
|
+
isExtensible: () => Reflect.isExtensible(resolve()),
|
|
90
|
+
ownKeys: () => Reflect.ownKeys(resolve()),
|
|
91
|
+
preventExtensions: () => Reflect.preventExtensions(resolve()),
|
|
92
|
+
set: (_, key, value) => Reflect.set(resolve(), key, value),
|
|
93
|
+
setPrototypeOf: (_, prototype) =>
|
|
94
|
+
Reflect.setPrototypeOf(resolve(), prototype),
|
|
95
|
+
})
|
|
36
96
|
}
|
|
37
97
|
|
|
38
98
|
/** Native configuration helpers and inferred Provider props. */
|
|
@@ -62,23 +122,30 @@ export function Provider(props: Provider.Props) {
|
|
|
62
122
|
if (Object.hasOwn(props, 'set'))
|
|
63
123
|
throw new Error('Provider uses vars instead of set.')
|
|
64
124
|
if (
|
|
65
|
-
(props.colorScheme !== 'light' &&
|
|
125
|
+
(props.colorScheme !== 'light' &&
|
|
126
|
+
props.colorScheme !== 'dark' &&
|
|
127
|
+
props.colorScheme !== 'system') ||
|
|
66
128
|
(props.vars !== undefined &&
|
|
67
129
|
(typeof props.vars !== 'string' || !props.vars.trim()))
|
|
68
130
|
)
|
|
69
131
|
throw new Error(
|
|
70
|
-
'Native appearance requires a
|
|
132
|
+
'Native appearance requires a light, dark, or system scheme and a nonempty vars name.',
|
|
71
133
|
)
|
|
72
134
|
|
|
73
135
|
const viewport = React.useContext(Viewport.context)
|
|
136
|
+
const system = React.useContext(Viewport.appearance)
|
|
137
|
+
const colorScheme =
|
|
138
|
+
props.colorScheme === 'system' ? system.colorScheme : props.colorScheme
|
|
139
|
+
const adaptive = props.colorScheme === 'system' ? system.adaptive : undefined
|
|
74
140
|
const [store] = React.useState(() =>
|
|
75
|
-
Store.create({
|
|
141
|
+
Store.create({ adaptive, colorScheme, set: props.vars, viewport }),
|
|
76
142
|
)
|
|
77
143
|
const [, publish] = React.useReducer((version: number) => version + 1, 0)
|
|
78
144
|
const committed = React.useRef(props.children)
|
|
79
145
|
const snapshot = store.getSnapshot()
|
|
80
146
|
const pending =
|
|
81
|
-
snapshot.
|
|
147
|
+
snapshot.adaptive !== adaptive ||
|
|
148
|
+
snapshot.colorScheme !== colorScheme ||
|
|
82
149
|
snapshot.set !== props.vars ||
|
|
83
150
|
snapshot.viewport?.width !== viewport?.width ||
|
|
84
151
|
snapshot.viewport?.height !== viewport?.height
|
|
@@ -89,9 +156,9 @@ export function Provider(props: Provider.Props) {
|
|
|
89
156
|
// Keep the committed subtree until its selection is published. This avoids
|
|
90
157
|
// rendering new application props with the previous store snapshot, without
|
|
91
158
|
// mutating an external store during a potentially abandoned React render.
|
|
92
|
-
store.update({
|
|
159
|
+
store.update({ adaptive, colorScheme, set: props.vars, viewport })
|
|
93
160
|
publish()
|
|
94
|
-
}, [
|
|
161
|
+
}, [adaptive, children, colorScheme, pending, props.vars, store, viewport])
|
|
95
162
|
|
|
96
163
|
return React.createElement(Store.context.Provider, { value: store }, children)
|
|
97
164
|
}
|
|
@@ -102,8 +169,11 @@ export declare namespace Provider {
|
|
|
102
169
|
type Props = {
|
|
103
170
|
/** Components that consume compiled native styling or values. */
|
|
104
171
|
readonly children?: React.ReactNode | undefined
|
|
105
|
-
/**
|
|
106
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Resolved device scheme, an application override, or `system` to follow the device.
|
|
174
|
+
* On iOS, `system` styles whose schemes differ only in color switch without updates.
|
|
175
|
+
*/
|
|
176
|
+
readonly colorScheme: 'dark' | 'light' | 'system'
|
|
107
177
|
/** Selected variables. Omission uses each configuration's default. */
|
|
108
178
|
readonly vars?: string | undefined
|
|
109
179
|
}
|
|
@@ -7,6 +7,14 @@ const dependency = Symbol('zyzz.native.dependency')
|
|
|
7
7
|
|
|
8
8
|
/** Render-local set and resolved appearance. */
|
|
9
9
|
export type Context = {
|
|
10
|
+
/**
|
|
11
|
+
* Builds a color that follows the platform appearance. When present, styles whose
|
|
12
|
+
* light and dark selections differ only in color use it and need no scheme update.
|
|
13
|
+
* Must be stable, since adapted styles are cached per selection pair.
|
|
14
|
+
*/
|
|
15
|
+
readonly adaptive?:
|
|
16
|
+
| ((light: string | number, dark: string | number) => unknown)
|
|
17
|
+
| undefined
|
|
10
18
|
/** Resolved system appearance or an explicit override. */
|
|
11
19
|
readonly colorScheme: 'dark' | 'light'
|
|
12
20
|
/** Named compiled set; omission uses each configuration default. */
|
|
@@ -37,7 +45,15 @@ export function create<const tables extends Tables>(
|
|
|
37
45
|
const set = context.set ?? defaultVars
|
|
38
46
|
const table = tables[set] ?? fallback
|
|
39
47
|
if (!table) throw new Error(`Unknown native set: ${set}.`)
|
|
40
|
-
|
|
48
|
+
const style = table[context.colorScheme](input!).style
|
|
49
|
+
if (!context.adaptive) return style
|
|
50
|
+
return (
|
|
51
|
+
adapt(
|
|
52
|
+
table.light(input!).style,
|
|
53
|
+
table.dark(input!).style,
|
|
54
|
+
context.adaptive,
|
|
55
|
+
) ?? style
|
|
56
|
+
)
|
|
41
57
|
}
|
|
42
58
|
return bind(select, (context) => {
|
|
43
59
|
const table = tables[context.set ?? defaultVars] ?? fallback
|
|
@@ -94,10 +110,10 @@ export function responsive<
|
|
|
94
110
|
export function application(value: unknown, input?: unknown): unknown {
|
|
95
111
|
return {
|
|
96
112
|
[binding]: (context: Context) => resolve(value, context, input),
|
|
97
|
-
[dependency]: (context: Context) =>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
113
|
+
[dependency]: (context: Context) =>
|
|
114
|
+
input === undefined
|
|
115
|
+
? key(value, context)
|
|
116
|
+
: [...key(value, context), ...key(input, context)],
|
|
101
117
|
}
|
|
102
118
|
}
|
|
103
119
|
|
|
@@ -117,6 +133,64 @@ export function key(value: unknown, context: Context): readonly unknown[] {
|
|
|
117
133
|
return []
|
|
118
134
|
}
|
|
119
135
|
|
|
136
|
+
const adapted = new WeakMap<object, WeakMap<object, object | null>>()
|
|
137
|
+
|
|
138
|
+
/** Merges selections that differ only in color, or returns undefined. Results keep their identity across schemes. */
|
|
139
|
+
function adapt(
|
|
140
|
+
light: unknown,
|
|
141
|
+
dark: unknown,
|
|
142
|
+
color: NonNullable<Context['adaptive']>,
|
|
143
|
+
): unknown {
|
|
144
|
+
if (light === dark) return light
|
|
145
|
+
if (!record(light) || !record(dark)) return undefined
|
|
146
|
+
let pairs = adapted.get(light)
|
|
147
|
+
if (!pairs) adapted.set(light, (pairs = new WeakMap()))
|
|
148
|
+
if (pairs.has(dark)) return pairs.get(dark) ?? undefined
|
|
149
|
+
const result: Record<string, unknown> | null = (() => {
|
|
150
|
+
const keys = Object.keys(light)
|
|
151
|
+
if (keys.length !== Object.keys(dark).length) return null
|
|
152
|
+
const merged: Record<string, unknown> = {}
|
|
153
|
+
for (const key of keys) {
|
|
154
|
+
if (!Object.hasOwn(dark, key)) return null
|
|
155
|
+
const [first, second] = [light[key], dark[key]]
|
|
156
|
+
if (equal(first, second)) merged[key] = first
|
|
157
|
+
else if (
|
|
158
|
+
(key === 'color' || key.endsWith('Color')) &&
|
|
159
|
+
(typeof first === 'string' || typeof first === 'number') &&
|
|
160
|
+
(typeof second === 'string' || typeof second === 'number')
|
|
161
|
+
)
|
|
162
|
+
merged[key] = color(first, second)
|
|
163
|
+
else return null
|
|
164
|
+
}
|
|
165
|
+
return merged
|
|
166
|
+
})()
|
|
167
|
+
const value = result && Object.freeze(result)
|
|
168
|
+
pairs.set(dark, value)
|
|
169
|
+
return value ?? undefined
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function equal(left: unknown, right: unknown): boolean {
|
|
173
|
+
if (Object.is(left, right)) return true
|
|
174
|
+
if (Array.isArray(left))
|
|
175
|
+
return (
|
|
176
|
+
Array.isArray(right) &&
|
|
177
|
+
left.length === right.length &&
|
|
178
|
+
left.every((value, index) => equal(value, right[index]))
|
|
179
|
+
)
|
|
180
|
+
if (!record(left) || !record(right)) return false
|
|
181
|
+
const keys = Object.keys(left)
|
|
182
|
+
return (
|
|
183
|
+
keys.length === Object.keys(right).length &&
|
|
184
|
+
keys.every(
|
|
185
|
+
(key) => Object.hasOwn(right, key) && equal(left[key], right[key]),
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function record(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
191
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
192
|
+
}
|
|
193
|
+
|
|
120
194
|
function bind(
|
|
121
195
|
select: (context: Context, input?: never) => unknown,
|
|
122
196
|
read: (context: Context) => readonly unknown[],
|
|
@@ -125,10 +199,10 @@ function bind(
|
|
|
125
199
|
return {
|
|
126
200
|
style: {
|
|
127
201
|
[binding]: (context: Context) => select(context, input),
|
|
128
|
-
[dependency]: (context: Context) =>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
202
|
+
[dependency]: (context: Context) =>
|
|
203
|
+
input === undefined
|
|
204
|
+
? read(context)
|
|
205
|
+
: [...read(context), ...key(input, context)],
|
|
132
206
|
},
|
|
133
207
|
}
|
|
134
208
|
}
|
package/src/unplugin/index.ts
CHANGED
|
@@ -11,8 +11,11 @@ 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'
|
|
18
|
+
import * as ThemeRules from '../web/internal/Themes.js'
|
|
16
19
|
|
|
17
20
|
/** Portable build configuration. Vite uses the options from `zyzz/vite` instead. */
|
|
18
21
|
export type Options = {
|
|
@@ -31,6 +34,7 @@ const portable = createUnplugin<Options | undefined, false>(
|
|
|
31
34
|
(options = {}, meta) => {
|
|
32
35
|
const root = Path.resolve(options.root ?? process.cwd())
|
|
33
36
|
const compiler = Graph.create()
|
|
37
|
+
const packages = Contracts.create()
|
|
34
38
|
const snapshot = Snapshot.create()
|
|
35
39
|
let graph: Graph.compile.ReturnType | undefined
|
|
36
40
|
let pending: Promise<void> | undefined
|
|
@@ -203,20 +207,17 @@ const portable = createUnplugin<Options | undefined, false>(
|
|
|
203
207
|
required = false,
|
|
204
208
|
): Promise<boolean> {
|
|
205
209
|
if (Object.hasOwn(contracts, file)) return true
|
|
206
|
-
const sidecar = `${file}.zyzz.json`
|
|
207
210
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
throw error
|
|
219
|
-
}
|
|
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
|
|
220
221
|
const metadata = Contract.read(contracts[file]!, new Map(), file)
|
|
221
222
|
for (const section of metadata.stylesheets) {
|
|
222
223
|
let owner = file
|
|
@@ -284,7 +285,10 @@ const portable = createUnplugin<Options | undefined, false>(
|
|
|
284
285
|
}
|
|
285
286
|
}
|
|
286
287
|
|
|
288
|
+
// Only the combined stylesheet is emitted, so generated theme rules are shared once.
|
|
287
289
|
const result = compiler.compile({
|
|
290
|
+
[ClassName.complete]: true,
|
|
291
|
+
[ThemeRules.shared]: 'all',
|
|
288
292
|
[Syntax.cache]: snapshot.programs(modules),
|
|
289
293
|
compiler: options.compiler,
|
|
290
294
|
reset: options.reset ? Reset.read() : undefined,
|
|
@@ -351,6 +355,7 @@ const portable = createUnplugin<Options | undefined, false>(
|
|
|
351
355
|
boundary: string,
|
|
352
356
|
outputDirectory = '.',
|
|
353
357
|
targets?: Readonly<Record<string, string>>,
|
|
358
|
+
emitted: ReadonlySet<string> = new Set(),
|
|
354
359
|
) {
|
|
355
360
|
const urls = new Set<string>()
|
|
356
361
|
AtRules.transform({
|
|
@@ -402,6 +407,18 @@ const portable = createUnplugin<Options | undefined, false>(
|
|
|
402
407
|
: { inputSourceMap: map, sourceMap: true }),
|
|
403
408
|
visitor: {
|
|
404
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
|
+
}
|
|
405
422
|
if (rule.type !== 'import') return
|
|
406
423
|
const url = replacements.get(rule.value.url)
|
|
407
424
|
if (url) return AtRules.relocateImport(rule.value, url)
|
|
@@ -432,6 +449,7 @@ const portable = createUnplugin<Options | undefined, false>(
|
|
|
432
449
|
},
|
|
433
450
|
]
|
|
434
451
|
const seen = new Set<string>()
|
|
452
|
+
const units = new Set<string>()
|
|
435
453
|
async function visit(id: string): Promise<void> {
|
|
436
454
|
if (seen.has(id)) return
|
|
437
455
|
seen.add(id)
|
|
@@ -439,12 +457,19 @@ const portable = createUnplugin<Options | undefined, false>(
|
|
|
439
457
|
await visit(dependency)
|
|
440
458
|
const output = result.modules[id]
|
|
441
459
|
if (output) {
|
|
460
|
+
const names = [...(output[ClassName.units]?.values() ?? [])].map(
|
|
461
|
+
(decision) => decision.name,
|
|
462
|
+
)
|
|
442
463
|
const processed = await stylesheet(
|
|
443
464
|
output.css,
|
|
444
465
|
JSON.stringify(output.cssMap),
|
|
445
466
|
Path.join(root, id.slice(4)),
|
|
446
467
|
await Fs.realpath(root),
|
|
468
|
+
'.',
|
|
469
|
+
undefined,
|
|
470
|
+
new Set(names.filter((name) => units.has(name))),
|
|
447
471
|
)
|
|
472
|
+
for (const name of names) units.add(name)
|
|
448
473
|
chunks.push({
|
|
449
474
|
code: Buffer.from(processed.code).toString(),
|
|
450
475
|
map: Buffer.from(processed.map!).toString(),
|
|
@@ -460,7 +485,7 @@ const portable = createUnplugin<Options | undefined, false>(
|
|
|
460
485
|
let line = 0
|
|
461
486
|
css = ''
|
|
462
487
|
for (const chunk of chunks) {
|
|
463
|
-
if (!chunk.code) continue
|
|
488
|
+
if (!chunk.code.trim()) continue
|
|
464
489
|
const traced = Mapping.fromMap(chunk.map)
|
|
465
490
|
for (const mapping of Mapping.allMappings(traced)) {
|
|
466
491
|
const generated = {
|
package/src/variants.ts
CHANGED
|
@@ -26,8 +26,9 @@ type ChoiceName<choices> = keyof choices extends infer key
|
|
|
26
26
|
? key
|
|
27
27
|
: never
|
|
28
28
|
: never
|
|
29
|
+
/** `void` targets skip dynamic choice return types, which resolve through `Checked` and cycle when selections are read first. */
|
|
29
30
|
type DynamicKeys<choices> = {
|
|
30
|
-
[key in keyof choices]: choices[key] extends (...args: never[]) =>
|
|
31
|
+
[key in keyof choices]: choices[key] extends (...args: never[]) => void
|
|
31
32
|
? key
|
|
32
33
|
: never
|
|
33
34
|
}[keyof choices]
|
|
@@ -36,7 +37,7 @@ type Choice<choices> =
|
|
|
36
37
|
| {
|
|
37
38
|
[key in DynamicKeys<choices>]: choices[key] extends (
|
|
38
39
|
values: infer values,
|
|
39
|
-
) =>
|
|
40
|
+
) => void
|
|
40
41
|
? { readonly [name in key]: values } & {
|
|
41
42
|
readonly [name in Exclude<keyof choices, key>]?: never
|
|
42
43
|
}
|
package/src/vite/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as Namespaces from '../compiler/internal/Namespaces.js'
|
|
6
6
|
import * as AtRules from '../compiler/internal/AtRules.js'
|
|
7
7
|
import * as Catalogs from '../compiler/internal/Catalogs.js'
|
|
8
|
+
import * as Contracts from '../node/internal/Contracts.js'
|
|
8
9
|
import * as Mapping from '@jridgewell/gen-mapping'
|
|
9
10
|
import * as Lightning from 'lightningcss'
|
|
10
11
|
import * as Crypto from 'node:crypto'
|
|
@@ -51,6 +52,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
|
|
|
51
52
|
const removed = new WeakMap<Environment, Set<string>>()
|
|
52
53
|
const reads = new WeakMap<Environment, Map<string, string>>()
|
|
53
54
|
const compilers = new WeakMap<Environment, Graph.create.ReturnType>()
|
|
55
|
+
const packages = Contracts.create()
|
|
54
56
|
|
|
55
57
|
function compiler(environment: Environment) {
|
|
56
58
|
let value = compilers.get(environment)
|
|
@@ -414,6 +416,16 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
|
|
|
414
416
|
const lazy: Record<string, ReadonlySet<string>> = Object.create(null)
|
|
415
417
|
const modules: Record<string, string> = Object.create(null)
|
|
416
418
|
const files = new Set<string>()
|
|
419
|
+
const contractHost: Contracts.read.Options = {
|
|
420
|
+
read: async (file) => {
|
|
421
|
+
const source = await snapshot(entry.environment).read(file)
|
|
422
|
+
host.watch(file)
|
|
423
|
+
files.add(file)
|
|
424
|
+
return source
|
|
425
|
+
},
|
|
426
|
+
resolve: async (specifier, importer) =>
|
|
427
|
+
(await resolve(specifier, importer))?.id.split(/[?#]/)[0],
|
|
428
|
+
}
|
|
417
429
|
|
|
418
430
|
async function visit(file: string, source?: string) {
|
|
419
431
|
if (files.has(file)) return
|
|
@@ -512,19 +524,15 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
|
|
|
512
524
|
}
|
|
513
525
|
if (resolved.external || !eligible(resolved.id)) {
|
|
514
526
|
const physical = resolved.id.split('?')[0]!.split('#')[0]!
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
} catch (error) {
|
|
525
|
-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT')
|
|
526
|
-
throw error
|
|
527
|
-
}
|
|
527
|
+
const contract =
|
|
528
|
+
Path.isAbsolute(physical) && /\.[cm]?[jt]sx?$/.test(physical)
|
|
529
|
+
? await packages.read(physical, contractHost)
|
|
530
|
+
: undefined
|
|
531
|
+
|
|
532
|
+
if (contract !== undefined) {
|
|
533
|
+
contracts[resolved.id] = contract
|
|
534
|
+
resolutions[specifier] = resolved.id
|
|
535
|
+
continue
|
|
528
536
|
}
|
|
529
537
|
|
|
530
538
|
resolutions[specifier] = null
|
|
@@ -649,25 +657,19 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
|
|
|
649
657
|
if (!Path.isAbsolute(physical) || !/\.[cm]?[jt]sx?$/.test(physical))
|
|
650
658
|
continue
|
|
651
659
|
|
|
652
|
-
|
|
653
|
-
|
|
660
|
+
const contract = await packages.read(physical, contractHost)
|
|
661
|
+
if (contract === undefined) continue
|
|
654
662
|
|
|
655
|
-
|
|
656
|
-
host.watch(sidecar)
|
|
657
|
-
files.add(sidecar)
|
|
663
|
+
contracts[resolved.id] = contract
|
|
658
664
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
665
|
+
// Discover the package contribution without compiling unrelated authoring
|
|
666
|
+
// expressions in an otherwise unreachable source file.
|
|
667
|
+
const discovery = `${sourceId(file)}.zyzz-discovery`
|
|
662
668
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
resolved.id
|
|
668
|
-
} catch (error) {
|
|
669
|
-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
670
|
-
}
|
|
669
|
+
modules[discovery] =
|
|
670
|
+
(modules[discovery] ?? '') +
|
|
671
|
+
`import ${JSON.stringify(specifier)};\n`
|
|
672
|
+
;(imports[discovery] ??= Object.create(null))[specifier] = resolved.id
|
|
671
673
|
}
|
|
672
674
|
}
|
|
673
675
|
|
|
@@ -713,13 +715,10 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
|
|
|
713
715
|
imports[owner] ??= Object.create(null)
|
|
714
716
|
imports[owner]![specifier] = target.id
|
|
715
717
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
host.watch(sidecar)
|
|
721
|
-
files.add(sidecar)
|
|
722
|
-
}
|
|
718
|
+
contracts[target.id] ??= (await packages.read(
|
|
719
|
+
target.id.split(/[?#]/)[0]!,
|
|
720
|
+
{ ...contractHost, required: true },
|
|
721
|
+
))!
|
|
723
722
|
|
|
724
723
|
await dependencies(target.id)
|
|
725
724
|
owner = target.id
|