Skip to content
5 changes: 5 additions & 0 deletions .changeset/style-dictionary-token-output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hebilicious/cssforge": minor
---

Add Style Dictionary token JSON output, with resolved values by default and an optional CSS-variable mode for usage matching.
103 changes: 100 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1027,19 +1027,116 @@ pnpm run cssforge
# Watch mode
pnpm run cssforge -- --watch

# Custom paths and output
pnpm run cssforge -- --config ./foo/bar/custom-path.ts --css ./dist/design-tokens.css --ts ./dist/design-tokens.ts --json ./dist/design-tokens.json --mode all
# Custom paths and output
pnpm run cssforge -- --config ./foo/bar/custom-path.ts --css ./dist/design-tokens.css --ts ./dist/design-tokens.ts --json ./dist/design-tokens.json --style-dictionary ./dist/design-tokens.sd.json --mode all

# Style Dictionary JSON with final values (default)
pnpm run cssforge -- --mode style-dictionary --style-dictionary ./dist/design-tokens.sd.json

# Keep CSS variables as values for usage matching
pnpm run cssforge -- --mode style-dictionary --style-dictionary ./dist/design-tokens.sd.json --style-dictionary-value-mode css-reference
```

## Programmatic Usage

You can also use CSS Forge programmatically:

```typescript
import { generateCSS } from "jsr:@hebilicious/cssforge";
import { generateCSS, generateStyleDictionaryJSON } from "jsr:@hebilicious/cssforge";

// Generate CSS string
const css = generateCSS(config);

// Write final values for Style Dictionary
const resolvedTokens = generateStyleDictionaryJSON(config);

// Keep var(--token) as each token's value for usage matching
const usageTokens = generateStyleDictionaryJSON(config, { valueMode: "css-reference" });
```

## Style Dictionary JSON

CSS Forge can generate a separate token file for Style Dictionary and other tools that read
the same JSON shape. This output does not change the CSS, TypeScript, or regular JSON files
you already generate.

### Generate the file

Use `style-dictionary` mode to generate only the token file:

```bash
pnpm run cssforge -- --mode style-dictionary --style-dictionary ./.cssforge/tokens.json
```

Use `--mode all` to generate it together with the CSS, TypeScript, and regular JSON outputs.
The `--style-dictionary` option controls where the token file is written.

A generated token looks like this:

```json
{
"palette": {
"neutral": {
"900": {
"value": "oklch(17.764% 0 0)",
"type": "color",
"$tier": "primitive",
"$resolvedValue": "oklch(17.764% 0 0)",
"attributes": {
"cssVariable": "--palette-neutral-900",
"cssVariableReference": "var(--palette-neutral-900)",
"resolvedValue": "oklch(17.764% 0 0)",
"sourcePath": "palette.neutral.900"
}
}
}
}
}
```

Semantic tokens also include `$reference` and `attributes.referencePaths`. These paths match
the keys in the generated file, so consumers can connect a semantic token to its source.

### Choose the value mode

| Mode | `value` contains | Use it for |
| --- | --- | --- |
| `resolved` (default) | The final value, such as `oklch(...)`, `1rem`, or `clamp(...)` | Style Dictionary transforms and token previews |
| `css-reference` | The token's own CSS variable, such as `var(--palette-neutral-900)` | Tools that match CSS variable usage in source files |

The default `resolved` mode recursively resolves references to other CSS Forge tokens. Cycles
and unknown CSS variables remain as `var(...)` instead of causing generation to fail.

`css-reference` values are CSS custom-property references, not Style Dictionary aliases.
Style Dictionary aliases use `{path.to.token}`. Use the default `resolved` mode when Style
Dictionary will transform the file.

```bash
# Keep CSS variables as values for usage matching
pnpm run cssforge -- --mode style-dictionary --style-dictionary ./.cssforge/tokens.json --style-dictionary-value-mode css-reference
```

### Programmatic API

```typescript
import { generateStyleDictionaryJSON } from "jsr:@hebilicious/cssforge";

const resolvedTokens = generateStyleDictionaryJSON(config);
const usageTokens = generateStyleDictionaryJSON(config, {
valueMode: "css-reference",
});
```

### Example: Musea

Musea can use the generated file as its token source:

```typescript
import { musea } from "@vizejs/vite-plugin-musea";

