dispersa 0.2.0 → 0.3.0

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 CHANGED
@@ -118,11 +118,11 @@ for (const output of result.outputs) {
118
118
  }
119
119
  ```
120
120
 
121
- > For file-based tokens, define JSON files and reference them with `$ref` in your resolver document. See the [`basic` example](./examples/basic/) for a complete setup.
121
+ > For file-based tokens, define JSON files and reference them with `$ref` in your resolver document. See the [`typescript-starter` example](./examples/typescript-starter/) for a complete setup.
122
122
 
123
123
  ## Output formats
124
124
 
125
- Dispersa ships four builder functions. Each returns an `OutputConfig` that can be passed to `build()`.
125
+ Dispersa ships five builder functions. Each returns an `OutputConfig` that can be passed to `build()`.
126
126
 
127
127
  ### `css(config)`
128
128
 
@@ -139,6 +139,7 @@ Renders CSS custom properties.
139
139
  | `minify` | `boolean` | `false` | Minify output |
140
140
  | `transforms` | `Transform[]` | -- | Per-output transforms |
141
141
  | `filters` | `Filter[]` | -- | Per-output filters |
142
+ | `hooks` | `LifecycleHooks` | -- | Per-output lifecycle hooks |
142
143
 
143
144
  ### `json(config)`
144
145
 
@@ -154,6 +155,7 @@ Renders JSON output.
154
155
  | `minify` | `boolean` | -- | Minify output |
155
156
  | `transforms` | `Transform[]` | -- | Per-output transforms |
156
157
  | `filters` | `Filter[]` | -- | Per-output filters |
158
+ | `hooks` | `LifecycleHooks` | -- | Per-output lifecycle hooks |
157
159
 
158
160
  ### `js(config)`
159
161
 
@@ -170,6 +172,25 @@ Renders JavaScript/TypeScript modules.
170
172
  | `minify` | `boolean` | -- | Minify output |
171
173
  | `transforms` | `Transform[]` | -- | Per-output transforms |
172
174
  | `filters` | `Filter[]` | -- | Per-output filters |
175
+ | `hooks` | `LifecycleHooks` | -- | Per-output lifecycle hooks |
176
+
177
+ ### `tailwind(config)`
178
+
179
+ Renders Tailwind CSS v4 `@theme` blocks.
180
+
181
+ | Option | Type | Default | Description |
182
+ | --------------- | ------------------------------ | ---------- | -------------------------------------------- |
183
+ | `name` | `string` | -- | Unique output identifier |
184
+ | `file` | `string \| function` | -- | Output path (supports `{modifier}` patterns) |
185
+ | `preset` | `'bundle' \| 'standalone'` | `'bundle'` | Output preset |
186
+ | `includeImport` | `boolean` | -- | Include `@import "tailwindcss"` directive |
187
+ | `namespace` | `string` | -- | Prefix for CSS variable names |
188
+ | `selector` | `string \| SelectorFunction` | `':root'` | CSS selector |
189
+ | `mediaQuery` | `string \| MediaQueryFunction` | -- | Media query wrapper |
190
+ | `minify` | `boolean` | `false` | Minify output |
191
+ | `transforms` | `Transform[]` | -- | Per-output transforms |
192
+ | `filters` | `Filter[]` | -- | Per-output filters |
193
+ | `hooks` | `LifecycleHooks` | -- | Per-output lifecycle hooks |
173
194
 
174
195
  ### Experimental: native platform outputs
175
196
 
@@ -743,15 +764,16 @@ const dispersa = new Dispersa(options?: DispersaOptions)
743
764
 
744
765
  ### Subpath exports
745
766
 
746
- | Export | Description |
747
- | ------------------------ | ---------------------------------------------------------------- |
748
- | `dispersa` | `Dispersa` class, builder functions (`css`, `json`, `js`), types |
749
- | `dispersa/transforms` | Built-in transform factories |
750
- | `dispersa/filters` | Built-in filter factories |
751
- | `dispersa/builders` | Output builder functions |
752
- | `dispersa/renderers` | Renderer types, `defineRenderer`, and `outputTree` helper |
753
- | `dispersa/preprocessors` | Preprocessor type |
754
- | `dispersa/errors` | Error classes (`DispersaError`, `TokenReferenceError`, etc.) |
767
+ | Export | Description |
768
+ | ------------------------ | ---------------------------------------------------------------------------- |
769
+ | `dispersa` | `Dispersa` class, builder functions (`css`, `json`, `js`, `tailwind`), types |
770
+ | `dispersa/transforms` | Built-in transform factories |
771
+ | `dispersa/filters` | Built-in filter factories |
772
+ | `dispersa/builders` | Output builder functions |
773
+ | `dispersa/renderers` | Renderer types, `defineRenderer`, and `outputTree` helper |
774
+ | `dispersa/preprocessors` | Preprocessor type |
775
+ | `dispersa/errors` | Error classes (`DispersaError`, `TokenReferenceError`, etc.) |
776
+ | `dispersa/config` | `defineConfig` helper for CLI config files |
755
777
 
756
778
  Everything outside these entry points is internal and not a stable API contract.
757
779
 
@@ -774,13 +796,17 @@ Resolver -> Preprocessors -> $ref resolution -> Parse/flatten -> Alias resolutio
774
796
 
775
797
  See [`examples/`](./examples/) for complete working projects. Suggested learning path:
776
798
 
777
- | Example | Focus |
778
- | ---------------------------------------------- | --------------------------------------------- |
779
- | [`basic`](./examples/basic/) | Minimal setup with light/dark themes |
780
- | [`no-filesystem`](./examples/no-filesystem/) | In-memory mode with inline tokens |
781
- | [`custom-plugins`](./examples/custom-plugins/) | Custom transforms, filters, and renderers |
782
- | [`advanced`](./examples/advanced/) | Multi-modifier system with all output formats |
783
- | [`enterprise`](./examples/enterprise/) | Multi-brand, multi-platform at scale |
799
+ | Example | Focus |
800
+ | ------------------------------------------------------ | --------------------------------------------------------- |
801
+ | [`typescript-starter`](./examples/typescript-starter/) | Programmatic build script with themed CSS |
802
+ | [`cli-starter`](./examples/cli-starter/) | Config-file workflow using the dispersa CLI |
803
+ | [`in-memory`](./examples/in-memory/) | In-memory mode with inline tokens |
804
+ | [`custom-plugins`](./examples/custom-plugins/) | Custom transforms, filters, and renderers |
805
+ | [`multi-format`](./examples/multi-format/) | Multi-modifier system with all output formats |
806
+ | [`multi-brand`](./examples/multi-brand/) | Multi-brand, multi-platform at scale |
807
+ | [`multi-platform`](./examples/multi-platform/) | CSS, Tailwind, iOS, and Android from one set |
808
+ | [`split-by-type`](./examples/split-by-type/) | Filtered outputs split by token category |
809
+ | [`atlassian-semantic`](./examples/atlassian-semantic/) | Semantic tokens with density, motion, and theme modifiers |
784
810
 
785
811
  ## License
786
812
 
package/dist/builders.cjs CHANGED
@@ -210,32 +210,55 @@ function filterTokensBySource(tokens, expectedSource) {
210
210
  }
211
211
  return filtered;
212
212
  }
213
- function interpolatePattern(pattern, modifierInputs, modifierName, context) {
214
- let result = pattern;
215
- if (modifierName !== void 0) {
216
- result = result.replace(/\{modifierName\}/g, modifierName);
213
+ function filterTokensFromSets(tokens) {
214
+ const filtered = {};
215
+ for (const [name, token] of Object.entries(tokens)) {
216
+ const hasModifierSource = typeof token._sourceModifier === "string" && token._sourceModifier !== "";
217
+ if (!hasModifierSource) {
218
+ filtered[name] = token;
219
+ }
217
220
  }
218
- if (context !== void 0) {
219
- result = result.replace(/\{context\}/g, context);
221
+ return filtered;
222
+ }
223
+ function resolveBaseFileName(fileName, defaults) {
224
+ if (typeof fileName === "function") {
225
+ return fileName({ ...defaults, _base: "true" });
226
+ }
227
+ if (/\{.+?\}/.test(fileName)) {
228
+ const baseInputs = Object.fromEntries(Object.keys(defaults).map((k) => [k, "base"]));
229
+ return collapseBaseSegments(interpolatePattern(fileName, baseInputs));
230
+ }
231
+ const extMatch = fileName.match(/(\.[^.]+)$/);
232
+ const extension = extMatch ? extMatch[1] : "";
233
+ const baseName = extension ? fileName.slice(0, -extension.length) : fileName;
234
+ return `${baseName}-base${extension}`;
235
+ }
236
+ function collapseBaseSegments(value) {
237
+ let result = value;
238
+ let previous = "";
239
+ while (result !== previous) {
240
+ previous = result;
241
+ result = result.replace(/\bbase([/-])base\b/, "base");
220
242
  }
243
+ return result;
244
+ }
245
+ function interpolatePattern(pattern, modifierInputs) {
246
+ let result = pattern;
221
247
  for (const [key, value] of Object.entries(modifierInputs)) {
222
248
  result = result.replaceAll(`{${key}}`, value);
223
249
  }
224
250
  return result;
225
251
  }
226
- function resolveFileName(fileName, modifierInputs, modifierName, context) {
252
+ function resolveFileName(fileName, modifierInputs) {
227
253
  if (typeof fileName === "function") {
228
254
  return fileName(modifierInputs);
229
255
  }
230
256
  if (/\{.+?\}/.test(fileName)) {
231
- return interpolatePattern(fileName, modifierInputs, modifierName, context);
257
+ return interpolatePattern(fileName, modifierInputs);
232
258
  }
233
259
  const extMatch = fileName.match(/(\.[^.]+)$/);
234
260
  const extension = extMatch ? extMatch[1] : "";
235
261
  const baseName = extension ? fileName.slice(0, -extension.length) : fileName;
236
- if (modifierName !== void 0 && context !== void 0) {
237
- return `${baseName}-${modifierName}-${context}${extension}`;
238
- }
239
262
  const modifierSuffix = Object.entries(modifierInputs).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)).map(([key, value]) => `${key}-${value}`).join("-");
240
263
  if (modifierSuffix) {
241
264
  return `${baseName}-${modifierSuffix}${extension}`;
@@ -1350,6 +1373,33 @@ function collectRemainder(tokens, included) {
1350
1373
  }
1351
1374
  return result;
1352
1375
  }
1376
+ function buildSetLayerBlocks(tokens, resolver) {
1377
+ const blocks = [];
1378
+ const included = /* @__PURE__ */ new Set();
1379
+ const addBlock = (key, blockTokens, description) => {
1380
+ if (Object.keys(blockTokens).length === 0) {
1381
+ return;
1382
+ }
1383
+ for (const k of Object.keys(blockTokens)) {
1384
+ included.add(k);
1385
+ }
1386
+ blocks.push({ key, description, tokens: blockTokens });
1387
+ };
1388
+ for (const item of resolver.resolutionOrder) {
1389
+ const ref = item.$ref;
1390
+ if (typeof ref !== "string" || !ref.startsWith("#/sets/")) {
1391
+ continue;
1392
+ }
1393
+ const setName = ref.slice("#/sets/".length);
1394
+ addBlock(
1395
+ `Set: ${setName}`,
1396
+ collectSetTokens(tokens, setName, included),
1397
+ resolver.sets?.[setName]?.description
1398
+ );
1399
+ }
1400
+ addBlock("Unattributed", collectRemainder(tokens, included));
1401
+ return blocks;
1402
+ }
1353
1403
  function buildDefaultLayerBlocks(tokens, baseModifierInputs, resolver) {
1354
1404
  const blocks = [];
1355
1405
  const included = /* @__PURE__ */ new Set();
@@ -2070,6 +2120,10 @@ var CssRenderer = class _CssRenderer {
2070
2120
  throw new ConfigurationError("Modifier preset requires modifiers to be defined in resolver");
2071
2121
  }
2072
2122
  const files = {};
2123
+ const baseResult = await this.buildModifierBaseFile(context, options);
2124
+ if (baseResult) {
2125
+ files[baseResult.fileName] = baseResult.content;
2126
+ }
2073
2127
  for (const [modifierName, modifierDef] of Object.entries(context.resolver.modifiers)) {
2074
2128
  for (const contextValue of Object.keys(modifierDef.contexts)) {
2075
2129
  const result = await this.buildModifierContextFile(
@@ -2085,6 +2139,59 @@ var CssRenderer = class _CssRenderer {
2085
2139
  }
2086
2140
  return { kind: "outputTree", files };
2087
2141
  }
2142
+ async buildModifierBaseFile(context, options) {
2143
+ const basePermutation = context.permutations.find(
2144
+ ({ modifierInputs }) => this.isBasePermutation(modifierInputs, context.meta.defaults)
2145
+ );
2146
+ if (!basePermutation) {
2147
+ return void 0;
2148
+ }
2149
+ const setTokens = filterTokensFromSets(basePermutation.tokens);
2150
+ if (Object.keys(setTokens).length === 0) {
2151
+ return void 0;
2152
+ }
2153
+ const setBlocks = buildSetLayerBlocks(setTokens, context.resolver);
2154
+ if (setBlocks.length === 0) {
2155
+ return void 0;
2156
+ }
2157
+ const modifiers = context.resolver.modifiers;
2158
+ const firstModifierName = Object.keys(modifiers)[0] ?? "";
2159
+ const firstModifierContext = context.meta.defaults[firstModifierName] ?? "";
2160
+ const baseModifierInputs = { ...context.meta.defaults };
2161
+ const selector = resolveSelector(
2162
+ options.selector,
2163
+ firstModifierName,
2164
+ firstModifierContext,
2165
+ true,
2166
+ baseModifierInputs
2167
+ );
2168
+ const mediaQuery = resolveMediaQuery(
2169
+ options.mediaQuery,
2170
+ firstModifierName,
2171
+ firstModifierContext,
2172
+ true,
2173
+ baseModifierInputs
2174
+ );
2175
+ const referenceTokens = basePermutation.tokens;
2176
+ const cssBlocks = [];
2177
+ for (const block of setBlocks) {
2178
+ const cleanTokens = stripInternalMetadata(block.tokens);
2179
+ const css2 = await this.formatTokens(cleanTokens, {
2180
+ selector,
2181
+ mediaQuery,
2182
+ minify: options.minify ?? false,
2183
+ preserveReferences: options.preserveReferences ?? false,
2184
+ referenceTokens
2185
+ });
2186
+ const header = block.description ? `/* ${block.key} */
2187
+ /* ${block.description} */` : `/* ${block.key} */`;
2188
+ cssBlocks.push(`${header}
2189
+ ${css2}`);
2190
+ }
2191
+ const content = cssBlocks.join("\n");
2192
+ const fileName = context.output.file ? resolveBaseFileName(context.output.file, context.meta.defaults) : `${context.output.name}-base.css`;
2193
+ return { fileName, content };
2194
+ }
2088
2195
  collectTokensForModifierContext(modifierName, contextValue, permutations) {
2089
2196
  const expectedSource = `${modifierName}-${contextValue}`;
2090
2197
  let tokensFromSource = {};
@@ -2131,7 +2238,7 @@ var CssRenderer = class _CssRenderer {
2131
2238
  preserveReferences: options.preserveReferences ?? false,
2132
2239
  referenceTokens
2133
2240
  });
2134
- const fileName = context.output.file ? resolveFileName(context.output.file, modifierInputs, modifierName, contextValue) : buildInMemoryOutputKey({
2241
+ const fileName = context.output.file ? resolveFileName(context.output.file, modifierInputs) : buildInMemoryOutputKey({
2135
2242
  outputName: context.output.name,
2136
2243
  extension: "css",
2137
2244
  modifierInputs,