@preply/ds-ai-core 11.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/AGENTS.md +7 -0
- package/README.md +34 -0
- package/dist/event-tracking.d.ts +21 -0
- package/dist/event-tracking.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38209 -0
- package/dist/tools/components/__tests__/get-component-docs.test.d.ts +2 -0
- package/dist/tools/components/__tests__/get-component-docs.test.d.ts.map +1 -0
- package/dist/tools/components/__tests__/list-components.test.d.ts +2 -0
- package/dist/tools/components/__tests__/list-components.test.d.ts.map +1 -0
- package/dist/tools/components/context/__test__/get-components-data.test.d.ts +2 -0
- package/dist/tools/components/context/__test__/get-components-data.test.d.ts.map +1 -0
- package/dist/tools/components/context/__test__/test-components/FixtureButton.d.ts +15 -0
- package/dist/tools/components/context/__test__/test-components/FixtureButton.d.ts.map +1 -0
- package/dist/tools/components/context/__test__/test-components/index.d.ts +2 -0
- package/dist/tools/components/context/__test__/test-components/index.d.ts.map +1 -0
- package/dist/tools/components/context/extract-component-docgen-info.d.ts +3 -0
- package/dist/tools/components/context/extract-component-docgen-info.d.ts.map +1 -0
- package/dist/tools/components/context/get-components-data.d.ts +24 -0
- package/dist/tools/components/context/get-components-data.d.ts.map +1 -0
- package/dist/tools/components/context/index.d.ts +6 -0
- package/dist/tools/components/context/index.d.ts.map +1 -0
- package/dist/tools/components/context/render-component-doc.d.ts +4 -0
- package/dist/tools/components/context/render-component-doc.d.ts.map +1 -0
- package/dist/tools/components/get-component-docs.d.ts +15 -0
- package/dist/tools/components/get-component-docs.d.ts.map +1 -0
- package/dist/tools/components/list-components.d.ts +9 -0
- package/dist/tools/components/list-components.d.ts.map +1 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/search-icon/__tests__/get-icons-data.test.d.ts +2 -0
- package/dist/tools/search-icon/__tests__/get-icons-data.test.d.ts.map +1 -0
- package/dist/tools/search-icon/__tests__/search-icon.test.d.ts +2 -0
- package/dist/tools/search-icon/__tests__/search-icon.test.d.ts.map +1 -0
- package/dist/tools/search-icon/get-icons-data.d.ts +6 -0
- package/dist/tools/search-icon/get-icons-data.d.ts.map +1 -0
- package/dist/tools/search-icon/search-icon.d.ts +18 -0
- package/dist/tools/search-icon/search-icon.d.ts.map +1 -0
- package/dist/tools/search-token/__tests__/search-token-by-name.test.d.ts +2 -0
- package/dist/tools/search-token/__tests__/search-token-by-name.test.d.ts.map +1 -0
- package/dist/tools/search-token/__tests__/search-token-by-value.test.d.ts +2 -0
- package/dist/tools/search-token/__tests__/search-token-by-value.test.d.ts.map +1 -0
- package/dist/tools/search-token/context/__tests__/get-tokens-data.test.d.ts +2 -0
- package/dist/tools/search-token/context/__tests__/get-tokens-data.test.d.ts.map +1 -0
- package/dist/tools/search-token/context/get-tokens-data.d.ts +13 -0
- package/dist/tools/search-token/context/get-tokens-data.d.ts.map +1 -0
- package/dist/tools/search-token/context/index.d.ts +6 -0
- package/dist/tools/search-token/context/index.d.ts.map +1 -0
- package/dist/tools/search-token/search-token-by-name.d.ts +26 -0
- package/dist/tools/search-token/search-token-by-name.d.ts.map +1 -0
- package/dist/tools/search-token/search-token-by-value.d.ts +24 -0
- package/dist/tools/search-token/search-token-by-value.d.ts.map +1 -0
- package/dist/tools/search-token/token-utils.d.ts +9 -0
- package/dist/tools/search-token/token-utils.d.ts.map +1 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/create-search-index.d.ts +48 -0
- package/dist/utils/create-search-index.d.ts.map +1 -0
- package/dist/utils/define-tool.d.ts +49 -0
- package/dist/utils/define-tool.d.ts.map +1 -0
- package/dist/utils/format-list.d.ts +26 -0
- package/dist/utils/format-list.d.ts.map +1 -0
- package/dist/utils/md.d.ts +24 -0
- package/dist/utils/md.d.ts.map +1 -0
- package/package.json +48 -0
- package/src/event-tracking.ts +117 -0
- package/src/index.ts +4 -0
- package/src/tools/components/__tests__/get-component-docs.test.ts +58 -0
- package/src/tools/components/__tests__/list-components.test.ts +63 -0
- package/src/tools/components/context/__test__/get-components-data.test.ts +57 -0
- package/src/tools/components/context/__test__/test-components/FixtureButton.tsx +18 -0
- package/src/tools/components/context/__test__/test-components/index.ts +1 -0
- package/src/tools/components/context/__test__/test-components/tsconfig.json +11 -0
- package/src/tools/components/context/extract-component-docgen-info.ts +108 -0
- package/src/tools/components/context/get-components-data.ts +94 -0
- package/src/tools/components/context/index.ts +4 -0
- package/src/tools/components/context/render-component-doc.ts +89 -0
- package/src/tools/components/get-component-docs.ts +26 -0
- package/src/tools/components/list-components.ts +36 -0
- package/src/tools/index.ts +5 -0
- package/src/tools/search-icon/__tests__/get-icons-data.test.ts +22 -0
- package/src/tools/search-icon/__tests__/search-icon.test.ts +235 -0
- package/src/tools/search-icon/__tests__/test-icons/NotIcon.md +1 -0
- package/src/tools/search-icon/__tests__/test-icons/OtherIcon.svg +1 -0
- package/src/tools/search-icon/__tests__/test-icons/TokyoUIClose.svg +1 -0
- package/src/tools/search-icon/__tests__/test-icons/TokyoUIHelp.svg +1 -0
- package/src/tools/search-icon/get-icons-data.ts +19 -0
- package/src/tools/search-icon/search-icon.ts +100 -0
- package/src/tools/search-token/__tests__/search-token-by-name.test.ts +384 -0
- package/src/tools/search-token/__tests__/search-token-by-value.test.ts +250 -0
- package/src/tools/search-token/context/__tests__/get-tokens-data.test.ts +148 -0
- package/src/tools/search-token/context/get-tokens-data.ts +103 -0
- package/src/tools/search-token/context/index.ts +4 -0
- package/src/tools/search-token/search-token-by-name.ts +110 -0
- package/src/tools/search-token/search-token-by-value.ts +107 -0
- package/src/tools/search-token/token-utils.ts +60 -0
- package/src/types.ts +3 -0
- package/src/utils/create-search-index.ts +121 -0
- package/src/utils/define-tool.ts +67 -0
- package/src/utils/format-list.ts +38 -0
- package/src/utils/md.ts +12 -0
- package/tsconfig.json +11 -0
- package/vite.config.ts +23 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
tokens: {
|
|
5
|
+
spacing: {
|
|
6
|
+
'4': ['base', 'abc123', false],
|
|
7
|
+
'8': ['base', 'def456', false],
|
|
8
|
+
},
|
|
9
|
+
root: {
|
|
10
|
+
radius: {
|
|
11
|
+
xs: ['base', 'ghi789', false],
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
color: {
|
|
15
|
+
text: {
|
|
16
|
+
primary: ['base', 'aaa111', true],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
} as unknown as typeof import('@preply/ds-core').tokens,
|
|
20
|
+
valuesWeb: {
|
|
21
|
+
tabc123: '4px',
|
|
22
|
+
tdef456: '8px',
|
|
23
|
+
tghi789: 2,
|
|
24
|
+
taaa111: { base: '#111111' },
|
|
25
|
+
} as unknown as typeof import('@preply/ds-theme-tokyo-ui').VALUES_WEB,
|
|
26
|
+
valuesRn: {
|
|
27
|
+
tabc123: 4,
|
|
28
|
+
tdef456: 8,
|
|
29
|
+
tghi789: 2,
|
|
30
|
+
taaa111: { base: '#101010' },
|
|
31
|
+
} as unknown as typeof import('@preply/ds-theme-tokyo-ui').VALUES_RN,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock(import('@preply/ds-theme-tokyo-ui'), async () => ({
|
|
35
|
+
VALUES_WEB: mocks.valuesWeb,
|
|
36
|
+
VALUES_RN: mocks.valuesRn,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
vi.mock(import('@preply/ds-core'), async () => ({
|
|
40
|
+
...(await import('@preply/ds-core')),
|
|
41
|
+
tokens: mocks.tokens,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.resetModules();
|
|
46
|
+
vi.resetAllMocks();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('getTokensData', () => {
|
|
50
|
+
it('flattens token tree and computes formatted names for web', async () => {
|
|
51
|
+
const { getTokensData } = await import('../get-tokens-data');
|
|
52
|
+
const data = getTokensData();
|
|
53
|
+
|
|
54
|
+
expect(data.web).toMatchInlineSnapshot(`
|
|
55
|
+
[
|
|
56
|
+
{
|
|
57
|
+
"less": "@spacing-4",
|
|
58
|
+
"name": "spacing.4",
|
|
59
|
+
"private": false,
|
|
60
|
+
"scss": "$spacing-4",
|
|
61
|
+
"typescript": "spacing[4]",
|
|
62
|
+
"value": "4px",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"less": "@spacing-8",
|
|
66
|
+
"name": "spacing.8",
|
|
67
|
+
"private": false,
|
|
68
|
+
"scss": "$spacing-8",
|
|
69
|
+
"typescript": "spacing[8]",
|
|
70
|
+
"value": "8px",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"less": "@root-radius-xs",
|
|
74
|
+
"name": "root.radius.xs",
|
|
75
|
+
"private": true,
|
|
76
|
+
"scss": "$root-radius-xs",
|
|
77
|
+
"typescript": "root.radius.xs",
|
|
78
|
+
"value": 2,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"less": "@color-text-primary",
|
|
82
|
+
"name": "color.text.primary",
|
|
83
|
+
"private": false,
|
|
84
|
+
"scss": "$color-text-primary",
|
|
85
|
+
"typescript": "color.text.primary",
|
|
86
|
+
"value": "#111111",
|
|
87
|
+
},
|
|
88
|
+
]
|
|
89
|
+
`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('resolves react-native values using VALUES_RN map', async () => {
|
|
93
|
+
const { getTokensData } = await import('../get-tokens-data');
|
|
94
|
+
const data = getTokensData();
|
|
95
|
+
|
|
96
|
+
expect(data['react-native']).toMatchInlineSnapshot(`
|
|
97
|
+
[
|
|
98
|
+
{
|
|
99
|
+
"less": "@spacing-4",
|
|
100
|
+
"name": "spacing.4",
|
|
101
|
+
"private": false,
|
|
102
|
+
"scss": "$spacing-4",
|
|
103
|
+
"typescript": "spacing[4]",
|
|
104
|
+
"value": 4,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"less": "@spacing-8",
|
|
108
|
+
"name": "spacing.8",
|
|
109
|
+
"private": false,
|
|
110
|
+
"scss": "$spacing-8",
|
|
111
|
+
"typescript": "spacing[8]",
|
|
112
|
+
"value": 8,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"less": "@root-radius-xs",
|
|
116
|
+
"name": "root.radius.xs",
|
|
117
|
+
"private": true,
|
|
118
|
+
"scss": "$root-radius-xs",
|
|
119
|
+
"typescript": "root.radius.xs",
|
|
120
|
+
"value": 2,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"less": "@color-text-primary",
|
|
124
|
+
"name": "color.text.primary",
|
|
125
|
+
"private": false,
|
|
126
|
+
"scss": "$color-text-primary",
|
|
127
|
+
"typescript": "color.text.primary",
|
|
128
|
+
"value": "#101010",
|
|
129
|
+
},
|
|
130
|
+
]
|
|
131
|
+
`);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('throws when token value cannot be resolved', async () => {
|
|
135
|
+
vi.doMock('@preply/ds-core', () => ({
|
|
136
|
+
isToken: (value: unknown) => Array.isArray(value),
|
|
137
|
+
tokens: {
|
|
138
|
+
spacing: {
|
|
139
|
+
'16': ['base', 'missing', false],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
const { getTokensData } = await import('../get-tokens-data');
|
|
145
|
+
|
|
146
|
+
expect(() => getTokensData()).toThrow('Token value not found for token id: missing');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { isToken, tokens } from '@preply/ds-core';
|
|
2
|
+
import { VALUES_RN, VALUES_WEB } from '@preply/ds-theme-tokyo-ui';
|
|
3
|
+
import { ThemeTokenValue, Token, TOKEN_ID_FIELD } from '@preply/ds-core-types';
|
|
4
|
+
import { Platform } from '../../../types';
|
|
5
|
+
|
|
6
|
+
const PRIVATE_TOKEN_PATTERNS = ['dsInternalPrimitive', 'root'];
|
|
7
|
+
|
|
8
|
+
/** @example formatScssName('dropShadow.1.shorthand') // '$drop-shadow-1-shorthand' */
|
|
9
|
+
const formatScssName = (name: string) => '$' + name.replace(/\./g, '-');
|
|
10
|
+
/** @example formatLessName('dropShadow.1.shorthand') // '@drop-shadow-1-shorthand' */
|
|
11
|
+
const formatLessName = (name: string) => '@' + name.replace(/\./g, '-');
|
|
12
|
+
/** @example formatTypescriptName('dropShadow.1.shorthand') // 'dropShadow[1].shorthand' */
|
|
13
|
+
const formatTypescriptName = (name: string) =>
|
|
14
|
+
name
|
|
15
|
+
.split('.')
|
|
16
|
+
.map(part => {
|
|
17
|
+
const isNumeric = !isNaN(Number(part));
|
|
18
|
+
const hasHyphen = part.includes('-');
|
|
19
|
+
if (isNumeric || hasHyphen) return `[${part}]`;
|
|
20
|
+
return part;
|
|
21
|
+
})
|
|
22
|
+
.join('.')
|
|
23
|
+
.replace(/\.\[/g, '[');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolves a token's numeric ID to its concrete theme value.
|
|
27
|
+
* Theme values are keyed as `t{id}` and can be:
|
|
28
|
+
* - a primitive (string | number) -> returned directly
|
|
29
|
+
* - an object with a `.base` key -> `.base` is the canonical value (responsive tokens)
|
|
30
|
+
* - an object without `.base` -> first value is used as fallback (e.g. platform-specific)
|
|
31
|
+
*/
|
|
32
|
+
function getTokenValue(token: Token, values: Record<string, ThemeTokenValue>): string | number {
|
|
33
|
+
const tokenId = token[TOKEN_ID_FIELD];
|
|
34
|
+
const value = values[`t${tokenId}`];
|
|
35
|
+
if (value === undefined) {
|
|
36
|
+
throw new Error(`Token value not found for token id: ${tokenId}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof value === 'object') {
|
|
40
|
+
const base = (value as Record<string, string | number>).base;
|
|
41
|
+
if (base !== undefined) return base;
|
|
42
|
+
|
|
43
|
+
const firstValue = Object.values(value)[0];
|
|
44
|
+
if (firstValue !== undefined) return firstValue as string | number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return value as string | number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Recursively walks the nested token tree, collecting leaf tokens (identified by `isToken`) into a flat array. */
|
|
51
|
+
function flattenTokens(
|
|
52
|
+
input: unknown,
|
|
53
|
+
values: Record<string, ThemeTokenValue>,
|
|
54
|
+
path: string[] = [],
|
|
55
|
+
output: TokenData[] = [],
|
|
56
|
+
): TokenData[] {
|
|
57
|
+
if (isToken(input)) {
|
|
58
|
+
const name = path.join('.');
|
|
59
|
+
const value = getTokenValue(input, values);
|
|
60
|
+
output.push({
|
|
61
|
+
name,
|
|
62
|
+
scss: formatScssName(name),
|
|
63
|
+
less: formatLessName(name),
|
|
64
|
+
typescript: formatTypescriptName(name),
|
|
65
|
+
value,
|
|
66
|
+
private: PRIVATE_TOKEN_PATTERNS.some(pattern => name.startsWith(pattern)),
|
|
67
|
+
});
|
|
68
|
+
return output;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!input || typeof input !== 'object') {
|
|
72
|
+
return output;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
|
|
76
|
+
flattenTokens(value, values, [...path, key], output);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return output;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type TokenData = {
|
|
83
|
+
name: string;
|
|
84
|
+
scss: string;
|
|
85
|
+
less: string;
|
|
86
|
+
typescript: string;
|
|
87
|
+
value: string | number;
|
|
88
|
+
private: boolean;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
type PlatformTokens = Record<Platform, TokenData[]>;
|
|
92
|
+
|
|
93
|
+
export function getTokensData(): PlatformTokens {
|
|
94
|
+
const valuesByPlatform: Record<Platform, Record<string, ThemeTokenValue>> = {
|
|
95
|
+
web: VALUES_WEB as Record<string, ThemeTokenValue>,
|
|
96
|
+
'react-native': VALUES_RN as Record<string, ThemeTokenValue>,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
web: flattenTokens(tokens, valuesByPlatform.web),
|
|
101
|
+
'react-native': flattenTokens(tokens, valuesByPlatform['react-native']),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createSearchIndex } from '../../utils/create-search-index';
|
|
2
|
+
import { PLATFORMS } from '../../types';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { defineTool } from '../../utils/define-tool';
|
|
5
|
+
import * as z from 'zod';
|
|
6
|
+
import { stringify } from 'yaml';
|
|
7
|
+
import { formatList } from '../../utils/format-list';
|
|
8
|
+
import tokens from './context';
|
|
9
|
+
import { Format, formatToken, getExample } from './token-utils';
|
|
10
|
+
|
|
11
|
+
export type { Format };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Normalizes token name to the unified format, so queries in all formats will match the same token:
|
|
15
|
+
* - dropShadow.1.shorthand
|
|
16
|
+
* - $dropShadow-1-shorthand
|
|
17
|
+
* - @dropShadow-1-shorthand
|
|
18
|
+
* - dropShadow[1].shorthand
|
|
19
|
+
*/
|
|
20
|
+
const normalize = (term: string) =>
|
|
21
|
+
term
|
|
22
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
23
|
+
.trim()
|
|
24
|
+
.toLowerCase();
|
|
25
|
+
|
|
26
|
+
const indexes = {
|
|
27
|
+
web: createSearchIndex(tokens.web, { ref: 'name', normalize }),
|
|
28
|
+
'react-native': createSearchIndex(tokens['react-native'], { ref: 'name', normalize }),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const searchTokenByName = defineTool({
|
|
32
|
+
name: 'search_token_by_name',
|
|
33
|
+
description: 'Search for a design system style tokens by name',
|
|
34
|
+
arguments: z.object({
|
|
35
|
+
name: z.string().describe(
|
|
36
|
+
dedent`
|
|
37
|
+
The token name to search for. This can be in any format:
|
|
38
|
+
- raw token name (e.g. dropShadow.1.shorthand)
|
|
39
|
+
- scss (e.g. $dropShadow-1-shorthand)
|
|
40
|
+
- less (e.g. @dropShadow-1-shorthand)
|
|
41
|
+
- typescript (e.g. dropShadow[1].shorthand)
|
|
42
|
+
`,
|
|
43
|
+
),
|
|
44
|
+
}),
|
|
45
|
+
options: z.object({
|
|
46
|
+
platform: z.enum(PLATFORMS).describe('Target platform for token values. Required.'),
|
|
47
|
+
format: z.enum(['scss', 'less', 'typescript']).optional().default('scss').describe(dedent`
|
|
48
|
+
Optional output format for web platform only. Defaults to "scss".
|
|
49
|
+
- "scss" - SCSS variable (e.g. $drop-shadow-1-shorthand)
|
|
50
|
+
- "less" - Less variable (e.g. @drop-shadow-1-shorthand)
|
|
51
|
+
- "typescript" - Typescript variable for usage in styled-components (e.g. dropShadow[1].shorthand)
|
|
52
|
+
|
|
53
|
+
For "react-native" platform this option is ignored.
|
|
54
|
+
`),
|
|
55
|
+
includeExample: z
|
|
56
|
+
.boolean()
|
|
57
|
+
.default(true)
|
|
58
|
+
.describe('Whether to include an usage example. Default: true'),
|
|
59
|
+
includePrivate: z.boolean().default(false).describe(dedent`
|
|
60
|
+
Whether to include private tokens. Default: false
|
|
61
|
+
|
|
62
|
+
Private tokens starts from "dsInternalPrimitive" or "root" prefixes.
|
|
63
|
+
They're not intended for public use, however they're still available in the system.
|
|
64
|
+
Only use this option if you wasn't able to find public token or you're confident that private token is what you need.
|
|
65
|
+
`),
|
|
66
|
+
}),
|
|
67
|
+
callback: ({ name, platform, format = 'scss', includeExample, includePrivate }) => {
|
|
68
|
+
const outputFormat: Format = platform === 'web' ? format : 'scss';
|
|
69
|
+
const matches = indexes[platform]
|
|
70
|
+
.search(name, 10)
|
|
71
|
+
.filter(m => includePrivate || !m.item.private);
|
|
72
|
+
|
|
73
|
+
if (!matches.length) {
|
|
74
|
+
return [`No results found for "${name}"`];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const [first, ...rest] = matches;
|
|
78
|
+
|
|
79
|
+
const examples = includeExample
|
|
80
|
+
? [
|
|
81
|
+
platform === 'web'
|
|
82
|
+
? `Here is an example of how to use design-system tokens in ${outputFormat}:`
|
|
83
|
+
: 'Here is an example of how to use design-system tokens in react-native:',
|
|
84
|
+
getExample(platform, outputFormat),
|
|
85
|
+
]
|
|
86
|
+
: [];
|
|
87
|
+
|
|
88
|
+
if (!first.exact) {
|
|
89
|
+
return [
|
|
90
|
+
`There is no exact match for "${name}", but here are some similar tokens:`,
|
|
91
|
+
formatList(matches.map(m => formatToken(m.item, platform, outputFormat))),
|
|
92
|
+
...examples,
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const similarMessages = rest.length
|
|
97
|
+
? [
|
|
98
|
+
'Also, here you can find some tokens with similar names:',
|
|
99
|
+
formatList(rest.map(m => formatToken(m.item, platform, outputFormat))),
|
|
100
|
+
]
|
|
101
|
+
: [];
|
|
102
|
+
|
|
103
|
+
return [
|
|
104
|
+
`Found exact match for "${name}":`,
|
|
105
|
+
stringify(formatToken(first.item, platform, outputFormat)).trim(),
|
|
106
|
+
...examples,
|
|
107
|
+
...similarMessages,
|
|
108
|
+
];
|
|
109
|
+
},
|
|
110
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Build-time macro: getTokensData() runs at compile time and its return value is inlined into the bundle
|
|
2
|
+
import { TokenData, getTokensData } from './context/get-tokens-data' with { type: 'macro' };
|
|
3
|
+
import { PLATFORMS, Platform } from '../../types';
|
|
4
|
+
import dedent from 'dedent';
|
|
5
|
+
import { defineTool } from '../../utils/define-tool';
|
|
6
|
+
import * as z from 'zod';
|
|
7
|
+
import { formatList } from '../../utils/format-list';
|
|
8
|
+
import { Format, formatToken, getExample } from './token-utils';
|
|
9
|
+
|
|
10
|
+
const allTokens = getTokensData();
|
|
11
|
+
|
|
12
|
+
function buildValueMap(tokens: TokenData[]): Record<string, TokenData[]> {
|
|
13
|
+
const map: Record<string, TokenData[]> = {};
|
|
14
|
+
for (const token of tokens) {
|
|
15
|
+
const key = token.value.toString().toLowerCase();
|
|
16
|
+
map[key] = map[key] ?? [];
|
|
17
|
+
map[key].push(token);
|
|
18
|
+
}
|
|
19
|
+
return map;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const valueMaps = {
|
|
23
|
+
web: buildValueMap(allTokens.web),
|
|
24
|
+
'react-native': buildValueMap(allTokens['react-native']),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function searchByValue(platform: Platform, value: string | number, includePrivate = false) {
|
|
28
|
+
const map = valueMaps[platform];
|
|
29
|
+
const normalizedValue = value.toString().toLowerCase();
|
|
30
|
+
let results = map[normalizedValue] ?? [];
|
|
31
|
+
|
|
32
|
+
if (!isNaN(Number(value))) {
|
|
33
|
+
const pxResults = map[`${normalizedValue}px`] ?? [];
|
|
34
|
+
const emResults = map[`${normalizedValue}em`] ?? [];
|
|
35
|
+
results = [...results, ...pxResults, ...emResults];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!includePrivate) {
|
|
39
|
+
results = results.filter(t => !t.private);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return [...new Set(results)];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const searchTokenByValue = defineTool({
|
|
46
|
+
name: 'search_token_by_value',
|
|
47
|
+
description: 'Search for a design system style tokens by its value',
|
|
48
|
+
arguments: z.object({
|
|
49
|
+
value: z.string().describe(
|
|
50
|
+
dedent`
|
|
51
|
+
The token value to search for. This can be:
|
|
52
|
+
- numeric values (e.g. 500)
|
|
53
|
+
- colors (e.g. #000000 or rgba(0, 0, 0, 0.5))
|
|
54
|
+
- sizes (e.g. 16px or 2em)
|
|
55
|
+
- fonts (e.g. Inter or Arial)
|
|
56
|
+
- etc.
|
|
57
|
+
`,
|
|
58
|
+
),
|
|
59
|
+
}),
|
|
60
|
+
options: z.object({
|
|
61
|
+
platform: z.enum(PLATFORMS).describe('Target platform for token values. Required.'),
|
|
62
|
+
format: z.enum(['scss', 'less', 'typescript']).optional().default('scss').describe(dedent`
|
|
63
|
+
Optional output format for web platform only. Defaults to "scss".
|
|
64
|
+
- "scss" - SCSS variable (e.g. $drop-shadow-1-shorthand)
|
|
65
|
+
- "less" - Less variable (e.g. @drop-shadow-1-shorthand)
|
|
66
|
+
- "typescript" - Typescript variable for usage in styled-components (e.g. dropShadow[1].shorthand)
|
|
67
|
+
|
|
68
|
+
For "react-native" platform this option is ignored.
|
|
69
|
+
`),
|
|
70
|
+
includeExample: z
|
|
71
|
+
.boolean()
|
|
72
|
+
.default(true)
|
|
73
|
+
.describe('Whether to include an usage example. Default: true'),
|
|
74
|
+
includePrivate: z.boolean().default(false).describe(dedent`
|
|
75
|
+
Whether to include private tokens. Default: false
|
|
76
|
+
|
|
77
|
+
Private tokens starts from "dsInternalPrimitive" or "root" prefixes.
|
|
78
|
+
They're not intended for public use, however they're still available in the system.
|
|
79
|
+
Only use this option if you wasn't able to find public token or you're confident that private token is what you need.
|
|
80
|
+
`),
|
|
81
|
+
}),
|
|
82
|
+
callback: ({ value, platform, format = 'scss', includeExample, includePrivate }) => {
|
|
83
|
+
const outputFormat: Format = platform === 'web' ? format : 'scss';
|
|
84
|
+
const results = searchByValue(platform, value, includePrivate);
|
|
85
|
+
|
|
86
|
+
if (!results.length) {
|
|
87
|
+
return [`No results found for "${value}"`];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const formattedResults = results.map(r => formatToken(r, platform, outputFormat));
|
|
91
|
+
|
|
92
|
+
const examples = includeExample
|
|
93
|
+
? [
|
|
94
|
+
platform === 'web'
|
|
95
|
+
? `Here is an example of how to use design-system tokens in ${outputFormat}:`
|
|
96
|
+
: 'Here is an example of how to use design-system tokens in react-native:',
|
|
97
|
+
getExample(platform, outputFormat),
|
|
98
|
+
]
|
|
99
|
+
: [];
|
|
100
|
+
|
|
101
|
+
return [
|
|
102
|
+
`Found ${results.length} tokens with value "${value}"`,
|
|
103
|
+
formatList(formattedResults),
|
|
104
|
+
...examples,
|
|
105
|
+
];
|
|
106
|
+
},
|
|
107
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { TokenData } from './context/get-tokens-data';
|
|
2
|
+
import { Platform } from '../../types';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
|
|
5
|
+
export type Format = 'scss' | 'less' | 'typescript';
|
|
6
|
+
|
|
7
|
+
export function formatToken(token: TokenData, platform: Platform, format: Format) {
|
|
8
|
+
return {
|
|
9
|
+
token: platform === 'web' ? token[format] : token.typescript,
|
|
10
|
+
value: token.value,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const webExamples: Record<Format, string> = {
|
|
15
|
+
scss: dedent`
|
|
16
|
+
@use '@preply/ds-web-core/dist/generated/tokens' as *;
|
|
17
|
+
|
|
18
|
+
.MyComponent {
|
|
19
|
+
box-shadow: $dropShadow-1-shorthand;
|
|
20
|
+
}
|
|
21
|
+
`,
|
|
22
|
+
less: dedent`
|
|
23
|
+
@import '~@preply/ds-web-core/dist/generated/tokens.less';
|
|
24
|
+
|
|
25
|
+
.MyComponent {
|
|
26
|
+
box-shadow: @dropShadow-1-shorthand;
|
|
27
|
+
}
|
|
28
|
+
`,
|
|
29
|
+
typescript: dedent`
|
|
30
|
+
import styled from 'styled-components';
|
|
31
|
+
import { dropShadow } from '@preply/ds-core';
|
|
32
|
+
import { getTokenVar } from '@preply/ds-web-core';
|
|
33
|
+
|
|
34
|
+
const MyComponent = styled.div\`
|
|
35
|
+
box-shadow: \${getTokenVar(dropShadow[1].shorthand)};
|
|
36
|
+
\`;
|
|
37
|
+
`,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const reactNativeExample = dedent`
|
|
41
|
+
import { View } from 'react-native';
|
|
42
|
+
import { color } from '@preply/ds-core';
|
|
43
|
+
import { createStyleSheet, useStyleSheet } from '@preply/ds-rn-core';
|
|
44
|
+
|
|
45
|
+
const rawStyles = createStyleSheet({
|
|
46
|
+
container: {
|
|
47
|
+
backgroundColor: color.background.primary,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const MyComponent = () => {
|
|
52
|
+
const styles = useStyleSheet(rawStyles);
|
|
53
|
+
|
|
54
|
+
return <View style={styles.container} />;
|
|
55
|
+
};
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
export function getExample(platform: Platform, format: Format) {
|
|
59
|
+
return platform === 'web' ? webExamples[format] : reactNativeExample;
|
|
60
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* eslint-disable import/no-named-as-default-member */
|
|
2
|
+
import lunr from 'lunr';
|
|
3
|
+
|
|
4
|
+
// Lunr requires globally unique names for registered pipeline functions.
|
|
5
|
+
// This counter ensures each createSearchIndex call gets a distinct function name.
|
|
6
|
+
let pipelineCounter = 0;
|
|
7
|
+
|
|
8
|
+
type Result<T> = {
|
|
9
|
+
item: T;
|
|
10
|
+
/** Whether the query matched a ref exactly (as opposed to a fuzzy/partial match). */
|
|
11
|
+
exact: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extracts only the keys of T whose values are strings.
|
|
16
|
+
* Used to constrain `ref` so it always points to a string field.
|
|
17
|
+
*/
|
|
18
|
+
type StringKeys<T extends Record<string, unknown>, K = keyof T> = K extends string
|
|
19
|
+
? T[K] extends string
|
|
20
|
+
? K
|
|
21
|
+
: never
|
|
22
|
+
: never;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Builds a lunr full-text search index over `items` and returns a thin wrapper
|
|
26
|
+
* with `search` (ranked results) and `get` (exact lookup) methods.
|
|
27
|
+
*
|
|
28
|
+
* @param items - The dataset to index.
|
|
29
|
+
* @param ref - Which string field to use as the unique document key.
|
|
30
|
+
* @param normalize - Optional transform applied to both stored refs and incoming
|
|
31
|
+
* queries so that e.g. case or naming-convention differences
|
|
32
|
+
* don't prevent matches. When provided, a custom pipeline
|
|
33
|
+
* function is also registered to split compound tokens.
|
|
34
|
+
*/
|
|
35
|
+
export function createSearchIndex<T extends Record<string, unknown>>(
|
|
36
|
+
/** The dataset to index. */
|
|
37
|
+
items: T[],
|
|
38
|
+
options: {
|
|
39
|
+
/** The field to use as the unique document key. */
|
|
40
|
+
ref: StringKeys<T>;
|
|
41
|
+
/** An optional transform applied to both stored refs and incoming queries */
|
|
42
|
+
normalize?: (term: string) => string;
|
|
43
|
+
},
|
|
44
|
+
) {
|
|
45
|
+
const { ref, normalize = s => s } = options;
|
|
46
|
+
|
|
47
|
+
const itemsByRef = new Map<string, T>();
|
|
48
|
+
for (const item of items) {
|
|
49
|
+
itemsByRef.set(normalize(item[ref] as string), item);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// When a normalize function is provided we also register a custom lunr
|
|
53
|
+
// pipeline step that splits compound tokens before stemming.
|
|
54
|
+
let pipelineFn: lunr.PipelineFunction | undefined;
|
|
55
|
+
if (options.normalize) {
|
|
56
|
+
const fnName = `createSearchIndex_normalize_${pipelineCounter++}`;
|
|
57
|
+
pipelineFn = (token: lunr.Token) => {
|
|
58
|
+
const tokenParts = token
|
|
59
|
+
.toString()
|
|
60
|
+
.replace(/[^a-zA-Z0-9]/g, ' ')
|
|
61
|
+
.trim()
|
|
62
|
+
.split(/\s+/);
|
|
63
|
+
|
|
64
|
+
return tokenParts.map(part => token.clone().update(() => part));
|
|
65
|
+
};
|
|
66
|
+
// Pipeline functions must be registered globally before they can be
|
|
67
|
+
// added to an index pipeline; the unique fnName avoids collisions.
|
|
68
|
+
lunr.Pipeline.registerFunction(pipelineFn, fnName);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Build the lunr index. `__ref` is an internal field that holds the
|
|
72
|
+
// normalized ref — we use a synthetic name to avoid clashing with any
|
|
73
|
+
// field already present on the items.
|
|
74
|
+
const index = lunr(function () {
|
|
75
|
+
this.ref('__ref');
|
|
76
|
+
this.field('__ref');
|
|
77
|
+
|
|
78
|
+
if (pipelineFn) {
|
|
79
|
+
this.pipeline.before(lunr.stemmer, pipelineFn);
|
|
80
|
+
this.searchPipeline.before(lunr.stemmer, pipelineFn);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const item of items) {
|
|
84
|
+
this.add({
|
|
85
|
+
...item,
|
|
86
|
+
__ref: normalize(item[ref] as string),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
/**
|
|
93
|
+
* Performs a full-text search over the indexed items.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```ts
|
|
97
|
+
* const results = index.search('dropShadow-1-shorthand');
|
|
98
|
+
* console.log(results);
|
|
99
|
+
* // [
|
|
100
|
+
* // { item: { name: 'dropShadow-1-shorthand' }, exact: true },
|
|
101
|
+
* // { item: { name: 'dropShadow-1-shorthand' }, exact: false },
|
|
102
|
+
* // ]
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
search(query: string, limit?: number): Result<T>[] {
|
|
106
|
+
const results = index.search(normalize(query)).map(result => {
|
|
107
|
+
const item = itemsByRef.get(result.ref)!;
|
|
108
|
+
return {
|
|
109
|
+
item,
|
|
110
|
+
exact: result.ref === normalize(query),
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return limit ? results.slice(0, limit) : results;
|
|
115
|
+
},
|
|
116
|
+
/** Direct lookup by normalized ref value. Returns undefined if not found. */
|
|
117
|
+
get(query: string): T | undefined {
|
|
118
|
+
return itemsByRef.get(normalize(query));
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|