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 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).
@@ -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 };