@commercetools/nimbus-design-token-ts-plugin 0.1.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 ADDED
@@ -0,0 +1,77 @@
1
+ # @commercetools/nimbus-design-token-ts-plugin
2
+
3
+ TypeScript language service plugin that shows design token CSS values in
4
+ autocomplete when editing Chakra UI styled-system properties.
5
+
6
+ When you type `gap="` in a component, the autocomplete dropdown shows entries
7
+ like:
8
+
9
+ ```
10
+ 600 = 24px
11
+ 400 = 16px
12
+ 200 = 8px
13
+ ```
14
+
15
+ instead of just the raw token names.
16
+
17
+ ## Supported Token Categories
18
+
19
+ - **Spacing** (including negatives): `gap`, `padding`, `margin`, etc.
20
+ - **Font sizes**: `fontSize`
21
+ - **Border radius**: `borderRadius`
22
+ - **Colors**: `color`, `bg`, `borderColor`, etc.
23
+ - **Blur**: `blur`
24
+ - **Shadows**: `shadow`, `boxShadow`
25
+ - **Font weights**: `fontWeight`
26
+ - **Line heights**: `lineHeight`
27
+ - **Sizes**: `width`, `height`, `minWidth`, etc.
28
+ - **Opacity**: `opacity`
29
+ - **Z-index**: `zIndex`
30
+ - **Durations**: `transitionDuration`, `animationDuration`
31
+ - And more
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pnpm add -D @commercetools/nimbus-design-token-ts-plugin
37
+ ```
38
+
39
+ ## Configuration
40
+
41
+ Add the plugin to your `tsconfig.json` (or `tsconfig.app.json` if using project
42
+ references):
43
+
44
+ ```json
45
+ {
46
+ "compilerOptions": {
47
+ "plugins": [{ "name": "@commercetools/nimbus-design-token-ts-plugin" }]
48
+ }
49
+ }
50
+ ```
51
+
52
+ ## VS Code Setup
53
+
54
+ 1. Open the command palette (Cmd+Shift+P / Ctrl+Shift+P)
55
+ 2. Run **"TypeScript: Select TypeScript Version"**
56
+ 3. Select **"Use Workspace Version"**
57
+ 4. Restart the TS server: **"TypeScript: Restart TS Server"**
58
+
59
+ The plugin only works with the workspace TypeScript version, not VS Code's
60
+ built-in version.
61
+
62
+ ## How It Works
63
+
64
+ The plugin intercepts TypeScript's `getCompletionsAtPosition` calls. When it
65
+ detects that the completion entries match a known design token category (by
66
+ fingerprinting the set of available values), it annotates each entry with the
67
+ resolved CSS value from `@commercetools/nimbus-tokens`.
68
+
69
+ The detection is automatic - no configuration of which properties map to which
70
+ token categories is needed. The plugin infers the category from the completions
71
+ TypeScript already provides.
72
+
73
+ ## Requirements
74
+
75
+ - `@commercetools/nimbus-tokens` must be installed (defined as peer dependency)
76
+ - TypeScript >= 5.0
77
+ - VS Code or any editor that supports TypeScript language service plugins
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Detects which token category a set of completion entries belongs to
3
+ * by computing overlap scores against known category fingerprints.
4
+ */
5
+ import type { CategorySets } from "./token-data";
6
+ /**
7
+ * Given a list of completion entry names from TS, determine which token
8
+ * category they belong to by finding the category with the highest overlap.
9
+ *
10
+ * Returns the category name or undefined if no match exceeds the threshold.
11
+ */
12
+ export declare function detectCategory(entryNames: string[], categorySets: CategorySets): string | undefined;
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /**
3
+ * Detects which token category a set of completion entries belongs to
4
+ * by computing overlap scores against known category fingerprints.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.detectCategory = detectCategory;
8
+ /** Names to exclude from fingerprint matching */
9
+ const EXCLUDED_PATTERNS = [
10
+ /^\[/, // array-like entries
11
+ /^currentBg$/,
12
+ /^currentColor$/,
13
+ /^colorPalette/,
14
+ /^true$/,
15
+ /^false$/,
16
+ /!/, // entries with "!" (important modifiers)
17
+ /^inherit$/,
18
+ /^initial$/,
19
+ /^unset$/,
20
+ /^auto$/,
21
+ /^none$/,
22
+ /^transparent$/,
23
+ /^current$/,
24
+ ];
25
+ /**
26
+ * Given a list of completion entry names from TS, determine which token
27
+ * category they belong to by finding the category with the highest overlap.
28
+ *
29
+ * Returns the category name or undefined if no match exceeds the threshold.
30
+ */
31
+ function detectCategory(entryNames, categorySets) {
32
+ // Filter out non-token entries
33
+ const filtered = entryNames.filter((name) => !EXCLUDED_PATTERNS.some((pattern) => pattern.test(name)));
34
+ if (filtered.length === 0)
35
+ return undefined;
36
+ const filteredSet = new Set(filtered);
37
+ // Collect all candidates that exceed the overlap threshold
38
+ const candidates = [];
39
+ for (const [category, tokenSet] of Object.entries(categorySets)) {
40
+ let intersection = 0;
41
+ for (const token of tokenSet) {
42
+ if (filteredSet.has(token)) {
43
+ intersection++;
44
+ }
45
+ }
46
+ const ratio = intersection / tokenSet.size;
47
+ // Require > 50% of the category's tokens to appear in completions
48
+ if (ratio > 0.5) {
49
+ candidates.push({ category, ratio, intersection });
50
+ }
51
+ }
52
+ if (candidates.length === 0)
53
+ return undefined;
54
+ // When multiple categories match (e.g. fontWeights is a subset of spacing),
55
+ // prefer the one with the most matching tokens (largest intersection).
56
+ // If tied on intersection, prefer the higher ratio.
57
+ candidates.sort((a, b) => {
58
+ if (b.intersection !== a.intersection)
59
+ return b.intersection - a.intersection;
60
+ return b.ratio - a.ratio;
61
+ });
62
+ return candidates[0].category;
63
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const category_detector_1 = require("./category-detector");
5
+ const token_data_1 = require("./token-data");
6
+ const data = (0, token_data_1.loadTokenData)();
7
+ const { categorySets } = data;
8
+ /** Helper: get all token names for a category plus some CSS keywords */
9
+ function entriesFor(category, extras = []) {
10
+ return [...categorySets[category], ...extras];
11
+ }
12
+ (0, vitest_1.describe)("detectCategory", () => {
13
+ (0, vitest_1.it)("detects spacing tokens", () => {
14
+ const entries = entriesFor("spacing", ["auto", "inherit"]);
15
+ (0, vitest_1.expect)((0, category_detector_1.detectCategory)(entries, categorySets)).toBe("spacing");
16
+ });
17
+ (0, vitest_1.it)("detects fontSize tokens", () => {
18
+ const entries = entriesFor("fontSizes", ["inherit"]);
19
+ (0, vitest_1.expect)((0, category_detector_1.detectCategory)(entries, categorySets)).toBe("fontSizes");
20
+ });
21
+ (0, vitest_1.it)("detects borderRadius tokens", () => {
22
+ const entries = entriesFor("radii", ["none", "inherit"]);
23
+ (0, vitest_1.expect)((0, category_detector_1.detectCategory)(entries, categorySets)).toBe("radii");
24
+ });
25
+ (0, vitest_1.it)("detects blur tokens", () => {
26
+ const entries = entriesFor("blurs", ["none"]);
27
+ (0, vitest_1.expect)((0, category_detector_1.detectCategory)(entries, categorySets)).toBe("blurs");
28
+ });
29
+ (0, vitest_1.it)("detects fontWeight tokens", () => {
30
+ const entries = entriesFor("fontWeights", ["inherit"]);
31
+ (0, vitest_1.expect)((0, category_detector_1.detectCategory)(entries, categorySets)).toBe("fontWeights");
32
+ });
33
+ (0, vitest_1.it)("detects color tokens", () => {
34
+ const entries = entriesFor("colors", [
35
+ "transparent",
36
+ "currentColor",
37
+ "inherit",
38
+ ]);
39
+ (0, vitest_1.expect)((0, category_detector_1.detectCategory)(entries, categorySets)).toBe("colors");
40
+ });
41
+ (0, vitest_1.it)("detects shadow tokens", () => {
42
+ const entries = entriesFor("shadows", ["none"]);
43
+ (0, vitest_1.expect)((0, category_detector_1.detectCategory)(entries, categorySets)).toBe("shadows");
44
+ });
45
+ (0, vitest_1.it)("detects opacity tokens", () => {
46
+ const entries = entriesFor("opacity", ["inherit"]);
47
+ (0, vitest_1.expect)((0, category_detector_1.detectCategory)(entries, categorySets)).toBe("opacity");
48
+ });
49
+ (0, vitest_1.it)("detects zIndex tokens", () => {
50
+ const entries = entriesFor("zIndex", ["auto"]);
51
+ (0, vitest_1.expect)((0, category_detector_1.detectCategory)(entries, categorySets)).toBe("zIndex");
52
+ });
53
+ (0, vitest_1.it)("returns undefined for unrecognized entries", () => {
54
+ const entries = ["foo", "bar", "baz", "qux"];
55
+ (0, vitest_1.expect)((0, category_detector_1.detectCategory)(entries, categorySets)).toBeUndefined();
56
+ });
57
+ (0, vitest_1.it)("returns undefined for empty entries", () => {
58
+ (0, vitest_1.expect)((0, category_detector_1.detectCategory)([], categorySets)).toBeUndefined();
59
+ });
60
+ (0, vitest_1.it)("returns undefined when only excluded entries are present", () => {
61
+ const entries = ["inherit", "initial", "auto", "none", "transparent"];
62
+ (0, vitest_1.expect)((0, category_detector_1.detectCategory)(entries, categorySets)).toBeUndefined();
63
+ });
64
+ (0, vitest_1.it)("prefers spacing over fontWeights when both match (subset handling)", () => {
65
+ // Spacing includes all fontWeight values (100-900) plus many more
66
+ // When all spacing tokens are present, spacing should win
67
+ const entries = entriesFor("spacing");
68
+ (0, vitest_1.expect)((0, category_detector_1.detectCategory)(entries, categorySets)).toBe("spacing");
69
+ });
70
+ (0, vitest_1.it)("correctly identifies fontWeights when only weight values are present", () => {
71
+ // When only 100-900 are present (no other spacing tokens),
72
+ // fontWeights should win since it has higher ratio
73
+ const entries = [
74
+ "100",
75
+ "200",
76
+ "300",
77
+ "400",
78
+ "500",
79
+ "600",
80
+ "700",
81
+ "800",
82
+ "900",
83
+ ];
84
+ (0, vitest_1.expect)((0, category_detector_1.detectCategory)(entries, categorySets)).toBe("fontWeights");
85
+ });
86
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Creates a proxy LanguageService that enriches completions with
3
+ * design token CSS values.
4
+ */
5
+ import type typescript from "typescript";
6
+ import type { TokenData } from "./token-data";
7
+ export declare function createPlugin(_ts: typeof typescript, info: typescript.server.PluginCreateInfo, tokenData: TokenData): typescript.LanguageService;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ /**
3
+ * Creates a proxy LanguageService that enriches completions with
4
+ * design token CSS values.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.createPlugin = createPlugin;
8
+ const category_detector_1 = require("./category-detector");
9
+ function createPlugin(_ts, info, tokenData) {
10
+ const proxy = Object.create(null);
11
+ // Copy all methods from the original language service
12
+ const ls = info.languageService;
13
+ const p = proxy;
14
+ for (const key of Object.keys(ls)) {
15
+ const value = ls[key];
16
+ if (typeof value === "function") {
17
+ p[key] = value.bind(info.languageService);
18
+ }
19
+ }
20
+ // Override getCompletionsAtPosition to enrich entries
21
+ proxy.getCompletionsAtPosition = (fileName, position, options) => {
22
+ const original = info.languageService.getCompletionsAtPosition(fileName, position, options);
23
+ if (!original || original.entries.length === 0) {
24
+ return original;
25
+ }
26
+ // Extract entry names for category detection
27
+ const entryNames = original.entries.map((e) => e.name);
28
+ const category = (0, category_detector_1.detectCategory)(entryNames, tokenData.categorySets);
29
+ if (!category) {
30
+ return original;
31
+ }
32
+ const values = tokenData.categoryValues[category];
33
+ if (!values) {
34
+ return original;
35
+ }
36
+ // Enrich each entry with its CSS value
37
+ for (const entry of original.entries) {
38
+ const cssValue = values[entry.name];
39
+ if (cssValue !== undefined) {
40
+ entry.labelDetails = { detail: ` = ${cssValue}` };
41
+ }
42
+ }
43
+ return original;
44
+ };
45
+ return proxy;
46
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * TypeScript Language Service Plugin entry point.
3
+ *
4
+ * Shows design token CSS values (e.g., "= 24px") next to token names
5
+ * in autocomplete when editing Chakra styled-system properties.
6
+ */
7
+ import type typescript from "typescript";
8
+ declare function init(_modules: {
9
+ typescript: typeof typescript;
10
+ }): {
11
+ create(info: typescript.server.PluginCreateInfo): typescript.LanguageService;
12
+ };
13
+ export = init;
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ /**
3
+ * TypeScript Language Service Plugin entry point.
4
+ *
5
+ * Shows design token CSS values (e.g., "= 24px") next to token names
6
+ * in autocomplete when editing Chakra styled-system properties.
7
+ */
8
+ const token_data_1 = require("./token-data");
9
+ const create_plugin_1 = require("./create-plugin");
10
+ function init(_modules) {
11
+ return {
12
+ create(info) {
13
+ info.project.projectService.logger.info("[nimbus-design-token-ts-plugin] Initializing...");
14
+ const tokenData = (0, token_data_1.loadTokenData)();
15
+ if (!tokenData) {
16
+ info.project.projectService.logger.info("[nimbus-design-token-ts-plugin] Could not load token data, plugin disabled.");
17
+ return info.languageService;
18
+ }
19
+ const categoryCount = Object.keys(tokenData.categorySets).length;
20
+ const totalTokens = Object.values(tokenData.categorySets).reduce((sum, set) => sum + set.size, 0);
21
+ info.project.projectService.logger.info(`[nimbus-design-token-ts-plugin] Loaded ${totalTokens} tokens across ${categoryCount} categories.`);
22
+ return (0, create_plugin_1.createPlugin)(_modules.typescript, info, tokenData);
23
+ },
24
+ };
25
+ }
26
+ module.exports = init;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Loads design token data from @commercetools/nimbus-tokens and builds
3
+ * lookup maps for enriching autocomplete with CSS values.
4
+ */
5
+ /** category name → { token name → CSS value string } */
6
+ export type CategoryValues = Record<string, Record<string, string>>;
7
+ /** category name → Set of all token names in that category */
8
+ export type CategorySets = Record<string, Set<string>>;
9
+ export interface TokenData {
10
+ categoryValues: CategoryValues;
11
+ categorySets: CategorySets;
12
+ }
13
+ export declare function loadTokenData(): TokenData | undefined;
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ /**
3
+ * Loads design token data from @commercetools/nimbus-tokens and builds
4
+ * lookup maps for enriching autocomplete with CSS values.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.loadTokenData = loadTokenData;
8
+ /**
9
+ * Attempt to load tokens and build lookup maps.
10
+ * Returns undefined if token loading fails (graceful degradation).
11
+ */
12
+ function loadDesignTokens() {
13
+ // Try main entrypoint first
14
+ try {
15
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
16
+ const mod = require("@commercetools/nimbus-tokens");
17
+ if (mod.designTokens)
18
+ return mod.designTokens;
19
+ }
20
+ catch {
21
+ // Main entrypoint may fail in some Node/ESM contexts
22
+ }
23
+ // Fallback: resolve the package directory and load the generated CJS entrypoint
24
+ try {
25
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
26
+ const path = require("path");
27
+ // Resolve any file from the package to find its directory
28
+ // Use require.resolve with paths to find the package location
29
+ const mainPath = require.resolve("@commercetools/nimbus-tokens");
30
+ // mainPath is something like .../packages/tokens/dist/commercetools-nimbus-tokens.esm.js
31
+ // Navigate up to the package root, then down to the generated CJS file
32
+ const distDir = path.dirname(mainPath);
33
+ const pkgDir = path.dirname(distDir);
34
+ const generatedCjsPath = path.join(pkgDir, "generated/ts/dist/commercetools-nimbus-tokens-generated-ts.cjs.dev.js");
35
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
36
+ const mod = require(generatedCjsPath);
37
+ if (mod.designTokens)
38
+ return mod.designTokens;
39
+ }
40
+ catch {
41
+ // Fallback also failed
42
+ }
43
+ return undefined;
44
+ }
45
+ function loadTokenData() {
46
+ try {
47
+ const tokens = loadDesignTokens();
48
+ if (!tokens)
49
+ return undefined;
50
+ const categoryValues = {};
51
+ const categorySets = {};
52
+ // Simple flat categories: key → string value
53
+ const flatCategories = {
54
+ spacing: "spacing",
55
+ blur: "blurs",
56
+ borderRadius: "radii",
57
+ borderWidth: "borderWidths",
58
+ fontSize: "fontSizes",
59
+ lineHeight: "lineHeights",
60
+ size: "sizes",
61
+ border: "borders",
62
+ shadow: "shadows",
63
+ duration: "durations",
64
+ easing: "easings",
65
+ fontFamily: "fonts",
66
+ breakpoints: "breakpoints",
67
+ };
68
+ for (const [tokenKey, categoryName] of Object.entries(flatCategories)) {
69
+ const data = tokens[tokenKey];
70
+ if (!data || typeof data !== "object")
71
+ continue;
72
+ const values = {};
73
+ for (const [name, val] of Object.entries(data)) {
74
+ if (typeof val === "string") {
75
+ values[name] = val;
76
+ }
77
+ else if (typeof val === "number") {
78
+ values[name] = String(val);
79
+ }
80
+ // Skip composite objects (e.g. textStyle entries, letterSpacing with {value})
81
+ }
82
+ if (Object.keys(values).length > 0) {
83
+ categoryValues[categoryName] = values;
84
+ categorySets[categoryName] = new Set(Object.keys(values));
85
+ }
86
+ }
87
+ // Spacing: also generate negative tokens
88
+ if (categoryValues["spacing"]) {
89
+ const negValues = {
90
+ ...categoryValues["spacing"],
91
+ };
92
+ for (const [name, val] of Object.entries(categoryValues["spacing"])) {
93
+ // Only negate numeric token names
94
+ if (/^\d+$/.test(name) && val.endsWith("px")) {
95
+ const px = parseFloat(val);
96
+ negValues[`-${name}`] = `-${px}px`;
97
+ }
98
+ }
99
+ categoryValues["spacing"] = negValues;
100
+ categorySets["spacing"] = new Set(Object.keys(negValues));
101
+ }
102
+ // Font weight: numeric values
103
+ if (tokens.fontWeight && typeof tokens.fontWeight === "object") {
104
+ const values = {};
105
+ for (const [name, val] of Object.entries(tokens.fontWeight)) {
106
+ if (typeof val === "number") {
107
+ values[name] = String(val);
108
+ }
109
+ else if (typeof val === "string") {
110
+ values[name] = val;
111
+ }
112
+ }
113
+ if (Object.keys(values).length > 0) {
114
+ categoryValues["fontWeights"] = values;
115
+ categorySets["fontWeights"] = new Set(Object.keys(values));
116
+ }
117
+ }
118
+ // Opacity: numeric values
119
+ if (tokens.opacity && typeof tokens.opacity === "object") {
120
+ const values = {};
121
+ for (const [name, val] of Object.entries(tokens.opacity)) {
122
+ if (typeof val === "number") {
123
+ values[name] = String(val);
124
+ }
125
+ else if (typeof val === "string") {
126
+ values[name] = val;
127
+ }
128
+ }
129
+ if (Object.keys(values).length > 0) {
130
+ categoryValues["opacity"] = values;
131
+ categorySets["opacity"] = new Set(Object.keys(values));
132
+ }
133
+ }
134
+ // Colors: flatten nested structure into dot-notation keys
135
+ // The color object has sub-groups: "blacks-and-whites", "system-palettes",
136
+ // "brand-palettes", "semantic-palettes"
137
+ if (tokens.color && typeof tokens.color === "object") {
138
+ const colorValues = {};
139
+ for (const groupObj of Object.values(tokens.color)) {
140
+ if (!groupObj || typeof groupObj !== "object")
141
+ continue;
142
+ flattenColors(groupObj, "", colorValues);
143
+ }
144
+ if (Object.keys(colorValues).length > 0) {
145
+ categoryValues["colors"] = colorValues;
146
+ categorySets["colors"] = new Set(Object.keys(colorValues));
147
+ }
148
+ }
149
+ // zIndex
150
+ if (tokens.zIndex && typeof tokens.zIndex === "object") {
151
+ const values = {};
152
+ for (const [name, val] of Object.entries(tokens.zIndex)) {
153
+ if (typeof val === "number") {
154
+ values[name] = String(val);
155
+ }
156
+ else if (typeof val === "string") {
157
+ values[name] = val;
158
+ }
159
+ }
160
+ if (Object.keys(values).length > 0) {
161
+ categoryValues["zIndex"] = values;
162
+ categorySets["zIndex"] = new Set(Object.keys(values));
163
+ }
164
+ }
165
+ return { categoryValues, categorySets };
166
+ }
167
+ catch {
168
+ // Token loading failed — plugin degrades gracefully
169
+ return undefined;
170
+ }
171
+ }
172
+ /**
173
+ * Recursively flatten a color object into dot-notation keys.
174
+ * For light/dark structures, use the light value.
175
+ * E.g. { amber: { light: { "1": "hsl(...)" } } } → "amber.1" = "hsl(...)"
176
+ * E.g. { black: "hsl(...)" } → "black" = "hsl(...)"
177
+ */
178
+ function flattenColors(obj, prefix, result) {
179
+ for (const [key, val] of Object.entries(obj)) {
180
+ const fullKey = prefix ? `${prefix}.${key}` : key;
181
+ if (typeof val === "string") {
182
+ result[fullKey] = val;
183
+ }
184
+ else if (val && typeof val === "object") {
185
+ const record = val;
186
+ // If this object has "light" and "dark" keys, use the light values
187
+ if ("light" in record && "dark" in record) {
188
+ const lightObj = record.light;
189
+ if (lightObj && typeof lightObj === "object") {
190
+ flattenColors(lightObj, prefix ? `${prefix}.${key}` : key, result);
191
+ }
192
+ }
193
+ else {
194
+ // Regular nested object — recurse
195
+ flattenColors(record, fullKey, result);
196
+ }
197
+ }
198
+ }
199
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const token_data_1 = require("./token-data");
5
+ const nimbus_tokens_1 = require("@commercetools/nimbus-tokens");
6
+ (0, vitest_1.describe)("loadTokenData", () => {
7
+ const data = (0, token_data_1.loadTokenData)();
8
+ (0, vitest_1.it)("loads token data successfully", () => {
9
+ (0, vitest_1.expect)(data).toBeDefined();
10
+ });
11
+ (0, vitest_1.it)("contains expected categories", () => {
12
+ const categories = Object.keys(data.categorySets);
13
+ (0, vitest_1.expect)(categories).toContain("spacing");
14
+ (0, vitest_1.expect)(categories).toContain("fontSizes");
15
+ (0, vitest_1.expect)(categories).toContain("radii");
16
+ (0, vitest_1.expect)(categories).toContain("blurs");
17
+ (0, vitest_1.expect)(categories).toContain("colors");
18
+ (0, vitest_1.expect)(categories).toContain("fontWeights");
19
+ (0, vitest_1.expect)(categories).toContain("shadows");
20
+ (0, vitest_1.expect)(categories).toContain("opacity");
21
+ (0, vitest_1.expect)(categories).toContain("zIndex");
22
+ });
23
+ (0, vitest_1.describe)("spacing tokens", () => {
24
+ (0, vitest_1.it)("maps token names to CSS values from source tokens", () => {
25
+ const spacing = data.categoryValues["spacing"];
26
+ for (const [name, value] of Object.entries(nimbus_tokens_1.designTokens.spacing)) {
27
+ if (typeof value === "string") {
28
+ (0, vitest_1.expect)(spacing[name]).toBe(value);
29
+ }
30
+ }
31
+ });
32
+ (0, vitest_1.it)("includes negative tokens derived from source", () => {
33
+ const spacing = data.categoryValues["spacing"];
34
+ for (const [name, value] of Object.entries(nimbus_tokens_1.designTokens.spacing)) {
35
+ if (typeof value === "string" &&
36
+ /^\d+$/.test(name) &&
37
+ value.endsWith("px")) {
38
+ const px = parseFloat(value);
39
+ (0, vitest_1.expect)(spacing[`-${name}`]).toBe(`-${px}px`);
40
+ }
41
+ }
42
+ });
43
+ (0, vitest_1.it)("has matching sets and values", () => {
44
+ const set = data.categorySets["spacing"];
45
+ const values = data.categoryValues["spacing"];
46
+ (0, vitest_1.expect)(set.size).toBe(Object.keys(values).length);
47
+ for (const name of set) {
48
+ (0, vitest_1.expect)(values[name]).toBeDefined();
49
+ }
50
+ });
51
+ });
52
+ (0, vitest_1.describe)("fontSize tokens", () => {
53
+ (0, vitest_1.it)("maps all source fontSize tokens", () => {
54
+ const fontSizes = data.categoryValues["fontSizes"];
55
+ for (const [name, value] of Object.entries(nimbus_tokens_1.designTokens.fontSize)) {
56
+ if (typeof value === "string") {
57
+ (0, vitest_1.expect)(fontSizes[name]).toBe(value);
58
+ }
59
+ }
60
+ });
61
+ });
62
+ (0, vitest_1.describe)("borderRadius tokens", () => {
63
+ (0, vitest_1.it)("maps all source borderRadius tokens", () => {
64
+ const radii = data.categoryValues["radii"];
65
+ for (const [name, value] of Object.entries(nimbus_tokens_1.designTokens.borderRadius)) {
66
+ if (typeof value === "string") {
67
+ (0, vitest_1.expect)(radii[name]).toBe(value);
68
+ }
69
+ }
70
+ });
71
+ });
72
+ (0, vitest_1.describe)("color tokens", () => {
73
+ (0, vitest_1.it)("includes flat colors from blacks-and-whites", () => {
74
+ const colors = data.categoryValues["colors"];
75
+ const bw = nimbus_tokens_1.designTokens.color["blacks-and-whites"];
76
+ (0, vitest_1.expect)(colors["black"]).toBe(bw.black);
77
+ (0, vitest_1.expect)(colors["white"]).toBe(bw.white);
78
+ });
79
+ (0, vitest_1.it)("includes alpha colors with dot notation", () => {
80
+ const colors = data.categoryValues["colors"];
81
+ const bw = nimbus_tokens_1.designTokens.color["blacks-and-whites"];
82
+ for (const [name, value] of Object.entries(bw.blackAlpha)) {
83
+ (0, vitest_1.expect)(colors[`blackAlpha.${name}`]).toBe(value);
84
+ }
85
+ });
86
+ (0, vitest_1.it)("flattens palette colors using light values", () => {
87
+ const colors = data.categoryValues["colors"];
88
+ const amber = nimbus_tokens_1.designTokens.color["system-palettes"].amber;
89
+ for (const [name, value] of Object.entries(amber.light)) {
90
+ (0, vitest_1.expect)(colors[`amber.${name}`]).toBe(value);
91
+ }
92
+ });
93
+ (0, vitest_1.it)("includes semantic palette colors", () => {
94
+ const colors = data.categoryValues["colors"];
95
+ const neutral = nimbus_tokens_1.designTokens.color["semantic-palettes"].neutral;
96
+ for (const [name, value] of Object.entries(neutral.light)) {
97
+ (0, vitest_1.expect)(colors[`neutral.${name}`]).toBe(value);
98
+ }
99
+ });
100
+ });
101
+ (0, vitest_1.describe)("fontWeight tokens", () => {
102
+ (0, vitest_1.it)("maps all source fontWeight tokens", () => {
103
+ const weights = data.categoryValues["fontWeights"];
104
+ for (const [name, value] of Object.entries(nimbus_tokens_1.designTokens.fontWeight)) {
105
+ (0, vitest_1.expect)(weights[name]).toBe(String(value));
106
+ }
107
+ });
108
+ });
109
+ (0, vitest_1.describe)("opacity tokens", () => {
110
+ (0, vitest_1.it)("maps all source opacity tokens", () => {
111
+ const opacity = data.categoryValues["opacity"];
112
+ for (const [name, value] of Object.entries(nimbus_tokens_1.designTokens.opacity)) {
113
+ (0, vitest_1.expect)(opacity[name]).toBe(String(value));
114
+ }
115
+ });
116
+ });
117
+ (0, vitest_1.describe)("zIndex tokens", () => {
118
+ (0, vitest_1.it)("maps all source zIndex tokens", () => {
119
+ const zIndex = data.categoryValues["zIndex"];
120
+ for (const [name, value] of Object.entries(nimbus_tokens_1.designTokens.zIndex)) {
121
+ (0, vitest_1.expect)(zIndex[name]).toBe(String(value));
122
+ }
123
+ });
124
+ });
125
+ });
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@commercetools/nimbus-design-token-ts-plugin",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript language service plugin that shows design token CSS values in autocomplete",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org/"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/commercetools/nimbus.git"
14
+ },
15
+ "peerDependencies": {
16
+ "@commercetools/nimbus-tokens": "^2.5.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^24.10.1",
20
+ "typescript": "~5.9.3",
21
+ "vitest": "^4.0.18",
22
+ "@commercetools/nimbus-tokens": "^2.5.0"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsc && echo '🎉 Nimbus design token typescript plugin built'",
29
+ "typecheck": "tsc --noEmit",
30
+ "test": "vitest run"
31
+ }
32
+ }