musea({
tokensPath: ".cssforge/tokens.json",
});
```

## Agentic usage
Expand Down
103 changes: 100 additions & 3 deletions packages/cssforge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1027,19 +1027,116 @@ pnpm run cssforge
# Watch mode
pnpm run cssforge -- --watch

# Custom paths and output
pnpm run cssforge -- --config ./foo/bar/custom-path.ts --css ./dist/design-tokens.css --ts ./dist/design-tokens.ts --json ./dist/design-tokens.json --mode all
# Custom paths and output
pnpm run cssforge -- --config ./foo/bar/custom-path.ts --css ./dist/design-tokens.css --ts ./dist/design-tokens.ts --json ./dist/design-tokens.json --style-dictionary ./dist/design-tokens.sd.json --mode all

# Style Dictionary JSON with final values (default)
pnpm run cssforge -- --mode style-dictionary --style-dictionary ./dist/design-tokens.sd.json

# Keep CSS variables as values for usage matching
pnpm run cssforge -- --mode style-dictionary --style-dictionary ./dist/design-tokens.sd.json --style-dictionary-value-mode css-reference
```

## Programmatic Usage

You can also use CSS Forge programmatically:

```typescript
import { generateCSS } from "jsr:@hebilicious/cssforge";
import { generateCSS, generateStyleDictionaryJSON } from "jsr:@hebilicious/cssforge";

// Generate CSS string
const css = generateCSS(config);

// Write final values for Style Dictionary
const resolvedTokens = generateStyleDictionaryJSON(config);

// Keep var(--token) as each token's value for usage matching
const usageTokens = generateStyleDictionaryJSON(config, { valueMode: "css-reference" });
```

## Style Dictionary JSON

CSS Forge can generate a separate token file for Style Dictionary and other tools that read
the same JSON shape. This output does not change the CSS, TypeScript, or regular JSON files
you already generate.

### Generate the file

Use `style-dictionary` mode to generate only the token file:

```bash
pnpm run cssforge -- --mode style-dictionary --style-dictionary ./.cssforge/tokens.json
```

Use `--mode all` to generate it together with the CSS, TypeScript, and regular JSON outputs.
The `--style-dictionary` option controls where the token file is written.

A generated token looks like this:

```json
{
"palette": {
"neutral": {
"900": {
"value": "oklch(17.764% 0 0)",
"type": "color",
"$tier": "primitive",
"$resolvedValue": "oklch(17.764% 0 0)",
"attributes": {
"cssVariable": "--palette-neutral-900",
"cssVariableReference": "var(--palette-neutral-900)",
"resolvedValue": "oklch(17.764% 0 0)",
"sourcePath": "palette.neutral.900"
}
}
}
}
}
```

Semantic tokens also include `$reference` and `attributes.referencePaths`. These paths match
the keys in the generated file, so consumers can connect a semantic token to its source.

### Choose the value mode

| Mode | `value` contains | Use it for |
| --- | --- | --- |
| `resolved` (default) | The final value, such as `oklch(...)`, `1rem`, or `clamp(...)` | Style Dictionary transforms and token previews |
| `css-reference` | The token's own CSS variable, such as `var(--palette-neutral-900)` | Tools that match CSS variable usage in source files |

The default `resolved` mode recursively resolves references to other CSS Forge tokens. Cycles
and unknown CSS variables remain as `var(...)` instead of causing generation to fail.

`css-reference` values are CSS custom-property references, not Style Dictionary aliases.
Style Dictionary aliases use `{path.to.token}`. Use the default `resolved` mode when Style
Dictionary will transform the file.

```bash
# Keep CSS variables as values for usage matching
pnpm run cssforge -- --mode style-dictionary --style-dictionary ./.cssforge/tokens.json --style-dictionary-value-mode css-reference
```

### Programmatic API

```typescript
import { generateStyleDictionaryJSON } from "jsr:@hebilicious/cssforge";

const resolvedTokens = generateStyleDictionaryJSON(config);
const usageTokens = generateStyleDictionaryJSON(config, {
valueMode: "css-reference",
});
```

### Example: Musea

Musea can use the generated file as its token source:

```typescript
import { musea } from "@vizejs/vite-plugin-musea";

musea({
tokensPath: ".cssforge/tokens.json",
});
```

## Agentic usage
Expand Down
79 changes: 74 additions & 5 deletions packages/cssforge/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,45 @@ import type { CommandDef } from "citty";
*/
import { defineCommand, runMain } from "citty";
import type { CSSForgeConfig } from "./config.ts";
import { generateCSS, generateJSON, generateTS } from "./generator.ts";
import {
generateCSS,
generateJSON,
generateStyleDictionaryJSON,
generateTS,
} from "./generator.ts";

const writeFileRecursive = (path: string, data: string) =>
fs
.mkdir(path.replace(/\/[^/]*$/, ""), { recursive: true })
.then(() => fs.writeFile(path, data));

