tokens-to-css 1.0.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/LICENSE +21 -0
- package/README.md +191 -0
- package/dist/index.d.ts +128 -0
- package/dist/index.js +1366 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Osvaldo Morgan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# tokens-to-css
|
|
2
|
+
|
|
3
|
+
Convert design-token JSON into a CSS custom-properties stylesheet.
|
|
4
|
+
|
|
5
|
+
You hand it a token file — a path or a URL — and it writes a stylesheet of
|
|
6
|
+
`:root` custom properties your app can link, with alias relationships kept as
|
|
7
|
+
`var(--…)` rather than flattened. It is not a multi-platform token pipeline and
|
|
8
|
+
does not try to become one.
|
|
9
|
+
|
|
10
|
+
Zero runtime dependencies.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm i -D tokens-to-css
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Use it
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import { generateCss } from 'tokens-to-css'
|
|
20
|
+
|
|
21
|
+
await generateCss('design/tokens.json')
|
|
22
|
+
// wrote assets/css/tokens.css
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
That is the whole integration. No config file, no CLI: you own the invocation
|
|
26
|
+
site, so it goes wherever your build already lives — an npm script, a build
|
|
27
|
+
step, a bootstrap file.
|
|
28
|
+
|
|
29
|
+
Given this:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"color": {
|
|
34
|
+
"brand": { "$value": "#5A4FCF", "$type": "color" },
|
|
35
|
+
"ink": { "$value": "#191627", "$type": "color" },
|
|
36
|
+
"text": { "$value": "{color.ink}" }
|
|
37
|
+
},
|
|
38
|
+
"space": {
|
|
39
|
+
"md": { "$value": { "value": 16, "unit": "px" }, "$type": "dimension" }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
you get this:
|
|
45
|
+
|
|
46
|
+
```css
|
|
47
|
+
:root {
|
|
48
|
+
--color-brand: #5A4FCF;
|
|
49
|
+
--color-ink: #191627;
|
|
50
|
+
--color-text: var(--color-ink);
|
|
51
|
+
--space-md: 16px;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Look at `--color-text`. Your token file said *text is ink*, and so does the
|
|
56
|
+
stylesheet — the relationship survived instead of being flattened to `#191627`.
|
|
57
|
+
Change the primitive and everything pointing at it moves, which is the reason to
|
|
58
|
+
keep tokens in a hierarchy at all.
|
|
59
|
+
|
|
60
|
+
A URL works wherever a path does:
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
await generateCss('https://tokens.example.com/design.json')
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## What it reads
|
|
67
|
+
|
|
68
|
+
Three shapes, checked in this order. The first that matches decides how the
|
|
69
|
+
document is read.
|
|
70
|
+
|
|
71
|
+
| | |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| **Tokens Studio** | Exports with `$themes` / `$metadata`. The token set wrapper is dropped from the name, so you get `--color-brand`, not `--global-color-brand`. |
|
|
74
|
+
| **DTCG** | `$value`, `$type`, aliases — including the object notation the current spec uses for colours and dimensions. |
|
|
75
|
+
| **Style Dictionary legacy** | `value` / `type` without the dollar. Converts to a byte-identical stylesheet. |
|
|
76
|
+
|
|
77
|
+
Hierarchy is not a separate concern: three-tier, CTI, EightShapes-like or any
|
|
78
|
+
other nesting all flatten through the same naming rule.
|
|
79
|
+
|
|
80
|
+
Full detail, including everything it refuses, is in
|
|
81
|
+
[docs/formats.md](docs/formats.md).
|
|
82
|
+
|
|
83
|
+
## When it fails
|
|
84
|
+
|
|
85
|
+
It either writes a correct stylesheet or writes nothing at all. There is no
|
|
86
|
+
partial output, and a failed run never touches the stylesheet already there.
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
TokenCssError [ALIAS_DANGLING]
|
|
90
|
+
1 reference points nowhere:
|
|
91
|
+
"color.text" references "color.inkk", which does not exist
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Every failure carries a stable code you can branch on:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
try {
|
|
98
|
+
await generateCss('design/tokens.json')
|
|
99
|
+
} catch (error) {
|
|
100
|
+
error.code // e.g. 'ALIAS_DANGLING'
|
|
101
|
+
error.source // the Token Source, as you passed it
|
|
102
|
+
error.tokenPaths // the offending tokens
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The eight codes are listed in [docs/failures.md](docs/failures.md), generated
|
|
107
|
+
from the source that defines them.
|
|
108
|
+
|
|
109
|
+
## Options
|
|
110
|
+
|
|
111
|
+
All optional.
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
await generateCss('design/tokens.json', {
|
|
115
|
+
outDir: 'public/styles', // default: assets/css
|
|
116
|
+
fileName: 'design-tokens.css', // default: tokens.css
|
|
117
|
+
baseDir: process.cwd(), // what relative paths resolve against
|
|
118
|
+
http: { // only used when the source is a URL
|
|
119
|
+
allowInsecure: false, // https only, unless you say otherwise
|
|
120
|
+
timeoutMs: 10_000,
|
|
121
|
+
maxBytes: 10_000_000,
|
|
122
|
+
maxRedirects: 3,
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A remote source is fetched under a guard: `https` by default, one deadline
|
|
128
|
+
across the whole exchange, a size cap enforced while the body streams, redirects
|
|
129
|
+
re-validated at every hop, and loopback, private, link-local and cloud-metadata
|
|
130
|
+
addresses refused — including when the URL names one literally.
|
|
131
|
+
|
|
132
|
+
## Documentation
|
|
133
|
+
|
|
134
|
+
| | |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| [Getting started](docs/getting-started.md) | Five steps from install to a stylesheet, including breaking it on purpose |
|
|
137
|
+
| [What it accepts](docs/formats.md) | The three shapes, the order they are checked, and everything refused |
|
|
138
|
+
| [The naming rule](docs/naming.md) | How `color.brand` becomes `--color-brand`, and why that is a promise |
|
|
139
|
+
| [Failure codes](docs/failures.md) | The eight codes and what each one means |
|
|
140
|
+
|
|
141
|
+
## What it will not do
|
|
142
|
+
|
|
143
|
+
Some of these are on purpose and stay that way; the rest are recorded as
|
|
144
|
+
deferred, not forgotten.
|
|
145
|
+
|
|
146
|
+
- **Invent units.** A token whose value is `16` emits `16`, never `16px`,
|
|
147
|
+
whatever its `$type` says.
|
|
148
|
+
- **Flatten references.** `var(--…)` all the way down.
|
|
149
|
+
- **Evaluate expressions.** `{spacing.md} * 2` is refused rather than computed.
|
|
150
|
+
There is no evaluator in this package. `calc()` and `clamp()` are valid CSS
|
|
151
|
+
and pass through untouched.
|
|
152
|
+
- **Convert composite tokens.** Typography, shadow, border, gradient and
|
|
153
|
+
transition are refused: a typography token is five CSS properties, and
|
|
154
|
+
accepting it would change what "one token, one custom property" means.
|
|
155
|
+
Deferred to a version after this one.
|
|
156
|
+
- **Pick a winner on a collision.** Two token paths that produce the same
|
|
157
|
+
custom-property name fail the conversion rather than one quietly overwriting
|
|
158
|
+
the other.
|
|
159
|
+
|
|
160
|
+
## Requirements
|
|
161
|
+
|
|
162
|
+
Node **22.12 or newer**. Development and CI target Node 24; CI also runs 22 and
|
|
163
|
+
26. ESM only, with TypeScript types included.
|
|
164
|
+
|
|
165
|
+
Conversion writes to disk, so it needs a writable filesystem. There is no
|
|
166
|
+
browser build.
|
|
167
|
+
|
|
168
|
+
## Contributing
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
npm install
|
|
172
|
+
npm run check # lint + typecheck + tests
|
|
173
|
+
npm run build
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`npm run lint` is not a style linter. It enforces the architecture: the build
|
|
177
|
+
fails if a pure stage (`src/dialects/`, `src/validate/`, `src/emit/`,
|
|
178
|
+
`src/model/`, `src/pipeline.ts`) reaches for the filesystem or the network, and
|
|
179
|
+
it checks that the generated fixtures and documentation still match the code
|
|
180
|
+
they came from.
|
|
181
|
+
|
|
182
|
+
The [fixtures](fixtures/README.md) are the specification — nine files that must
|
|
183
|
+
convert byte for byte, seventeen that must fail with a named code. Where the
|
|
184
|
+
documentation and the fixtures disagree, the fixtures are right.
|
|
185
|
+
|
|
186
|
+
The requirements, the architecture and the work breakdown live under
|
|
187
|
+
[`_bmad-output/planning-artifacts/`](_bmad-output/planning-artifacts/).
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT — see [LICENSE](LICENSE).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
//#region src/options.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* What a caller passes in and gets back.
|
|
4
|
+
*
|
|
5
|
+
* These live apart from the public surface so the orchestrator can read them
|
|
6
|
+
* without importing the entry point that calls it — a cycle between the two
|
|
7
|
+
* would work in ESM and still be the wrong shape (AD-1).
|
|
8
|
+
*/
|
|
9
|
+
/** How a remote Token Source is fetched. Ignored when the source is a local path. */
|
|
10
|
+
interface HttpOptions {
|
|
11
|
+
/** Allow `http:` URLs. Off by default — only `https:` is fetched. */
|
|
12
|
+
readonly allowInsecure?: boolean;
|
|
13
|
+
/** Total budget for the request, in milliseconds. */
|
|
14
|
+
readonly timeoutMs?: number;
|
|
15
|
+
/** Largest response body accepted, in bytes. */
|
|
16
|
+
readonly maxBytes?: number;
|
|
17
|
+
/** How many redirects to follow before giving up. */
|
|
18
|
+
readonly maxRedirects?: number;
|
|
19
|
+
}
|
|
20
|
+
/** Everything a caller can adjust about a conversion. */
|
|
21
|
+
interface GenerateCssOptions {
|
|
22
|
+
/** Directory the stylesheet is written to. Defaults to `assets/css`. */
|
|
23
|
+
readonly outDir?: string;
|
|
24
|
+
/** Filename inside that directory. Defaults to `tokens.css`. */
|
|
25
|
+
readonly fileName?: string;
|
|
26
|
+
/** Base for resolving relative paths. Defaults to the current working directory. */
|
|
27
|
+
readonly baseDir?: string;
|
|
28
|
+
/** Network policy for a URL Token Source. */
|
|
29
|
+
readonly http?: HttpOptions;
|
|
30
|
+
}
|
|
31
|
+
/** What a successful conversion reports back. Deliberately carries no CSS. */
|
|
32
|
+
interface GenerateCssResult {
|
|
33
|
+
/** Absolute path of the stylesheet that was written. */
|
|
34
|
+
readonly outputPath: string;
|
|
35
|
+
/** How many custom properties it declares. */
|
|
36
|
+
readonly tokenCount: number;
|
|
37
|
+
}
|
|
38
|
+
/** The defaults a conversion uses when the caller says nothing. */
|
|
39
|
+
declare const DEFAULTS: Readonly<{
|
|
40
|
+
outDir: "assets/css";
|
|
41
|
+
fileName: "tokens.css";
|
|
42
|
+
http: Readonly<{
|
|
43
|
+
allowInsecure: false;
|
|
44
|
+
timeoutMs: 10000;
|
|
45
|
+
maxBytes: 10000000;
|
|
46
|
+
maxRedirects: 3;
|
|
47
|
+
}>;
|
|
48
|
+
}>;
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/errors.d.ts
|
|
51
|
+
/**
|
|
52
|
+
* The failure contract — public surface, frozen by semver (AD-4).
|
|
53
|
+
*
|
|
54
|
+
* Every failure in this library is a `TokenCssError` carrying one of the codes
|
|
55
|
+
* below. Callers branch on `code`; they never match on message text, so message
|
|
56
|
+
* wording stays free to improve. Renaming or merging a code is a major version.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* The complete set of ways a conversion can fail.
|
|
60
|
+
*
|
|
61
|
+
* One code per failure class in the PRD. Adding a code is a minor version;
|
|
62
|
+
* renaming, merging, or removing one is a major version.
|
|
63
|
+
*/
|
|
64
|
+
declare const FailureCode: Readonly<{
|
|
65
|
+
/** The path or URL could not be read: missing, denied, unreachable, timed out, oversized, or refused by network policy. */
|
|
66
|
+
readonly SOURCE_UNREADABLE: 'SOURCE_UNREADABLE';
|
|
67
|
+
/** The source was read but its content is not valid JSON. */
|
|
68
|
+
readonly SOURCE_INVALID_JSON: 'SOURCE_INVALID_JSON';
|
|
69
|
+
/** The document is not a shape this version accepts. */
|
|
70
|
+
readonly FORMAT_NOT_ALLOWED: 'FORMAT_NOT_ALLOWED';
|
|
71
|
+
/** Aliases reference each other in a loop. */
|
|
72
|
+
readonly ALIAS_CYCLE: 'ALIAS_CYCLE';
|
|
73
|
+
/** An alias points at a token that does not exist. */
|
|
74
|
+
readonly ALIAS_DANGLING: 'ALIAS_DANGLING';
|
|
75
|
+
/** A value is an object, array, boolean, or null rather than a scalar. */
|
|
76
|
+
readonly COMPOSITE_VALUE: 'COMPOSITE_VALUE';
|
|
77
|
+
/** Two or more tokens would emit the same custom-property name. */
|
|
78
|
+
readonly NAME_COLLISION: 'NAME_COLLISION';
|
|
79
|
+
/** The stylesheet could not be written. */
|
|
80
|
+
readonly OUTPUT_WRITE_FAILED: 'OUTPUT_WRITE_FAILED';
|
|
81
|
+
}>;
|
|
82
|
+
/** One of the eight failure codes. */
|
|
83
|
+
type FailureCode = (typeof FailureCode)[keyof typeof FailureCode];
|
|
84
|
+
/** What a `TokenCssError` is constructed from. */
|
|
85
|
+
interface TokenCssErrorInit {
|
|
86
|
+
/** The failure class. */
|
|
87
|
+
readonly code: FailureCode;
|
|
88
|
+
/** The Token Source the conversion was working on, as the caller supplied it. */
|
|
89
|
+
readonly source: string;
|
|
90
|
+
/** Dotted paths of the offending tokens, for token-scoped failures. */
|
|
91
|
+
readonly tokenPaths?: readonly string[];
|
|
92
|
+
/** The underlying error, when this one wraps something lower-level. */
|
|
93
|
+
readonly cause?: unknown;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The only error this library throws.
|
|
97
|
+
*
|
|
98
|
+
* There are no subclasses: the `code` is what callers branch on, and one type
|
|
99
|
+
* means a caller never has to ask which error shape it caught.
|
|
100
|
+
*/
|
|
101
|
+
declare class TokenCssError extends Error {
|
|
102
|
+
readonly name = "TokenCssError";
|
|
103
|
+
/** The failure class. Stable across minor versions. */
|
|
104
|
+
readonly code: FailureCode;
|
|
105
|
+
/** The Token Source being converted when this failed. */
|
|
106
|
+
readonly source: string;
|
|
107
|
+
/** Offending token paths — empty for failures that are not token-scoped. */
|
|
108
|
+
readonly tokenPaths: readonly string[];
|
|
109
|
+
constructor(message: string, init: TokenCssErrorInit);
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/index.d.ts
|
|
113
|
+
/**
|
|
114
|
+
* Convert a design-token document into a CSS custom-properties stylesheet.
|
|
115
|
+
*
|
|
116
|
+
* Reads the Token Source, validates it completely, and writes the stylesheet —
|
|
117
|
+
* or throws a `TokenCssError` and writes nothing at all. There is no partial
|
|
118
|
+
* success: a previous stylesheet at the target path is left untouched whenever
|
|
119
|
+
* the conversion fails.
|
|
120
|
+
*
|
|
121
|
+
* @param source Path to a single local file, or a URL.
|
|
122
|
+
* @param options Output location and network policy.
|
|
123
|
+
* @returns Where the stylesheet was written, and how many properties it holds.
|
|
124
|
+
* @throws {TokenCssError} With a `code` naming the failure class.
|
|
125
|
+
*/
|
|
126
|
+
declare function generateCss(source: string | URL, options?: GenerateCssOptions): Promise<GenerateCssResult>;
|
|
127
|
+
//#endregion
|
|
128
|
+
export { DEFAULTS, FailureCode, type GenerateCssOptions, type GenerateCssResult, type HttpOptions, TokenCssError, type TokenCssErrorInit, generateCss };
|