@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 +77 -0
- package/dist/category-detector.d.ts +12 -0
- package/dist/category-detector.js +63 -0
- package/dist/category-detector.spec.d.ts +1 -0
- package/dist/category-detector.spec.js +86 -0
- package/dist/create-plugin.d.ts +7 -0
- package/dist/create-plugin.js +46 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +26 -0
- package/dist/token-data.d.ts +13 -0
- package/dist/token-data.js +199 -0
- package/dist/token-data.spec.d.ts +1 -0
- package/dist/token-data.spec.js +125 -0
- package/package.json +32 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|