zyzz 0.0.13 → 0.0.15

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 (42) hide show
  1. package/dist/compiler/Graph.d.ts +3 -1
  2. package/dist/compiler/Graph.d.ts.map +1 -1
  3. package/dist/compiler/Graph.js +49 -5
  4. package/dist/compiler/Graph.js.map +1 -1
  5. package/dist/compiler/Source.d.ts.map +1 -1
  6. package/dist/compiler/Source.js +4 -1
  7. package/dist/compiler/Source.js.map +1 -1
  8. package/dist/compiler/Transform.d.ts +5 -0
  9. package/dist/compiler/Transform.d.ts.map +1 -1
  10. package/dist/compiler/Transform.js +8 -2
  11. package/dist/compiler/Transform.js.map +1 -1
  12. package/dist/compiler/internal/Stylesheets.d.ts +2 -0
  13. package/dist/compiler/internal/Stylesheets.d.ts.map +1 -1
  14. package/dist/compiler/internal/Stylesheets.js +2 -0
  15. package/dist/compiler/internal/Stylesheets.js.map +1 -1
  16. package/dist/default.js +1 -1
  17. package/dist/default.js.map +1 -1
  18. package/dist/default.js.zyzz.json +1 -1
  19. package/dist/internal/Identity.d.ts +4 -0
  20. package/dist/internal/Identity.d.ts.map +1 -1
  21. package/dist/internal/Identity.js +25 -0
  22. package/dist/internal/Identity.js.map +1 -1
  23. package/dist/next/loader.d.ts.map +1 -1
  24. package/dist/next/loader.js +30 -7
  25. package/dist/next/loader.js.map +1 -1
  26. package/dist/web/Css.d.ts +5 -0
  27. package/dist/web/Css.d.ts.map +1 -1
  28. package/dist/web/Css.js +3 -2
  29. package/dist/web/Css.js.map +1 -1
  30. package/dist/web/internal/Themes.d.ts +14 -1
  31. package/dist/web/internal/Themes.d.ts.map +1 -1
  32. package/dist/web/internal/Themes.js +45 -15
  33. package/dist/web/internal/Themes.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/compiler/Graph.ts +61 -6
  36. package/src/compiler/Source.ts +6 -1
  37. package/src/compiler/Transform.ts +12 -2
  38. package/src/compiler/internal/Stylesheets.ts +3 -0
  39. package/src/internal/Identity.ts +32 -0
  40. package/src/next/loader.ts +30 -11
  41. package/src/web/Css.ts +7 -1
  42. package/src/web/internal/Themes.ts +61 -13
@@ -17,6 +17,7 @@ import type * as Theme from '../internal/Theme.js'
17
17
  import * as Token from '../internal/Token.js'
18
18
  import * as Contract from './internal/Contract.js'
19
19
  import * as Relative from './internal/Relative.js'
20
+ import * as ThemeRules from '../web/internal/Themes.js'
20
21
  import * as Themes from './internal/Themes.js'
21
22
  import * as Source from './Source.js'
22
23
  import * as Scope from './internal/Scope.js'
