dispersa 0.1.1 → 0.1.2

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 ADDED
@@ -0,0 +1,743 @@
1
+ # Dispersa
2
+
3
+ A TypeScript build system for processing [DTCG 2025.10](https://www.designtokens.org/) design tokens. Dispersa loads resolver documents, resolves references and modifiers, applies filters and transforms, then renders output to CSS, JSON, and JS/TS modules.
4
+
5
+ ## Features
6
+
7
+ - **DTCG 2025.10 compliant** -- full support for the resolver and token format specifications
8
+ - **Multiple outputs** -- CSS custom properties, JSON, JS/TS modules
9
+ - **Extensible pipeline** -- custom preprocessors, filters, transforms, and renderers
10
+ - **Schema validation** -- AJV runtime validation with schema-generated TypeScript types
11
+ - **In-memory mode** -- use without the filesystem for build tools, APIs, and testing
12
+ - **CLI** -- config-first workflow with auto-discovery
13
+
14
+ ## Token types
15
+
16
+ **Standard DTCG types:** `color`, `dimension`, `fontFamily`, `fontWeight`, `duration`, `cubicBezier`, `number`
17
+
18
+ **Composite types:** `shadow`, `typography`, `border`, `strokeStyle`, `transition`, `gradient`
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pnpm add dispersa
24
+ ```
25
+
26
+ ## Quick start
27
+
28
+ Define a DTCG resolver document (`tokens.resolver.json`):
29
+
30
+ ```json
31
+ {
32
+ "version": "2025.10",
33
+ "sets": {
34
+ "core": {
35
+ "sources": [{ "$ref": "./tokens/base.json" }, { "$ref": "./tokens/alias.json" }]
36
+ }
37
+ },
38
+ "modifiers": {
39
+ "theme": {
40
+ "default": "light",
41
+ "contexts": {
42
+ "light": [{ "$ref": "./tokens/themes/light.json" }],
43
+ "dark": [{ "$ref": "./tokens/themes/dark.json" }]
44
+ }
45
+ }
46
+ },
47
+ "resolutionOrder": [{ "$ref": "#/sets/core" }, { "$ref": "#/modifiers/theme" }]
48
+ }
49
+ ```
50
+
51
+ Build tokens:
52
+
53
+ ```typescript
54
+ import { Dispersa, css, json } from 'dispersa'
55
+ import { colorToHex, nameKebabCase } from 'dispersa/transforms'
56
+
57
+ const dispersa = new Dispersa({
58
+ resolver: './tokens.resolver.json',
59
+ buildPath: './dist',
60
+ })
61
+
62
+ const result = await dispersa.build({
63
+ outputs: [
64
+ css({
65
+ name: 'css',
66
+ file: 'tokens.css',
67
+ preset: 'bundle',
68
+ selector: ':root',
69
+ transforms: [nameKebabCase(), colorToHex()],
70
+ }),
71
+ json({
72
+ name: 'json',
73
+ file: 'tokens-{theme}.json',
74
+ preset: 'standalone',
75
+ structure: 'flat',
76
+ }),
77
+ ],
78
+ })
79
+
80
+ if (result.success) {
81
+ console.log(`Generated ${result.outputs.length} file(s)`)
82
+ }
83
+ ```
84
+
85
+ ## Output formats
86
+
87
+ Dispersa ships four builder functions. Each returns an `OutputConfig` that can be passed to `build()`.
88
+
89
+ ### `css(config)`
90
+
91
+ Renders CSS custom properties.
92
+
93
+ | Option | Type | Default | Description |
94
+ | -------------------- | ---------------------------------------- | ---------- | -------------------------------------------- |
95
+ | `name` | `string` | -- | Unique output identifier |
96
+ | `file` | `string \| function` | -- | Output path (supports `{modifier}` patterns) |
97
+ | `preset` | `'bundle' \| 'standalone' \| 'modifier'` | `'bundle'` | Output preset |
98
+ | `selector` | `string \| SelectorFunction` | `':root'` | CSS selector |
99
+ | `mediaQuery` | `string \| MediaQueryFunction` | -- | Media query wrapper |
100
+ | `preserveReferences` | `boolean` | `false` | Emit `var()` references for aliases |
101
+ | `minify` | `boolean` | `false` | Minify output |
102
+ | `transforms` | `Transform[]` | -- | Per-output transforms |
103
+ | `filters` | `Filter[]` | -- | Per-output filters |
104
+
105
+ ### `json(config)`
106
+
107
+ Renders JSON output.
108
+
109
+ | Option | Type | Default | Description |
110
+ | ----------------- | -------------------------- | -------------- | -------------------------------------------- |
111
+ | `name` | `string` | -- | Unique output identifier |
112
+ | `file` | `string \| function` | -- | Output path (supports `{modifier}` patterns) |
113
+ | `preset` | `'bundle' \| 'standalone'` | `'standalone'` | Output preset |
114
+ | `structure` | `'flat' \| 'nested'` | -- | Token structure in output |
115
+ | `includeMetadata` | `boolean` | -- | Include DTCG metadata fields |
116
+ | `minify` | `boolean` | -- | Minify output |
117
+ | `transforms` | `Transform[]` | -- | Per-output transforms |
118
+ | `filters` | `Filter[]` | -- | Per-output filters |
119
+
120
+ ### `js(config)`
121
+
122
+ Renders JavaScript/TypeScript modules.
123
+
124
+ | Option | Type | Default | Description |
125
+ | ---------------- | -------------------------- | -------------- | -------------------------------------------- |
126
+ | `name` | `string` | -- | Unique output identifier |
127
+ | `file` | `string \| function` | -- | Output path (supports `{modifier}` patterns) |
128
+ | `preset` | `'bundle' \| 'standalone'` | `'standalone'` | Output preset |
129
+ | `structure` | `'flat' \| 'nested'` | -- | Token structure in output |
130
+ | `moduleName` | `string` | -- | Module name for exports |
131
+ | `generateHelper` | `boolean` | -- | Generate token lookup helper (bundle mode) |
132
+ | `minify` | `boolean` | -- | Minify output |
133
+ | `transforms` | `Transform[]` | -- | Per-output transforms |
134
+ | `filters` | `Filter[]` | -- | Per-output filters |
135
+
136
+ ## Output presets
137
+
138
+ Presets control how modifier permutations are packaged into files.
139
+
140
+ **`standalone`** -- each permutation produces its own complete file. Use pattern-based filenames to distinguish them:
141
+
142
+ ```typescript
143
+ css({
144
+ name: 'css',
145
+ file: 'tokens-{theme}.css',
146
+ preset: 'standalone',
147
+ selector: ':root',
148
+ })
149
+ // -> tokens-light.css, tokens-dark.css (each with all tokens)
150
+ ```
151
+
152
+ **`bundle`** -- all permutations are bundled into a single file with format-specific grouping (CSS selectors, JSON keys, JS named exports):
153
+
154
+ ```typescript
155
+ css({
156
+ name: 'css',
157
+ file: 'tokens.css',
158
+ preset: 'bundle',
159
+ selector: ':root',
160
+ })
161
+ // -> tokens.css with :root { ... } and [data-theme="dark"] { ... }
162
+ ```
163
+
164
+ **`modifier`** -- CSS-only preset that emits only the tokens that differ per modifier context, not the full set:
165
+
166
+ ```typescript
167
+ css({
168
+ name: 'css',
169
+ file: 'tokens.css',
170
+ preset: 'modifier',
171
+ selector: (modifierName, context, isBase) => {
172
+ if (isBase) return ':root'
173
+ return `[data-${modifierName}="${context}"]`
174
+ },
175
+ })
176
+ ```
177
+
178
+ ## Built-in transforms
179
+
180
+ Import from `dispersa/transforms`. All transforms are factory functions that return a `Transform` object.
181
+
182
+ ### Color
183
+
184
+ | Factory | Output |
185
+ | ------------------------ | ----------------------- |
186
+ | `colorToHex()` | `#rrggbb` / `#rrggbbaa` |
187
+ | `colorToRgb()` | `rgb()` / `rgba()` |
188
+ | `colorToHsl()` | `hsl()` / `hsla()` |
189
+ | `colorToOklch()` | `oklch()` |
190
+ | `colorToOklab()` | `oklab()` |
191
+ | `colorToLch()` | `lch()` |
192
+ | `colorToLab()` | `lab()` |
193
+ | `colorToHwb()` | `hwb()` |
194
+ | `colorToColorFunction()` | CSS `color()` function |
195
+
196
+ ### Dimension
197
+
198
+ | Factory | Output |
199
+ | ----------------------- | -------------- |
200
+ | `dimensionToPx()` | `"16px"` |
201
+ | `dimensionToRem()` | `"1rem"` |
202
+ | `dimensionToUnitless()` | `16` (numeric) |
203
+
204
+ ### Name
205
+
206
+ | Factory | Output |
207
+ | -------------------- | --------------------------- |
208
+ | `nameKebabCase()` | `color-brand-primary` |
209
+ | `nameCamelCase()` | `colorBrandPrimary` |
210
+ | `nameSnakeCase()` | `color_brand_primary` |
211
+ | `namePascalCase()` | `ColorBrandPrimary` |
212
+ | `nameConstantCase()` | `COLOR_BRAND_PRIMARY` |
213
+ | `nameCssVar()` | `--color-brand-primary` |
214
+ | `namePrefix(prefix)` | `ds-color-brand-primary` |
215
+ | `nameSuffix(suffix)` | `color-brand-primary-token` |
216
+
217
+ ### Other
218
+
219
+ | Factory | Output |
220
+ | ---------------------- | ------------------ |
221
+ | `fontWeightToNumber()` | `400`, `700`, etc. |
222
+ | `durationToMs()` | `"200ms"` |
223
+ | `durationToSeconds()` | `"0.2s"` |
224
+
225
+ ## Built-in filters
226
+
227
+ Import from `dispersa/filters`. All filters are factory functions that return a `Filter` object.
228
+
229
+ | Factory | Description |
230
+ | ----------------- | ----------------------------------------------------------- |
231
+ | `byType(type)` | Include tokens matching the given `$type` |
232
+ | `byPath(pattern)` | Include tokens whose path matches a string or `RegExp` |
233
+ | `isAlias()` | Include only alias tokens (tokens referencing other tokens) |
234
+ | `isBase()` | Include only base tokens (tokens with direct values) |
235
+
236
+ ```typescript
237
+ import { byType, isAlias } from 'dispersa/filters'
238
+
239
+ css({
240
+ name: 'colors-only',
241
+ file: 'colors.css',
242
+ preset: 'bundle',
243
+ filters: [byType('color')],
244
+ transforms: [nameKebabCase(), colorToHex()],
245
+ })
246
+
247
+ css({
248
+ name: 'semantic-only',
249
+ file: 'semantic.css',
250
+ preset: 'modifier',
251
+ filters: [isAlias()],
252
+ transforms: [nameKebabCase(), colorToHex()],
253
+ })
254
+ ```
255
+
256
+ ## Extending the pipeline
257
+
258
+ ### Custom transforms
259
+
260
+ A `Transform` has an optional `matcher` (to scope which tokens it applies to) and a `transform` function:
261
+
262
+ ```typescript
263
+ import type { Transform } from 'dispersa'
264
+
265
+ const addPrefix: Transform = {
266
+ matcher: (token) => token.$type === 'color',
267
+ transform: (token) => ({
268
+ ...token,
269
+ name: `brand-${token.name}`,
270
+ }),
271
+ }
272
+ ```
273
+
274
+ ### Custom filters
275
+
276
+ A `Filter` has a single `filter` function that returns `true` to keep a token:
277
+
278
+ ```typescript
279
+ import type { Filter } from 'dispersa'
280
+
281
+ const excludeDeprecated: Filter = {
282
+ filter: (token) => !token.$deprecated,
283
+ }
284
+ ```
285
+
286
+ ### Custom preprocessors
287
+
288
+ A `Preprocessor` transforms raw token objects before parsing:
289
+
290
+ ```typescript
291
+ import type { Preprocessor } from 'dispersa'
292
+
293
+ const stripMetadata: Preprocessor = {
294
+ name: 'strip-metadata',
295
+ preprocess: (rawTokens) => {
296
+ const { _metadata, ...tokens } = rawTokens
297
+ return tokens
298
+ },
299
+ }
300
+
301
+ await dispersa.build({
302
+ preprocessors: [stripMetadata],
303
+ outputs: [
304
+ /* ... */
305
+ ],
306
+ })
307
+ ```
308
+
309
+ ### Custom renderers
310
+
311
+ Use `defineRenderer<T>()` to create type-safe custom renderers. The generic parameter gives you autocomplete and type-checking on both `context` and `options` inside `format()`:
312
+
313
+ ```typescript
314
+ import { defineRenderer, outputTree } from 'dispersa'
315
+ import type { RenderContext } from 'dispersa'
316
+
317
+ // 1. Define your renderer-specific options
318
+ type SwiftUIOptions = {
319
+ structName?: string
320
+ accessLevel?: 'public' | 'internal'
321
+ }
322
+
323
+ // 2. Create the renderer with defineRenderer<T>()
324
+ const swiftUIRenderer = defineRenderer<SwiftUIOptions>({
325
+ format(context, options) {
326
+ const structName = options?.structName ?? 'DesignTokens'
327
+ const access = options?.accessLevel ?? 'public'
328
+ const tokens = context.permutations[0]?.tokens ?? {}
329
+
330
+ const props = Object.entries(tokens)
331
+ .map(([name, token]) => ` ${access} static let ${name} = ${JSON.stringify(token.$value)}`)
332
+ .join('\n')
333
+
334
+ return `import SwiftUI\n\n${access} struct ${structName} {\n${props}\n}\n`
335
+ },
336
+ })
337
+
338
+ // 3. Use it in your build config
339
+ await dispersa.build({
340
+ outputs: [
341
+ {
342
+ name: 'swift',
343
+ renderer: swiftUIRenderer,
344
+ file: 'DesignTokens.swift',
345
+ options: { structName: 'AppTokens', accessLevel: 'public' },
346
+ transforms: [nameCamelCase()],
347
+ },
348
+ ],
349
+ })
350
+ ```
351
+
352
+ #### RenderContext
353
+
354
+ Every renderer receives a `RenderContext` with these fields:
355
+
356
+ | Field | Type | Description |
357
+ | -------------- | ------------------------------ | --------------------------------------------------------------------------------------------- |
358
+ | `permutations` | `{ tokens, modifierInputs }[]` | Resolved tokens for each permutation (theme/platform combo) |
359
+ | `output` | `OutputConfig` | The current output configuration (name, file, options, transforms, filters) |
360
+ | `resolver` | `ResolverDocument` | The resolved DTCG resolver document |
361
+ | `meta` | `RenderMeta` | Modifier metadata: `dimensions` (e.g. `['theme', 'platform']`), `defaults`, `basePermutation` |
362
+ | `buildPath` | `string \| undefined` | Output directory (undefined in in-memory mode) |
363
+
364
+ #### Multi-file output with outputTree
365
+
366
+ When your renderer needs to produce multiple files, return an `OutputTree` instead of a string:
367
+
368
+ ```typescript
369
+ import { defineRenderer, outputTree } from 'dispersa'
370
+
371
+ const multiFileRenderer = defineRenderer({
372
+ format(context) {
373
+ const files: Record<string, string> = {}
374
+
375
+ for (const { tokens, modifierInputs } of context.permutations) {
376
+ const content = Object.entries(tokens)
377
+ .map(([name, token]) => `${name}: ${JSON.stringify(token.$value)}`)
378
+ .join('\n')
379
+
380
+ const key = Object.values(modifierInputs).join('-') || 'default'
381
+ files[`tokens-${key}.yaml`] = content
382
+ }
383
+
384
+ return outputTree(files)
385
+ },
386
+ })
387
+ ```
388
+
389
+ #### Presets: bundle, standalone, modifier
390
+
391
+ The built-in renderers support three presets that control how permutations are handled:
392
+
393
+ | Preset | Behavior | Use case |
394
+ | ------------ | ----------------------------------------------------------------------------- | ------------------------ |
395
+ | `bundle` | All permutations in one file (e.g. CSS cascade with `:root` + `[data-theme]`) | Single-file delivery |
396
+ | `standalone` | One file per permutation (e.g. `tokens-light.css`, `tokens-dark.css`) | Platform-specific builds |
397
+ | `modifier` | Only the diff between a permutation and the base | Overlay/patch files |
398
+
399
+ Custom renderers can use `context.meta.basePermutation` to determine which permutation is the base.
400
+
401
+ #### Composing transforms and filters with renderers
402
+
403
+ Each `OutputConfig` (returned by builders like `css()` or constructed manually) bundles transforms, filters, and a renderer together. Global transforms/filters from `BuildConfig` are applied first, then per-output transforms/filters:
404
+
405
+ ```typescript
406
+ await dispersa.build({
407
+ // Global: applied to ALL outputs
408
+ transforms: [nameKebabCase()],
409
+ filters: [byType('color')],
410
+
411
+ outputs: [
412
+ css({
413
+ name: 'css',
414
+ preset: 'bundle',
415
+ // Per-output: applied AFTER global transforms
416
+ transforms: [colorToHex()],
417
+ }),
418
+ {
419
+ name: 'swift',
420
+ renderer: swiftUIRenderer,
421
+ // Per-output: applied AFTER global transforms
422
+ transforms: [nameCamelCase()],
423
+ },
424
+ ],
425
+ })
426
+ ```
427
+
428
+ ## Dynamic selectors and media queries
429
+
430
+ The CSS builder accepts functions for `selector` and `mediaQuery`, giving full control over how rules are generated per modifier context:
431
+
432
+ ```typescript
433
+ css({
434
+ name: 'css',
435
+ file: 'tokens.css',
436
+ preset: 'bundle',
437
+ selector: (modifierName, context, isBase, allInputs) => {
438
+ if (isBase) return ':root'
439
+ return `[data-${modifierName}="${context}"]`
440
+ },
441
+ mediaQuery: (modifierName, context, isBase) => {
442
+ if (modifierName === 'platform' && context === 'mobile') {
443
+ return '(max-width: 768px)'
444
+ }
445
+ return ''
446
+ },
447
+ })
448
+ ```
449
+
450
+ The function signature for both is:
451
+
452
+ ```typescript
453
+ ;(
454
+ modifierName: string,
455
+ context: string,
456
+ isBase: boolean,
457
+ allModifierInputs: Record<string, string>,
458
+ ) => string
459
+ ```
460
+
461
+ ## Token references
462
+
463
+ Dispersa supports two reference mechanisms:
464
+
465
+ **Aliases** (`{token.name}`) reference another token's value within `$value`:
466
+
467
+ ```json
468
+ {
469
+ "color": {
470
+ "primary": {
471
+ "$type": "color",
472
+ "$value": { "colorSpace": "srgb", "components": [0, 0.4, 0.8] }
473
+ },
474
+ "action": {
475
+ "$type": "color",
476
+ "$value": "{color.primary}"
477
+ }
478
+ }
479
+ }
480
+ ```
481
+
482
+ **JSON Pointer `$ref`** references files, resolver sets, or property-level values:
483
+
484
+ ```json
485
+ {
486
+ "colors": {
487
+ "blue": {
488
+ "$type": "color",
489
+ "$value": { "colorSpace": "srgb", "components": [0.2, 0.4, 0.9] }
490
+ },
491
+ "primary": {
492
+ "$type": "color",
493
+ "$ref": "#/colors/blue/$value"
494
+ }
495
+ }
496
+ }
497
+ ```
498
+
499
+ Token-level `$ref` preserves the token shape and resolves into `$value`. When `$type` is missing on an alias or `$ref` token, it is inferred from the referenced token.
500
+
501
+ ## In-memory mode
502
+
503
+ Dispersa can run entirely without the filesystem. Pass a `ResolverDocument` object directly and omit `buildPath` to get output content in memory:
504
+
505
+ ```typescript
506
+ import type { ResolverDocument } from 'dispersa'
507
+ import { Dispersa, css } from 'dispersa'
508
+ import { colorToHex, nameKebabCase } from 'dispersa/transforms'
509
+
510
+ const resolver: ResolverDocument = {
511
+ version: '2025.10',
512
+ sets: {
513
+ base: {
514
+ sources: [
515
+ {
516
+ color: {
517
+ primary: {
518
+ $type: 'color',
519
+ $value: { colorSpace: 'srgb', components: [0, 0.4, 0.8] },
520
+ },
521
+ },
522
+ },
523
+ ],
524
+ },
525
+ },
526
+ resolutionOrder: [{ $ref: '#/sets/base' }],
527
+ }
528
+
529
+ const dispersa = new Dispersa({ resolver })
530
+
531
+ const result = await dispersa.build({
532
+ outputs: [
533
+ css({
534
+ name: 'css',
535
+ preset: 'bundle',
536
+ selector: ':root',
537
+ transforms: [nameKebabCase(), colorToHex()],
538
+ }),
539
+ ],
540
+ })
541
+
542
+ // Access generated content directly
543
+ for (const output of result.outputs) {
544
+ console.log(output.content)
545
+ }
546
+ ```
547
+
548
+ ## Error handling
549
+
550
+ - **`build()`** returns a `BuildResult` object. It never throws.
551
+ - **`buildOrThrow()`** is the fail-fast variant that throws on invalid config, resolver errors, or build failures.
552
+
553
+ ```typescript
554
+ type BuildResult = {
555
+ success: boolean
556
+ outputs: { name: string; path?: string; content: string }[]
557
+ errors?: BuildError[]
558
+ }
559
+
560
+ type BuildError = {
561
+ message: string
562
+ code: ErrorCode
563
+ path?: string // file path (for FILE_OPERATION errors)
564
+ tokenPath?: string // token path (for TOKEN_REFERENCE, CIRCULAR_REFERENCE errors)
565
+ severity: 'error' | 'warning'
566
+ suggestions?: string[] // e.g. similar token names for TOKEN_REFERENCE errors
567
+ }
568
+ ```
569
+
570
+ `ErrorCode` is a union of all failure types:
571
+
572
+ | Code | Description |
573
+ | -------------------- | ------------------------------------------- |
574
+ | `TOKEN_REFERENCE` | Unresolved alias reference (`{token.name}`) |
575
+ | `CIRCULAR_REFERENCE` | Circular alias chain detected |
576
+ | `VALIDATION` | Schema or structural validation failure |
577
+ | `COLOR_PARSE` | Invalid color value |
578
+ | `DIMENSION_FORMAT` | Invalid dimension value |
579
+ | `FILE_OPERATION` | File read/write failure |
580
+ | `CONFIGURATION` | Invalid build or renderer configuration |
581
+ | `BASE_PERMUTATION` | Missing base permutation for bundle mode |
582
+ | `MODIFIER` | Invalid modifier input or context |
583
+ | `UNKNOWN` | Catch-all for unexpected errors |
584
+
585
+ ## Lifecycle hooks
586
+
587
+ Both `BuildConfig.hooks` (global) and `OutputConfig.hooks` (per-output) accept the same `LifecycleHooks` type. Global hooks fire once per build; per-output hooks fire in the context of each output.
588
+
589
+ ```typescript
590
+ await dispersa.build({
591
+ outputs: [
592
+ css({
593
+ name: 'css',
594
+ preset: 'bundle',
595
+ hooks: {
596
+ onBuildStart: ({ config }) => {
597
+ console.log(`[css] starting...`)
598
+ },
599
+ onBuildEnd: (result) => {
600
+ console.log(`[css] ${result.success ? 'done' : 'failed'}`)
601
+ },
602
+ },
603
+ }),
604
+ ],
605
+ hooks: {
606
+ onBuildStart: ({ config }) => {
607
+ console.log(`Building ${config.outputs.length} output(s)...`)
608
+ },
609
+ onBuildEnd: (result) => {
610
+ if (result.success) {
611
+ console.log(`Build succeeded: ${result.outputs.length} file(s)`)
612
+ } else {
613
+ console.error(`Build failed: ${result.errors?.length} error(s)`)
614
+ }
615
+ },
616
+ },
617
+ })
618
+ ```
619
+
620
+ **Execution order:**
621
+
622
+ | # | Hook | Scope | When it fires |
623
+ | --- | -------------- | ---------- | ----------------------------------------------- |
624
+ | 1 | `onBuildStart` | Global | Before permutation resolution |
625
+ | 2 | `onBuildStart` | Per-output | Before each output is processed |
626
+ | 3 | `onBuildEnd` | Per-output | After each output finishes (success or failure) |
627
+ | 4 | `onBuildEnd` | Global | After all outputs complete (success or failure) |
628
+
629
+ All hooks support both sync and async functions.
630
+
631
+ ## CLI
632
+
633
+ Dispersa ships a CLI package (`dispersa-cli`) with a config-first workflow.
634
+
635
+ ```bash
636
+ pnpm add dispersa-cli
637
+ ```
638
+
639
+ ```bash
640
+ dispersa build
641
+ dispersa build --config ./dispersa.config.ts
642
+ ```
643
+
644
+ The CLI auto-discovers config files named `dispersa.config.(ts|js|mts|mjs|cts|cjs)`. Use `defineConfig` for type safety:
645
+
646
+ ```typescript
647
+ // dispersa.config.ts
648
+ import { defineConfig } from 'dispersa-cli'
649
+ import { css, json } from 'dispersa'
650
+ import { colorToHex, nameKebabCase } from 'dispersa/transforms'
651
+
652
+ export default defineConfig({
653
+ resolver: './tokens.resolver.json',
654
+ buildPath: './dist',
655
+ outputs: [
656
+ css({
657
+ name: 'css',
658
+ file: 'tokens.css',
659
+ preset: 'bundle',
660
+ selector: ':root',
661
+ transforms: [nameKebabCase(), colorToHex()],
662
+ }),
663
+ json({
664
+ name: 'json',
665
+ file: 'tokens-{theme}.json',
666
+ preset: 'standalone',
667
+ structure: 'flat',
668
+ }),
669
+ ],
670
+ })
671
+ ```
672
+
673
+ ## API reference
674
+
675
+ ### `Dispersa` class
676
+
677
+ ```typescript
678
+ const dispersa = new Dispersa(options?: DispersaOptions)
679
+ ```
680
+
681
+ **Constructor options:**
682
+
683
+ | Option | Type | Description |
684
+ | ------------ | --------------------------------------- | ------------------------------------------------------ |
685
+ | `resolver` | `string \| ResolverDocument` | Default resolver (file path or inline object) |
686
+ | `buildPath` | `string` | Default output directory |
687
+ | `validation` | `{ mode?: 'error' \| 'warn' \| 'off' }` | Validation behavior (`'warn'` logs via `console.warn`) |
688
+
689
+ **Methods:**
690
+
691
+ | Method | Description |
692
+ | ------------------------------------------- | ----------------------------------------------------- |
693
+ | `build(config)` | Build tokens. Returns `BuildResult` (never throws). |
694
+ | `buildOrThrow(config)` | Build tokens. Throws on failure. |
695
+ | `buildPermutation(config, modifierInputs?)` | Build a single permutation. |
696
+ | `resolveTokens(resolver, modifierInputs?)` | Resolve tokens for one permutation without rendering. |
697
+ | `resolveAllPermutations(resolver)` | Resolve tokens for every permutation. |
698
+ | `generateTypes(tokens, fileName, options?)` | Generate a `.d.ts` file from resolved tokens. |
699
+
700
+ ### Subpath exports
701
+
702
+ | Export | Description |
703
+ | ------------------------ | ---------------------------------------------------------------- |
704
+ | `dispersa` | `Dispersa` class, builder functions (`css`, `json`, `js`), types |
705
+ | `dispersa/transforms` | Built-in transform factories |
706
+ | `dispersa/filters` | Built-in filter factories |
707
+ | `dispersa/builders` | Output builder functions |
708
+ | `dispersa/renderers` | Renderer types, `defineRenderer`, and `outputTree` helper |
709
+ | `dispersa/preprocessors` | Preprocessor type |
710
+ | `dispersa/errors` | Error classes (`DispersaError`, `TokenReferenceError`, etc.) |
711
+
712
+ Everything outside these entry points is internal and not a stable API contract.
713
+
714
+ ## Pipeline overview
715
+
716
+ ```
717
+ Resolver -> Preprocessors -> $ref resolution -> Parse/flatten -> Alias resolution -> Filters -> Transforms -> Renderers
718
+ ```
719
+
720
+ 1. **Resolver** -- loads sets and applies modifier contexts per the DTCG resolver spec
721
+ 2. **Preprocessors** -- transform raw token objects before parsing
722
+ 3. **$ref resolution** -- resolves JSON Pointer references within token documents
723
+ 4. **Parse/flatten** -- resolves group extensions, validates names, flattens to dot-path keys
724
+ 5. **Alias resolution** -- resolves `{token.name}` references with cycle detection
725
+ 6. **Filters** -- removes tokens (global filters first, then per-output)
726
+ 7. **Transforms** -- mutates token values and names (global first, then per-output)
727
+ 8. **Renderers** -- formats tokens into the target output (CSS, JSON, JS, or custom)
728
+
729
+ ## Examples
730
+
731
+ See [`examples/`](./examples/) for complete working projects. Suggested learning path:
732
+
733
+ | Example | Focus |
734
+ | ---------------------------------------------- | --------------------------------------------- |
735
+ | [`basic`](./examples/basic/) | Minimal setup with light/dark themes |
736
+ | [`no-filesystem`](./examples/no-filesystem/) | In-memory mode with inline tokens |
737
+ | [`custom-plugins`](./examples/custom-plugins/) | Custom transforms, filters, and renderers |
738
+ | [`advanced`](./examples/advanced/) | Multi-modifier system with all output formats |
739
+ | [`enterprise`](./examples/enterprise/) | Multi-brand, multi-platform at scale |
740
+
741
+ ## License
742
+
743
+ MIT