const outputModes = ["css", "json", "ts", "style-dictionary", "all"] as const;
type OutputMode = (typeof outputModes)[number];
const styleDictionaryValueModes = ["css-reference", "resolved"] as const;
type StyleDictionaryValueMode = (typeof styleDictionaryValueModes)[number];

const isOutputMode = (value: unknown): value is OutputMode =>
typeof value === "string" && outputModes.some((mode) => mode === value);

const isStyleDictionaryValueMode = (value: unknown): value is StyleDictionaryValueMode =>
typeof value === "string" && styleDictionaryValueModes.some((mode) => mode === value);

/**
* Defines the options for the build command.
*/
export interface BuildOptions {
/** Path to the configuration file. */
config: string;
/** The output mode. */
mode: "css" | "json" | "ts" | "all";
mode: OutputMode;
/** Path for the CSS output file. */
cssOutput: string;
/** Path for the JSON output file. */
jsonOutput: string;
/** Path for the Style Dictionary-compatible JSON output file. */
styleDictionaryOutput?: string;
/** Value representation used by the Style Dictionary-compatible JSON output. */
styleDictionaryValueMode?: StyleDictionaryValueMode;
/** Path for the TypeScript output file. */
tsOutput: string;
}
Expand All @@ -47,9 +67,14 @@ export async function build({
tsOutput,
cssOutput,
jsonOutput,
styleDictionaryOutput,
styleDictionaryValueMode = "resolved",
mode,
}: BuildOptions): Promise<{ success: boolean; error?: unknown }> {
try {
if (!isOutputMode(mode)) {
throw new Error(`Invalid output mode: ${mode}`);
}
const absoluteconfig = resolve(process.cwd(), config);
const absoluteCssOutput = resolve(process.cwd(), cssOutput);
const absoluteJsonOutput = resolve(process.cwd(), jsonOutput);
Expand All @@ -75,6 +100,23 @@ export async function build({
console.log(`✔ Generated JSON written to ${jsonOutput}`);
}

if (mode === "style-dictionary" || mode === "all") {
if (!isStyleDictionaryValueMode(styleDictionaryValueMode)) {
throw new Error(
`Invalid Style Dictionary value mode: ${styleDictionaryValueMode}`,
);
}
const outputPath = styleDictionaryOutput ?? "./.cssforge/tokens.sd.json";
const absoluteStyleDictionaryOutput = resolve(process.cwd(), outputPath);
await writeFileRecursive(
absoluteStyleDictionaryOutput,
generateStyleDictionaryJSON(userConfig.default as CSSForgeConfig, {
valueMode: styleDictionaryValueMode,
}),
);
console.log(`✔ Generated Style Dictionary JSON written to ${outputPath}`);
}

if (mode === "ts" || mode === "all") {
await writeFileRecursive(
absoluteTsOutput,
Expand Down Expand Up @@ -147,7 +189,7 @@ const mainCommand = defineCommand({
},
mode: {
type: "string",
description: "Output mode (css, json, ts, all)",
description: "Output mode (css, json, ts, style-dictionary, all)",
alias: "m",
default: "all",
},
Expand All @@ -161,6 +203,17 @@ const mainCommand = defineCommand({
description: "Optional path for an output JSON file",
default: "./.cssforge/output.json",
},
"style-dictionary": {
type: "string",
description: "Path for the Style Dictionary token JSON file",
default: "./.cssforge/tokens.sd.json",
},
"style-dictionary-value-mode": {
type: "string",
description:
"Token value mode: css-reference for usage matching, or resolved for previews and builds",
default: "resolved",
},
css: {
type: "string",
description: "Path for the output CSS file",
Expand All @@ -174,14 +227,30 @@ const mainCommand = defineCommand({
},
async run({ args }) {
const { watch: shouldWatch, config, css, json, ts, mode, prefix } = args;
const styleDictionary = args["style-dictionary"];
const styleDictionaryValueMode = args["style-dictionary-value-mode"];
if (!isOutputMode(mode)) {
console.error(`Error during build: Error: Invalid output mode: ${mode}`);
process.exit(1);
return;
}
if (!isStyleDictionaryValueMode(styleDictionaryValueMode)) {
console.error(
`Error during build: Error: Invalid Style Dictionary value mode: ${styleDictionaryValueMode}`,
);
process.exit(1);
return;
}
const realPath = (p: string) => resolve(prefix, p);
const settings = {
const settings: BuildOptions = {
mode,
config: realPath(config),
cssOutput: realPath(css),
tsOutput: realPath(ts),
jsonOutput: realPath(json),
} as BuildOptions;
styleDictionaryOutput: realPath(styleDictionary),
styleDictionaryValueMode,
};
if (shouldWatch) {
const cleanup = await watch(settings);

Expand Down
Loading
Loading