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.
Files changed (119) hide show
  1. package/README.md +23 -365
  2. package/dist/Style.d.ts +2 -0
  3. package/dist/Style.d.ts.map +1 -1
  4. package/dist/compiler/Graph.d.ts +3 -0
  5. package/dist/compiler/Graph.d.ts.map +1 -1
  6. package/dist/compiler/Graph.js +38 -2
  7. package/dist/compiler/Graph.js.map +1 -1
  8. package/dist/compiler/Source.d.ts.map +1 -1
  9. package/dist/compiler/Source.js +11 -3
  10. package/dist/compiler/Source.js.map +1 -1
  11. package/dist/compiler/Transform.d.ts +4 -0
  12. package/dist/compiler/Transform.d.ts.map +1 -1
  13. package/dist/compiler/Transform.js +79 -15
  14. package/dist/compiler/Transform.js.map +1 -1
  15. package/dist/compiler/internal/Applications.d.ts +20 -0
  16. package/dist/compiler/internal/Applications.d.ts.map +1 -1
  17. package/dist/compiler/internal/Applications.js +151 -47
  18. package/dist/compiler/internal/Applications.js.map +1 -1
  19. package/dist/compiler/internal/Compositions.d.ts.map +1 -1
  20. package/dist/compiler/internal/Compositions.js +6 -0
  21. package/dist/compiler/internal/Compositions.js.map +1 -1
  22. package/dist/compiler/internal/Configurations.d.ts.map +1 -1
  23. package/dist/compiler/internal/Configurations.js +4 -0
  24. package/dist/compiler/internal/Configurations.js.map +1 -1
  25. package/dist/compiler/internal/Contract.d.ts.map +1 -1
  26. package/dist/compiler/internal/Contract.js +34 -7
  27. package/dist/compiler/internal/Contract.js.map +1 -1
  28. package/dist/compiler/internal/PackedStyles.d.ts.map +1 -1
  29. package/dist/compiler/internal/PackedStyles.js +7 -0
  30. package/dist/compiler/internal/PackedStyles.js.map +1 -1
  31. package/dist/compiler/internal/Themes.d.ts.map +1 -1
  32. package/dist/compiler/internal/Themes.js +20 -8
  33. package/dist/compiler/internal/Themes.js.map +1 -1
  34. package/dist/internal/Configuration.d.ts +10 -0
  35. package/dist/internal/Configuration.d.ts.map +1 -1
  36. package/dist/internal/Configuration.js +10 -1
  37. package/dist/internal/Configuration.js.map +1 -1
  38. package/dist/internal/Token.d.ts +2 -0
  39. package/dist/internal/Token.d.ts.map +1 -1
  40. package/dist/internal/Token.js.map +1 -1
  41. package/dist/next/internal/Project.d.ts.map +1 -1
  42. package/dist/next/internal/Project.js +30 -25
  43. package/dist/next/internal/Project.js.map +1 -1
  44. package/dist/next/loader.js +2 -1
  45. package/dist/next/loader.js.map +1 -1
  46. package/dist/node/Host.d.ts.map +1 -1
  47. package/dist/node/Host.js +64 -27
  48. package/dist/node/Host.js.map +1 -1
  49. package/dist/node/internal/Contracts.d.ts +22 -0
  50. package/dist/node/internal/Contracts.d.ts.map +1 -0
  51. package/dist/node/internal/Contracts.js +287 -0
  52. package/dist/node/internal/Contracts.js.map +1 -0
  53. package/dist/react-native/internal/Device.js +90 -37
  54. package/dist/react-native/internal/Device.js.map +3 -3
  55. package/dist/react-native/internal/NativeZyzz.js.map +2 -2
  56. package/dist/react-native/internal/Store.d.ts +4 -0
  57. package/dist/react-native/internal/Store.d.ts.map +1 -1
  58. package/dist/react-native/internal/Store.js.map +1 -1
  59. package/dist/react-native/internal/Viewport.d.ts +6 -0
  60. package/dist/react-native/internal/Viewport.d.ts.map +1 -1
  61. package/dist/react-native/internal/Viewport.js +2 -0
  62. package/dist/react-native/internal/Viewport.js.map +1 -1
  63. package/dist/react-native/react.d.ts +5 -2
  64. package/dist/react-native/react.d.ts.map +1 -1
  65. package/dist/react-native/react.js +60 -8
  66. package/dist/react-native/react.js.map +1 -1
  67. package/dist/runtime/NativeContext.d.ts +6 -0
  68. package/dist/runtime/NativeContext.d.ts.map +1 -1
  69. package/dist/runtime/NativeContext.js +62 -9
  70. package/dist/runtime/NativeContext.js.map +1 -1
  71. package/dist/unplugin/index.d.ts.map +1 -1
  72. package/dist/unplugin/index.js +35 -14
  73. package/dist/unplugin/index.js.map +1 -1
  74. package/dist/variants.d.ts +3 -2
  75. package/dist/variants.d.ts.map +1 -1
  76. package/dist/variants.js.map +1 -1
  77. package/dist/vite/index.d.ts.map +1 -1
  78. package/dist/vite/index.js +30 -37
  79. package/dist/vite/index.js.map +1 -1
  80. package/dist/web/Css.d.ts +10 -5
  81. package/dist/web/Css.d.ts.map +1 -1
  82. package/dist/web/Css.js +170 -31
  83. package/dist/web/Css.js.map +1 -1
  84. package/dist/web/internal/ClassName.d.ts +49 -0
  85. package/dist/web/internal/ClassName.d.ts.map +1 -1
  86. package/dist/web/internal/ClassName.js +57 -0
  87. package/dist/web/internal/ClassName.js.map +1 -1
  88. package/native/NativeZyzz.cpp +52 -65
  89. package/native/NativeZyzz.h +0 -4
  90. package/native/ZyzzSpecJSI.h +0 -10
  91. package/native/android/ZyzzSpec-generated.cpp +0 -6
  92. package/package.json +1 -1
  93. package/src/Style.ts +2 -0
  94. package/src/compiler/Graph.ts +42 -2
  95. package/src/compiler/Source.ts +11 -3
  96. package/src/compiler/Transform.ts +130 -29
  97. package/src/compiler/internal/Applications.ts +239 -63
  98. package/src/compiler/internal/Compositions.ts +9 -0
  99. package/src/compiler/internal/Configurations.ts +4 -0
  100. package/src/compiler/internal/Contract.ts +59 -7
  101. package/src/compiler/internal/PackedStyles.ts +9 -0
  102. package/src/compiler/internal/Themes.ts +25 -7
  103. package/src/internal/Configuration.ts +23 -1
  104. package/src/internal/Token.ts +2 -0
  105. package/src/next/internal/Project.ts +28 -23
  106. package/src/next/loader.ts +2 -1
  107. package/src/node/Host.ts +90 -36
  108. package/src/node/internal/Contracts.ts +384 -0
  109. package/src/react-native/internal/Device.ts +129 -48
  110. package/src/react-native/internal/NativeZyzz.ts +4 -6
  111. package/src/react-native/internal/Store.ts +4 -0
  112. package/src/react-native/internal/Viewport.ts +7 -0
  113. package/src/react-native/react.ts +81 -11
  114. package/src/runtime/NativeContext.ts +83 -9
  115. package/src/unplugin/index.ts +39 -14
  116. package/src/variants.ts +3 -2
  117. package/src/vite/index.ts +35 -36
  118. package/src/web/Css.ts +228 -42
  119. 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