@@ -75,11 +76,13 @@ export declare namespace compile {
75
76
  type ReturnType = {
76
77
  /** Packed resources deduplicated by the host across scoped source outputs. */
77
78
  readonly [Stylesheets.packed]?: readonly Stylesheets.Resource[]
79
+ /** Independently loaded reset for module-based hosts. */
80
+ readonly [Stylesheets.reset]?: Stylesheets.Resource | undefined
78
81
  /** Versioned compiler-only JSON per module; publish beside the compiled entrypoint as <entry>.zyzz.json. */
79
82
  readonly contracts: Readonly<Record<string, string>>
80
83
  /** Direct static runtime source and library-contract dependencies, keyed by module identity. */
81
84
  readonly dependencies: Readonly<Record<string, readonly string[]>>
82
- /** One eager stylesheet for all supplied modules. Load before module CSS. */
85
+ /** Shared contributions and responsive token defaults. Load before module CSS. */
83
86
  readonly sharedCss?: string | undefined
84
87
  /** Source map for source-owned and packed global contributions. */
85
88
  readonly sharedCssMap?: Mapping.EncodedSourceMap | undefined
@@ -190,6 +193,11 @@ function build(options: compile.Options, cache?: Cache): Cache {
190
193
  if (cache?.reset !== options.reset) cache = undefined
191
194
  if (cache?.cssOutput !== options.cssOutput) cache = undefined
192
195
  if (cache?.composition !== options.composition) cache = undefined
196
+ if (
197
+ (cache?.entry === undefined) !==
198
+ (options[Stylesheets.entry] === undefined)
199
+ )
200
+ cache = undefined
193
201
  const ids = Object.keys(options.modules).sort()
194
202
  const programs = new Map<string, ReturnType<typeof Syntax.parse>>()
195
203
  function parse(input: Syntax.parse.Options) {
@@ -1329,7 +1337,7 @@ function build(options: compile.Options, cache?: Cache): Cache {
1329
1337
  const sharedVisited = new Set<string>()
1330
1338
  const packed: Stylesheets.Resource[] = []
1331
1339
 
1332
- const shared = (() => {
1340
+ const sharedSections = (() => {
1333
1341
  try {
1334
1342
  const entry = options[Stylesheets.entry]
1335
1343
  const packedSections =
@@ -1379,7 +1387,7 @@ function build(options: compile.Options, cache?: Cache): Cache {
1379
1387
  if (order.css) packed.unshift({ ...order, id: order.css })
1380
1388
  }
1381
1389
 
1382
- return Stylesheets.render([
1390
+ return [
1383
1391
  ...(resetSource === undefined
1384
1392
  ? []
1385
1393
  : [
@@ -1393,7 +1401,7 @@ function build(options: compile.Options, cache?: Cache): Cache {
1393
1401
  },
1394
1402
  ]),
1395
1403
  ...sharedSections,
1396
- ...(options.reset === undefined
1404
+ ...(options.reset === undefined || entry !== undefined
1397
1405
  ? []
1398
1406
  : [
1399
1407
  {
@@ -1404,7 +1412,7 @@ function build(options: compile.Options, cache?: Cache): Cache {
1404
1412
  layers: [['reset']],
1405
1413
  },
1406
1414
  ]),
1407
- ])
1415
+ ] satisfies Stylesheets.Section[]
1408
1416
  } catch (error) {
1409
1417
  return fail(
1410
1418
  error instanceof Stylesheets.ConflictError ? error.source : ids[0]!,
@@ -1413,7 +1421,19 @@ function build(options: compile.Options, cache?: Cache): Cache {
1413
1421
  }
1414
1422
  })()
1415
1423
 
1416
- const sharedCss = shared.css
1424
+ function renderShared(sections: readonly Stylesheets.Section[]) {
1425
+ try {
1426
+ return Stylesheets.render(sections)
1427
+ } catch (error) {
1428
+ return fail(
1429
+ error instanceof Stylesheets.ConflictError ? error.source : ids[0]!,
1430
+ (error as Error).message,
1431
+ )
1432
+ }
1433
+ }
1434
+
1435
+ const contributions = renderShared(sharedSections)
1436
+
1417
1437
  // Every stylesheet includes all graph scopes, including unimported alternatives.
1418
1438
  const names = Object.keys(themes)
1419
1439
  const previousNames = Object.keys(previous?.themes ?? {})
@@ -1549,6 +1569,8 @@ function build(options: compile.Options, cache?: Cache): Cache {
1549
1569
  extracted.get(moduleId) === previous!.extracted.get(moduleId)
1550
1570
  ? previous!.result.modules[moduleId]!
1551
1571
  : Transform.compile({
1572
+ [ThemeRules.shared]:
1573
+ options[Stylesheets.entry] === undefined ? 'defaults' : 'all',
1552
1574
  compiler: options.compiler,
1553
1575
  development: options.development,
1554
1576
  composition: options.composition,
@@ -1627,6 +1649,23 @@ function build(options: compile.Options, cache?: Cache): Cache {
1627
1649
  styleClasses[call.identity] = modules[moduleId]!.classes[call.name]!
1628
1650
  }
1629
1651
 
1652
+ const defaults = new Map<string, Stylesheets.Section>()
1653
+ if (options[Stylesheets.entry] === undefined)
1654
+ for (const [source, output] of Object.entries(modules))
1655
+ for (const resource of output[ThemeRules.shared] ?? [])
1656
+ if (!defaults.has(resource.id))
1657
+ defaults.set(resource.id, {
1658
+ css: resource.css,
1659
+ key: resource.id,
1660
+ layers: [],
1661
+ source,
1662
+ })
1663
+
1664
+ const shared = defaults.size
1665
+ ? renderShared([...sharedSections, ...defaults.values()])
1666
+ : contributions
1667
+ const sharedCss = shared.css
1668
+
1630
1669
  function publishedStyle(link: Themes.Link): Themes.Link {
1631
1670
  return {
1632
1671
  ...link,
@@ -1698,6 +1737,22 @@ function build(options: compile.Options, cache?: Cache): Cache {
1698
1737
  transformed,
1699
1738
  resolutions: Object.freeze(resolutions),
1700
1739
  result: Object.freeze({
1740
+ ...(options[Stylesheets.entry] !== undefined &&
1741
+ options.reset !== undefined
1742
+ ? {
1743
+ [Stylesheets.reset]: {
1744
+ id: 'zyzz/reset.css',
1745
+ ...Stylesheets.render([
1746
+ {
1747
+ content: options.reset,
1748
+ css: options.reset,
1749
+ layers: [['reset']],
1750
+ source: 'zyzz/reset.css',
1751
+ },
1752
+ ]),
1753
+ },
1754
+ }
1755
+ : {}),
1701
1756
  ...(packed.length ? { [Stylesheets.packed]: packed } : {}),
1702
1757
  ...(sharedCss
1703
1758
  ? {
@@ -263,7 +263,12 @@ export function extract(options: extract.Options): extract.ReturnType {
263
263
  try {
264
264
  return Themes.collect(program, {
265
265
  staticBindings: staticData.bindings,
266
- namespace,
266
+ namespace: `src-${Identity.label(
267
+ options.moduleId
268
+ .split('/')
269
+ .at(-1)!
270
+ .replace(/\.[cm]?[jt]sx?$/, ''),
271
+ )}-${Identity.compact(options.moduleId)}`,
267
272
  contributionCalls: new Set(
268
273
  contributions.calls.map((call) => call.start),
269
274
  ),
@@ -15,6 +15,7 @@ import * as Scheme from '../internal/Scheme.js'
15
15
  import * as Syntax from './internal/Syntax.js'
16
16
  import * as Source from './Source.js'
17
17
  import type * as Style from '../Style.js'
18
+ import * as ThemeRules from '../web/internal/Themes.js'
18
19
  import * as Themes from './internal/Themes.js'
19
20
  import * as Walker from 'oxc-walker'
20
21
 
@@ -195,6 +196,7 @@ export function compile(options: compile.Options): compile.ReturnType {
195
196
  )
196
197
 
197
198
  const emitted = Css.compile({
199
+ [ThemeRules.shared]: options[ThemeRules.shared],
198
200
  development: options.development,
199
201
  scope: options.moduleId,
200
202
  composition: options.composition,
@@ -595,7 +597,8 @@ export function compile(options: compile.Options): compile.ReturnType {
595
597
  return `{${helpers(JSON.stringify(call.options?.themes ? entries : []), call.options?.themes ? String(fallback) : undefined).replace(/,$/, '')}}`
596
598
  usesSelection = true
597
599
  const selector = `${selection}.create(${JSON.stringify(entries)},${call.options?.output === 'html'},'set',${JSON.stringify(fallback)})`
598
- return `{${helpers(JSON.stringify(call.options?.themes ? entries : []), call.options?.themes ? String(fallback) : undefined)}vars:/*#__PURE__*/${selector}}`
600
+ // Rollup drops required selectors when a pure call initializes a destructured object property.
601
+ return `{${helpers(JSON.stringify(call.options?.themes ? entries : []), call.options?.themes ? String(fallback) : undefined)}vars:${selector}}`
599
602
  }
600
603
 
601
604
  if (call.options?.themes) {
@@ -980,7 +983,7 @@ export function compile(options: compile.Options): compile.ReturnType {
980
983
 
981
984
  if (
982
985
  rule.startsWith(':root{') ||
983
- rule.startsWith(':where(*){--z-f') ||
986
+ rule.startsWith(':where(*){--z-') ||
984
987
  (rule.startsWith('@media ') &&
985
988
  (rule.includes(':root{') ||
986
989
  Object.values(emitted.vars).some((name) =>
@@ -1150,6 +1153,9 @@ export function compile(options: compile.Options): compile.ReturnType {
1150
1153
  true,
1151
1154
  )
1152
1155
  return Object.freeze({
1156
+ ...(emitted[ThemeRules.shared]
1157
+ ? { [ThemeRules.shared]: emitted[ThemeRules.shared] }
1158
+ : {}),
1153
1159
  classes,
1154
1160
  code: portable ? options.source : module.toString(),
1155
1161
  css: namespaced.css,
@@ -1172,6 +1178,8 @@ export declare namespace compile {
1172
1178
  type ErrorType = Css.CompileError | Source.ExtractError
1173
1179
  /** Supplied module identity and source; no file loading occurs. */
1174
1180
  type Options = Source.extract.Options & {
1181
+ /** Separates generated token definitions for independently loaded modules. */
1182
+ readonly [ThemeRules.shared]?: 'all' | 'defaults' | undefined
1175
1183
  /** Disable source rewriting while emitting CSS for runtime authoring. Defaults to true. */
1176
1184
  readonly compiler?: boolean | undefined
1177
1185
  /** Whether compiled applications can be combined with one another. */
@@ -1191,6 +1199,8 @@ export declare namespace compile {
1191
1199
  type ReturnType = {
1192
1200
  /** Internal native node replacements, when supplied by the graph adapter. */
1193
1201
  readonly [Edits.key]?: readonly Edits.Edit[] | undefined
1202
+ /** Generated token resources delivered alongside this module's CSS. */
1203
+ readonly [ThemeRules.shared]?: readonly ThemeRules.Resource[] | undefined
1194
1204
  /** Module-scoped class lists keyed by extracted definition identity. */
1195
1205
  readonly classes: Readonly<Record<string, string>>
1196
1206
  /** Rewritten source with imports for surviving props callables. */
@@ -23,6 +23,9 @@ export const entry = Symbol('stylesheet entry')
23
23
  /** Independently shared packed sections for hosts that load source modules separately. */
24
24
  export const packed = Symbol('packed stylesheets')
25
25
 
26
+ /** Reset stylesheet shared independently of authored contributions and layer order. */
27
+ export const reset = Symbol('reset stylesheet')
28
+
26
29
  /** One packed resource with stable source ownership for development updates. */
27
30
  export type Resource = ReturnType<typeof render> & { readonly id: string }
28
31
 
@@ -3,6 +3,28 @@ import * as Binding from './Binding.js'
3
3
  import * as Token from './Token.js'
4
4
  import type * as Style from '../Style.js'
5
5
 
6
+ /** Encodes both hash streams in eleven CSS identifier characters. */
7
+ export function compact(value: string): string {
8
+ let first = 2166136261
9
+ let second = 5381
10
+ for (let index = 0; index < value.length; index++) {
11
+ const code = value.charCodeAt(index)
12
+ first = Math.imul(first ^ code, 16777619)
13
+ second = Math.imul(second, 33) ^ code
14
+ }
15
+
16
+ const alphabet =
17
+ '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-'
18
+ let bits = (BigInt(first >>> 0) << 32n) | BigInt(second >>> 0)
19
+ let result = ''
20
+ for (let index = 0; index < 11; index++) {
21
+ result = alphabet[Number(bits & 63n)]! + result
22
+ bits >>= 6n
23
+ }
24
+
25
+ return result
26
+ }
27
+
6
28
  /** Encodes an explicit identity without losing punctuation or Unicode distinctions. */
7
29
  export function encode(value: string): string {
8
30
  return Array.from(value, (character) =>
@@ -10,6 +32,16 @@ export function encode(value: string): string {
10
32
  ).join('-')
11
33
  }
12
34
 
35
+ /** Keeps an authored label readable while its separate hash disambiguates spelling. */
36
+ export function label(value: string): string {
37
+ return (
38
+ value
39
+ .replace(/[^a-zA-Z0-9-]+/g, '-')
40
+ .replace(/^-+|-+$/g, '')
41
+ .slice(0, 64) || 'value'
42
+ )
43
+ }
44
+
13
45
  /** Requires an explicit identity at an uncompiled authoring boundary. */
14
46
  export function requireId(id: string | undefined, kind: string): string {
15
47
  if (!id)
@@ -3,8 +3,9 @@ import * as AtRules from '../compiler/internal/AtRules.js'
3
3
  import * as Crypto from 'node:crypto'
4
4
  import * as Fs from 'node:fs/promises'
5
5
  import * as Path from 'node:path'
6
- import * as Stylesheets from '../compiler/internal/Stylesheets.js'
7
6
  import * as Project from './internal/Project.js'
7
+ import * as Stylesheets from '../compiler/internal/Stylesheets.js'
8
+ import * as ThemeRules from '../web/internal/Themes.js'
8
9
 
9
10
  type Context = Project.Context & {
10
11
  async(): (error: Error | null, code?: string, map?: object) => void
@@ -36,9 +37,16 @@ async function compile(context: Context, source: string) {
36
37
  const root = options.root
37
38
  const directory = Path.resolve(root, '.zyzz', 'next')
38
39
  if (options.mode) {
39
- const hash = new URLSearchParams(context.resourceQuery).get('zyzz')
40
+ const query = new URLSearchParams(context.resourceQuery)
41
+ const hash = query.get('zyzz')
40
42
  if (!hash || !/^[a-f0-9]{64}$/.test(hash))
41
43
  throw new Error('Invalid generated stylesheet request.')
44
+ const css = query.get('css')
45
+ if (css !== null)
46
+ return {
47
+ code: Buffer.from(css, 'base64url').toString('utf8'),
48
+ map: undefined,
49
+ }
42
50
  const file = Path.join(directory, `${hash}.css`)
43
51
  context.addDependency(file)
44
52
  return { code: await Fs.readFile(file, 'utf8'), map: undefined }
@@ -178,6 +186,15 @@ async function compile(context: Context, source: string) {
178
186
  }
179
187
 
180
188
  const styles = [
189
+ ...(graph[Stylesheets.reset]
190
+ ? [
191
+ {
192
+ css: graph[Stylesheets.reset].css,
193
+ id: graph[Stylesheets.reset].id,
194
+ map: JSON.stringify(graph[Stylesheets.reset].map),
195
+ },
196
+ ]
197
+ : []),
181
198
  ...(await Promise.all(
182
199
  (graph[Stylesheets.packed] ?? []).map(async (resource) => {
183
200
  const output = await compileShared(resource)
@@ -193,6 +210,11 @@ async function compile(context: Context, source: string) {
193
210
  css: shared?.code.toString(),
194
211
  map: shared?.map?.toString(),
195
212
  },
213
+ ...(output[ThemeRules.shared] ?? []).map((resource) => ({
214
+ css: resource.css,
215
+ id: resource.id,
216
+ map: undefined,
217
+ })),
196
218
  { id: undefined, css: output.css, map: JSON.stringify(cssMap) },
197
219
  ]
198
220
  const requests: string[] = []
@@ -215,13 +237,7 @@ async function compile(context: Context, source: string) {
215
237
  : '')
216
238
  const hash = Crypto.createHash('sha256')
217
239
  .update(
218
- options.development &&
219
- !(
220
- index === (graph[Stylesheets.packed]?.length ?? 0) &&
221
- graph.sharedCssMap?.sources.every(
222
- (name) => name === 'zyzz/reset.css',
223
- )
224
- )
240
+ options.development
225
241
  ? (stylesheet.id ??
226
242
  JSON.stringify([context.resourcePath, index, graph.dependencies]))
227
243
  : css,
@@ -240,7 +256,7 @@ async function compile(context: Context, source: string) {
240
256
  await Fs.writeFile(temporary, css)
241
257
  await Fs.rename(temporary, file)
242
258
  }
243
- } else {
259
+ } else if (options.bundler !== 'turbopack') {
244
260
  try {
245
261
  await Fs.writeFile(file, css, { flag: 'wx' })
246
262
  } catch (error) {
@@ -264,7 +280,10 @@ async function compile(context: Context, source: string) {
264
280
  )
265
281
  if (existing !== code) await Fs.writeFile(request, code)
266
282
  } else if (options.bundler === 'turbopack') {
267
- request = Path.join(directory, 'style.css') + `?zyzz=${hash}`
283
+ // Persist CSS with the cached transform, independently of generated filesystem artifacts.
284
+ request =
285
+ Path.join(directory, 'style.css') +
286
+ `?zyzz=${hash}&css=${Buffer.from(css).toString('base64url')}`
268
287
  }
269
288
  const relative = Path.relative(Path.dirname(context.resourcePath), request)
270
289
  .split(Path.sep)
package/src/web/Css.ts CHANGED
@@ -670,8 +670,9 @@ export function compile<
670
670
  ? (theme ??= Themes.create()).emit(
671
671
  options.vars ?? {},
672
672
  options.schemes ?? false,
673
+ options[Themes.shared],
673
674
  )
674
- : { classes: Object.freeze({}), css: '' }
675
+ : { classes: Object.freeze({}), css: '', resources: [] }
675
676
  } catch (error) {
676
677
  throw new CompileError([
677
678
  {
@@ -692,6 +693,7 @@ export function compile<
692
693
  .join('\n')
693
694
 
694
695
  return Object.freeze({
696
+ ...(scopes.resources.length ? { [Themes.shared]: scopes.resources } : {}),
695
697
  ...(contributionCss ? { contributionCss, scopedCss } : {}),
696
698
  classes: Object.freeze(classes),
697
699
  css: [contributionCss, scopedCss].filter(Boolean).join('\n'),
@@ -709,6 +711,8 @@ export declare namespace compile {
709
711
  name extends string = string,
710
712
  themeName extends string = string,
711
713
  > = {
714
+ /** Separates generated token rules for hosts with independent CSS resources. */
715
+ readonly [Themes.shared]?: 'all' | 'defaults' | undefined
712
716
  /** Fixed class identities used by CSS-only consumers. */
713
717
  readonly names?: Readonly<Record<string, string>> | undefined
714
718
  /**
@@ -743,6 +747,8 @@ export declare namespace compile {
743
747
  name extends string = string,
744
748
  themeName extends string = string,
745
749
  > = {
750
+ /** Shared token resources omitted from this module's stylesheet. */
751
+ readonly [Themes.shared]?: readonly Themes.Resource[] | undefined
746
752
  /** Contribution text separated for graph-wide hoisting. */
747
753
  readonly contributionCss?: string | undefined
748
754
  /** Ordinary scope and style rules when contributions were supplied. */
@@ -41,12 +41,14 @@ export function create() {
41
41
  contract.paths.set(token.path, name)
42
42
  }
43
43
 
44
- return `var(${name},${literal(value)})`
44
+ const label = contract.identity?.startsWith('src-') ? token.path : undefined
45
+ return `var(${name},${literal(value, label)})`
45
46
  }
46
47
 
47
48
  function emit(
48
49
  themes: Readonly<Record<string, Theme.Definition | Vars.Definition>>,
49
50
  schemes = false,
51
+ separate?: 'all' | 'defaults',
50
52
  ) {
51
53
  const classes: Record<string, string> = Object.create(null)
52
54
  const rules: string[] = []
@@ -101,13 +103,20 @@ export function create() {
101
103
  if (value === undefined)
102
104
  throw new Error('Theme scope is missing a live token.')
103
105
 
104
- return `${name}:${literal(value)};`
106
+ const label = contract.identity?.startsWith('src-') ? path : undefined
107
+ return `${name}:${literal(value, label)};`
105
108
  })
106
109
  .join('')
107
110
 
108
111
  rules.push(`.${className}{${body}}`)
109
112
  for (const [path, name] of contract.paths)
110
- conditional(data.values[path]!, name, `.${className}`, rules)
113
+ conditional(
114
+ data.values[path]!,
115
+ name,
116
+ `.${className}`,
117
+ rules,
118
+ contract.identity?.startsWith('src-') ? path : undefined,
119
+ )
111
120
  }
112
121
 
113
122
  // Scheme rules travel with the module that can apply them, so lowered
@@ -116,26 +125,50 @@ export function create() {
116
125
 
117
126
  return {
118
127
  classes: Object.freeze(classes),
119
- css: [...defaults.values(), ...rules].join('\n'),
128
+ css:
129
+ separate === 'all'
130
+ ? ''
131
+ : [...(separate ? [] : defaults.values()), ...rules].join('\n'),
132
+ resources: separate
133
+ ? [
134
+ ...[...defaults].map(([id, css]) => ({ css, id })),
135
+ // Scope rules stay together to preserve cascade order without one import per token.
136
+ ...(separate === 'all' && rules.length
137
+ ? [
138
+ {
139
+ css: rules.join('\n'),
140
+ id: JSON.stringify([
141
+ classes,
142
+ [...contracts.values()].flatMap((contract) => [
143
+ ...contract.paths.values(),
144
+ ]),
145
+ ]),
146
+ },
147
+ ]
148
+ : []),
149
+ ]
150
+ : [],
120
151
  }
121
152
  }
122
153
 
123
- function literal(value: Token.Value): string {
154
+ function literal(value: Token.Value, label?: string): string {
124
155
  if (Token.is(value)) return serialize(value)
125
156
  if (typeof value !== 'object') return String(value)
126
157
  if ('default' in value) {
127
158
  // Separate fallback properties preserve extensions and resolve references within each scope.
128
- const base = literal(value.default)
159
+ const base = literal(value.default, label)
129
160
  const rules = [`:where(*){--fallback:${base};}`]
130
- conditional(value, '--fallback', ':where(*)', rules)
161
+ conditional(value, '--fallback', ':where(*)', rules, label)
131
162
  const css = rules.join('')
132
- const name = `--z-f${Identity.hash(css)}`
163
+ const name = label
164
+ ? `--z-${Identity.label(label)}-fallback-${Identity.compact(css)}`
165
+ : `--z-f${Identity.hash(css)}`
133
166
  const emitted = [`:where(*){${name}:${base};}`]
134
- conditional(value, name, ':where(*)', emitted)
167
+ conditional(value, name, ':where(*)', emitted, label)
135
168
  defaults.set(name, emitted.join(''))
136
169
  return `var(${name})`
137
170
  }
138
- return `light-dark(${literal(value.light)},${literal(value.dark)})`
171
+ return `light-dark(${literal(value.light, label)},${literal(value.dark, label)})`
139
172
  }
140
173
 
141
174
  function conditional(
@@ -143,6 +176,7 @@ export function create() {
143
176
  name: string,
144
177
  selector: string,
145
178
  rules: string[],
179
+ label?: string,
146
180
  ) {
147
181
  if (
148
182
  !value ||
@@ -151,11 +185,11 @@ export function create() {
151
185
  !('default' in value)
152
186
  )
153
187
  return
154
- conditional(value.default, name, selector, rules)
188
+ conditional(value.default, name, selector, rules, label)
155
189
  for (const [query, entry] of Object.entries(value)) {
156
190
  if (query === 'default') continue
157
- const nested = [`${selector}{${name}:${literal(entry)};}`]
158
- conditional(entry, name, selector, nested)
191
+ const nested = [`${selector}{${name}:${literal(entry, label)};}`]
192
+ conditional(entry, name, selector, nested, label)
159
193
  rules.push(`${query}{${nested.join('')}}`)
160
194
  }
161
195
  }
@@ -171,6 +205,20 @@ export function encode(value: string): string {
171
205
  )
172
206
  }
173
207
 
208
+ /** One generated definition whose scope order must remain intact. */
209
+ export type Resource = {
210
+ /** Complete fallback or scoped token definition. */
211
+ readonly css: string
212
+ /** Stable identity for stylesheet replacement during development. */
213
+ readonly id: string
214
+ }
215
+
216
+ /** Selects independently shareable defaults or all token rules. */
217
+ export const shared = Symbol('shared token rules')
218
+
174
219
  function variable(index: number | string, path: string): string {
220
+ // Packed declarations retain the spelling assigned by their compiler.
221
+ if (typeof index === 'string' && index.startsWith('src-'))
222
+ return `--z-${Identity.label(path)}-${Identity.compact(JSON.stringify([index, path]))}`
175
223
  return `--z-t${encode(String(index))}-${encode(path)}`
176
224
  }