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 +743 -0
- package/dist/builders.cjs.map +1 -1
- package/dist/builders.js.map +1 -1
- package/dist/filters.cjs.map +1 -1
- package/dist/filters.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/renderers.cjs.map +1 -1
- package/dist/renderers.js.map +1 -1
- package/package.json +1 -1
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
|