- ...config,
43
+ get appearance() {
44
+ return create().appearance
45
+ },
34
46
  Provider: BoundProvider,
35
- }) as defineConfig.ReturnType<options>
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' && props.colorScheme !== 'dark') ||
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 resolved light/dark scheme and a nonempty vars name.',
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({ colorScheme: props.colorScheme, set: props.vars, viewport }),
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.colorScheme !== props.colorScheme ||
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({ colorScheme: props.colorScheme, set: props.vars, viewport })
159
+ store.update({ adaptive, colorScheme, set: props.vars, viewport })
93
160
  publish()
94
- }, [children, pending, props.colorScheme, props.vars, store, viewport])
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
- /** Resolved device scheme or an application override. */
106
- readonly colorScheme: 'dark' | 'light'
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
- return table[context.colorScheme](input!).style
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
- ...key(value, context),
99
- ...key(input, context),
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
- ...read(context),
130
- ...key(input, context),
131
- ],
202
+ [dependency]: (context: Context) =>
203
+ input === undefined
204
+ ? read(context)
205
+ : [...read(context), ...key(input, context)],
132
206
  },
133
207
  }
134
208
  }
@@ -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
- files.add(sidecar)
209
- try {
210
- contracts[file] = await snapshot.read(sidecar)
211
- } catch (error) {
212
- if (
213
- !required &&
214
- (error as NodeJS.ErrnoException).code === 'ENOENT'
215
- )
216
- return false
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[]) => unknown
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
- ) => unknown
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 sidecar = `${physical}.zyzz.json`
516
-
517
- if (Path.isAbsolute(physical) && /\.[cm]?[jt]sx?$/.test(physical)) {
518
- try {
519
- contracts[resolved.id] = await Fs.readFile(sidecar, 'utf8')
520
- host.watch(sidecar)
521
- files.add(sidecar)
522
- resolutions[specifier] = resolved.id
523
- continue
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
- try {
653
- const sidecar = `${physical}.zyzz.json`
660
+ const contract = await packages.read(physical, contractHost)
661
+ if (contract === undefined) continue
654
662
 
655
- contracts[resolved.id] = await Fs.readFile(sidecar, 'utf8')
656
- host.watch(sidecar)
657
- files.add(sidecar)
663
+ contracts[resolved.id] = contract
658
664
 
659
- // Discover the package contribution without compiling unrelated authoring
660
- // expressions in an otherwise unreachable source file.
661
- const discovery = `${sourceId(file)}.zyzz-discovery`
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
- modules[discovery] =
664
- (modules[discovery] ?? '') +
665
- `import ${JSON.stringify(specifier)};\n`
666
- ;(imports[discovery] ??= Object.create(null))[specifier] =
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
- if (!Object.hasOwn(contracts, target.id)) {
717
- const sidecar = `${target.id.split(/[?#]/)[0]}.zyzz.json`
718
-
719
- contracts[target.id] = await Fs.readFile(sidecar, 'utf8')
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