@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,117 @@
|
|
|
1
|
+
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
2
|
+
import { PostHog } from 'posthog-node';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { userInfo } from 'os';
|
|
6
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
7
|
+
import envPaths from 'env-paths';
|
|
8
|
+
|
|
9
|
+
const DATA_PATH = envPaths(pkg.name).data;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns persistent client ID or null if it failed to generate.
|
|
13
|
+
* Can be used to identify a client across sessions when user name is not available.
|
|
14
|
+
*/
|
|
15
|
+
function getUserId() {
|
|
16
|
+
try {
|
|
17
|
+
// Try to read client ID from file or generate a new one
|
|
18
|
+
const clientIdPath = path.join(DATA_PATH, 'client-id.txt');
|
|
19
|
+
try {
|
|
20
|
+
return fs.readFileSync(clientIdPath, 'utf8');
|
|
21
|
+
} catch {
|
|
22
|
+
const clientId = crypto.randomUUID();
|
|
23
|
+
if (!fs.existsSync(DATA_PATH)) {
|
|
24
|
+
fs.mkdirSync(DATA_PATH, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
fs.writeFileSync(clientIdPath, clientId, 'utf8');
|
|
27
|
+
return clientId;
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
return crypto.randomUUID();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns current user name or null if it's not available.
|
|
36
|
+
*/
|
|
37
|
+
function getUserName() {
|
|
38
|
+
try {
|
|
39
|
+
const userName = userInfo().username;
|
|
40
|
+
return userName;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const client = new PostHog('phc_idhNqodmRjwHSolig26UgSRsgb6pIyYRIvIBzgNAkGZ', {
|
|
47
|
+
host: 'https://eu.i.posthog.com',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Identifies the current user.
|
|
52
|
+
* Should be called once at the start of the program.
|
|
53
|
+
*/
|
|
54
|
+
export function identify() {
|
|
55
|
+
try {
|
|
56
|
+
const userId = getUserId();
|
|
57
|
+
const userName = getUserName();
|
|
58
|
+
const data = {
|
|
59
|
+
distinctId: userId,
|
|
60
|
+
properties: {
|
|
61
|
+
userName,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
client.identify(data);
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Tracks a generic event.
|
|
72
|
+
*/
|
|
73
|
+
export function trackEvent(eventName: string, properties?: Record<string, unknown>) {
|
|
74
|
+
try {
|
|
75
|
+
const userId = getUserId();
|
|
76
|
+
const data = {
|
|
77
|
+
distinctId: userId,
|
|
78
|
+
event: eventName,
|
|
79
|
+
properties: {
|
|
80
|
+
...properties,
|
|
81
|
+
mcpVersion: pkg.version,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
client.capture(data);
|
|
85
|
+
} catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Tracks a tool call.
|
|
92
|
+
* Should be called for each tool call.
|
|
93
|
+
*/
|
|
94
|
+
export function trackToolCall(
|
|
95
|
+
toolName: string,
|
|
96
|
+
args: Record<string, unknown>,
|
|
97
|
+
meta?: Record<string, unknown>,
|
|
98
|
+
) {
|
|
99
|
+
trackEvent('tool_called', {
|
|
100
|
+
toolName,
|
|
101
|
+
arguments: args,
|
|
102
|
+
meta,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Flushes all pending events and shuts down the PostHog client.
|
|
108
|
+
* Must be called before process exit in short-lived processes (e.g. CLI)
|
|
109
|
+
* to ensure events are actually sent over the network.
|
|
110
|
+
*/
|
|
111
|
+
export async function shutdownEventTracking() {
|
|
112
|
+
try {
|
|
113
|
+
await client.shutdown();
|
|
114
|
+
} catch {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getComponentDocs } from '../get-component-docs';
|
|
3
|
+
|
|
4
|
+
vi.mock(import('../context/get-components-data'), () => ({
|
|
5
|
+
getComponentsData: () => ({
|
|
6
|
+
web: {
|
|
7
|
+
Button: {
|
|
8
|
+
meta: {
|
|
9
|
+
name: 'Button',
|
|
10
|
+
import: "import { Button } from '@preply/ds-web-lib';",
|
|
11
|
+
description: 'Button component',
|
|
12
|
+
deprecated: false,
|
|
13
|
+
},
|
|
14
|
+
docs: 'Button component docs',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
'react-native': {
|
|
18
|
+
Button: {
|
|
19
|
+
meta: {
|
|
20
|
+
name: 'Button',
|
|
21
|
+
import: "import { Button } from '@preply/ds-rn-lib';",
|
|
22
|
+
description: 'Button component for rn',
|
|
23
|
+
deprecated: false,
|
|
24
|
+
},
|
|
25
|
+
docs: 'Button component docs for rn',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
}),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
describe('getComponentDocs tool', () => {
|
|
32
|
+
it('returns docs for existing web component', () => {
|
|
33
|
+
expect(getComponentDocs.callback({ name: 'Button', platform: 'web' }))
|
|
34
|
+
.toMatchInlineSnapshot(`
|
|
35
|
+
[
|
|
36
|
+
"Button component docs",
|
|
37
|
+
]
|
|
38
|
+
`);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns docs for existing react-native component', () => {
|
|
42
|
+
expect(getComponentDocs.callback({ name: 'Button', platform: 'react-native' }))
|
|
43
|
+
.toMatchInlineSnapshot(`
|
|
44
|
+
[
|
|
45
|
+
"Button component docs for rn",
|
|
46
|
+
]
|
|
47
|
+
`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns error when component is not found', () => {
|
|
51
|
+
expect(getComponentDocs.callback({ name: 'Missing', platform: 'web' }))
|
|
52
|
+
.toMatchInlineSnapshot(`
|
|
53
|
+
[
|
|
54
|
+
"Component "Missing" not found",
|
|
55
|
+
]
|
|
56
|
+
`);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { listComponents } from '../list-components';
|
|
3
|
+
|
|
4
|
+
vi.mock(import('../context/get-components-data'), () => ({
|
|
5
|
+
getComponentsData: () => ({
|
|
6
|
+
web: {
|
|
7
|
+
Button: {
|
|
8
|
+
meta: {
|
|
9
|
+
name: 'Button',
|
|
10
|
+
import: "import { Button } from '@preply/ds-web-lib';",
|
|
11
|
+
description: 'Button component',
|
|
12
|
+
},
|
|
13
|
+
docs: 'Button component docs',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
'react-native': {
|
|
17
|
+
Button: {
|
|
18
|
+
meta: {
|
|
19
|
+
name: 'Button',
|
|
20
|
+
import: "import { Button } from '@preply/ds-rn-lib';",
|
|
21
|
+
description: 'Button component for rn',
|
|
22
|
+
},
|
|
23
|
+
docs: 'Button component docs for rn',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
describe('listComponents tool', () => {
|
|
30
|
+
it('returns component list for web platform', () => {
|
|
31
|
+
expect(listComponents.callback({ platform: 'web' })).toMatchInlineSnapshot(`
|
|
32
|
+
[
|
|
33
|
+
"Each item has the following fields:
|
|
34
|
+
- name - The name of the component (e.g. Button)
|
|
35
|
+
- import - The import path of the component (e.g. "import { Button } from '@preply/ds-web-lib';")
|
|
36
|
+
- description - The description of the component
|
|
37
|
+
- deprecated - Whether the component is deprecated (false if missing)",
|
|
38
|
+
"---
|
|
39
|
+
name: Button
|
|
40
|
+
import: import { Button } from '@preply/ds-web-lib';
|
|
41
|
+
description: Button component
|
|
42
|
+
---",
|
|
43
|
+
]
|
|
44
|
+
`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('returns component list for react-native platform', () => {
|
|
48
|
+
expect(listComponents.callback({ platform: 'react-native' })).toMatchInlineSnapshot(`
|
|
49
|
+
[
|
|
50
|
+
"Each item has the following fields:
|
|
51
|
+
- name - The name of the component (e.g. Button)
|
|
52
|
+
- import - The import path of the component (e.g. "import { Button } from '@preply/ds-rn-lib';")
|
|
53
|
+
- description - The description of the component
|
|
54
|
+
- deprecated - Whether the component is deprecated (false if missing)",
|
|
55
|
+
"---
|
|
56
|
+
name: Button
|
|
57
|
+
import: import { Button } from '@preply/ds-rn-lib';
|
|
58
|
+
description: Button component for rn
|
|
59
|
+
---",
|
|
60
|
+
]
|
|
61
|
+
`);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { buildComponents } from '../get-components-data';
|
|
4
|
+
|
|
5
|
+
describe('getComponentsData', () => {
|
|
6
|
+
it('builds docs for a real fixture entrypoint', () => {
|
|
7
|
+
const components = buildComponents([
|
|
8
|
+
{
|
|
9
|
+
importPath: '@preply/ds-web-lib/fixtures',
|
|
10
|
+
entrypoint: path.resolve(import.meta.dirname, 'test-components/index.ts'),
|
|
11
|
+
},
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
expect(components).toStrictEqual({
|
|
15
|
+
FixtureButton: {
|
|
16
|
+
meta: {
|
|
17
|
+
name: 'FixtureButton',
|
|
18
|
+
import: "import { FixtureButton } from '@preply/ds-web-lib/fixtures';",
|
|
19
|
+
description: 'Lightweight fixture component used in docs generation tests.',
|
|
20
|
+
},
|
|
21
|
+
docs: expect.any(String),
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(components.FixtureButton.docs).toMatchInlineSnapshot(`
|
|
26
|
+
"# FixtureButton
|
|
27
|
+
|
|
28
|
+
\`\`\`
|
|
29
|
+
import { FixtureButton } from '@preply/ds-web-lib/fixtures';
|
|
30
|
+
\`\`\`
|
|
31
|
+
|
|
32
|
+
Lightweight fixture component used in docs generation tests.
|
|
33
|
+
|
|
34
|
+
@example
|
|
35
|
+
|
|
36
|
+
\`\`\`
|
|
37
|
+
<FixtureButton label="Save" />
|
|
38
|
+
\`\`\`
|
|
39
|
+
|
|
40
|
+
## Props
|
|
41
|
+
|
|
42
|
+
### \`label\`
|
|
43
|
+
|
|
44
|
+
- Type: \`string\`
|
|
45
|
+
- Required
|
|
46
|
+
|
|
47
|
+
Visible button label
|
|
48
|
+
|
|
49
|
+
### \`disabled\`
|
|
50
|
+
|
|
51
|
+
- Type: \`boolean | undefined\`
|
|
52
|
+
- Default value: \`false\`
|
|
53
|
+
|
|
54
|
+
Disabled state"
|
|
55
|
+
`);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export type FixtureButtonProps = {
|
|
4
|
+
/** Visible button label */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Disabled state */
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Lightweight fixture component used in docs generation tests.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* <FixtureButton label="Save" />
|
|
15
|
+
*/
|
|
16
|
+
export const FixtureButton = ({ label, disabled = false }: FixtureButtonProps) => {
|
|
17
|
+
return <button disabled={disabled}>{label}</button>;
|
|
18
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FixtureButton } from './FixtureButton';
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/* eslint-disable import/no-named-as-default-member */
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { ComponentDoc, withCompilerOptions } from 'react-docgen-typescript';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Follows re-export alias chains to the original symbol declaration.
|
|
8
|
+
* Needed because entry files typically re-export components (e.g. `export { Button } from './Button'`),
|
|
9
|
+
* and react-docgen-typescript needs the real declaration to extract prop types.
|
|
10
|
+
*/
|
|
11
|
+
function resolveSymbol(symbol: ts.Symbol, checker: ts.TypeChecker): ts.Symbol {
|
|
12
|
+
while (symbol.flags & ts.SymbolFlags.Alias) {
|
|
13
|
+
const next = checker.getAliasedSymbol(symbol);
|
|
14
|
+
if (next === symbol) break;
|
|
15
|
+
symbol = next;
|
|
16
|
+
}
|
|
17
|
+
return symbol;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getDocForSymbol(program: ts.Program, symbol: ts.Symbol, aliasName: string) {
|
|
21
|
+
const decl = symbol.getDeclarations()?.[0];
|
|
22
|
+
if (!decl) return null;
|
|
23
|
+
|
|
24
|
+
const sourcePath = decl.getSourceFile().fileName;
|
|
25
|
+
|
|
26
|
+
const parser = withCompilerOptions(program.getCompilerOptions(), {
|
|
27
|
+
shouldExtractValuesFromUnion: true,
|
|
28
|
+
shouldIncludePropTagMap: true,
|
|
29
|
+
}).parseWithProgramProvider;
|
|
30
|
+
|
|
31
|
+
const docs = parser.call(null, sourcePath, () => program);
|
|
32
|
+
|
|
33
|
+
return docs.find(d => d.displayName === aliasName);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function extractDocgenInfo(entry: string, ignoreErrors: (string | RegExp)[] = []) {
|
|
37
|
+
console.info(`Extracting docgen info from ${entry}`);
|
|
38
|
+
|
|
39
|
+
const configPath = ts.findConfigFile(
|
|
40
|
+
path.dirname(path.resolve(entry)),
|
|
41
|
+
ts.sys.fileExists,
|
|
42
|
+
'tsconfig.json',
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (!configPath) {
|
|
46
|
+
throw new Error('No tsconfig.json found');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { config } = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
50
|
+
const { options, fileNames } = ts.parseJsonConfigFileContent(
|
|
51
|
+
config,
|
|
52
|
+
ts.sys,
|
|
53
|
+
path.dirname(configPath),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const program = ts.createProgram({ rootNames: fileNames, options });
|
|
57
|
+
const checker = program.getTypeChecker();
|
|
58
|
+
const source = program.getSourceFile(entry);
|
|
59
|
+
|
|
60
|
+
if (!source) {
|
|
61
|
+
throw new Error(`Cannot load: ${entry}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const moduleSymbol = checker.getSymbolAtLocation(source);
|
|
65
|
+
if (!moduleSymbol) {
|
|
66
|
+
throw new Error(`No symbol information for: ${entry}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const exportSymbols = checker.getExportsOfModule(moduleSymbol);
|
|
70
|
+
const docs: ComponentDoc[] = [];
|
|
71
|
+
|
|
72
|
+
console.info(`Parsing ${exportSymbols.length} exports`);
|
|
73
|
+
|
|
74
|
+
for (const exp of exportSymbols) {
|
|
75
|
+
const aliasName = exp.getName();
|
|
76
|
+
const realSym = resolveSymbol(exp, checker);
|
|
77
|
+
|
|
78
|
+
if ((realSym.flags & ts.SymbolFlags.Value) === 0) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const doc = getDocForSymbol(program, realSym, aliasName);
|
|
83
|
+
|
|
84
|
+
if (!doc) {
|
|
85
|
+
const isIgnored = ignoreErrors.some(i => {
|
|
86
|
+
if (typeof i === 'string' && i === aliasName) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (i instanceof RegExp && i.test(aliasName)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return false;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (!isIgnored) {
|
|
98
|
+
throw new Error(`Failed to parse component: ${aliasName}`);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
docs.push(doc);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.info(`Parsed ${docs.length} exports`);
|
|
106
|
+
|
|
107
|
+
return docs;
|
|
108
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { extractDocgenInfo } from './extract-component-docgen-info';
|
|
3
|
+
import { renderComponentDoc, renderImportExample } from './render-component-doc';
|
|
4
|
+
|
|
5
|
+
export type Entry = {
|
|
6
|
+
importPath: string;
|
|
7
|
+
entrypoint: string;
|
|
8
|
+
ignoreErrors?: (string | RegExp)[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ComponentMeta = {
|
|
12
|
+
name: string;
|
|
13
|
+
import: string;
|
|
14
|
+
description: string;
|
|
15
|
+
deprecated?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ComponentInfo = {
|
|
19
|
+
meta: ComponentMeta;
|
|
20
|
+
docs: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ComponentsMap = Record<string, ComponentInfo>;
|
|
24
|
+
|
|
25
|
+
type PlatformComponents = {
|
|
26
|
+
web: ComponentsMap;
|
|
27
|
+
'react-native': ComponentsMap;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function buildComponents(entries: Entry[]): ComponentsMap {
|
|
31
|
+
const components: ComponentsMap = {};
|
|
32
|
+
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const docgenInfos = extractDocgenInfo(entry.entrypoint, entry.ignoreErrors);
|
|
35
|
+
for (const docgenInfo of docgenInfos) {
|
|
36
|
+
const componentDocs = renderComponentDoc(docgenInfo, entry.importPath);
|
|
37
|
+
if (componentDocs) {
|
|
38
|
+
const { displayName, description, tags } = docgenInfo;
|
|
39
|
+
components[displayName] = {
|
|
40
|
+
meta: {
|
|
41
|
+
name: displayName,
|
|
42
|
+
import: renderImportExample(docgenInfo, entry.importPath),
|
|
43
|
+
description: description,
|
|
44
|
+
},
|
|
45
|
+
docs: componentDocs,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (tags?.deprecated) {
|
|
49
|
+
components[displayName].meta.deprecated = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return components;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const PACKAGES_DIR = path.resolve(import.meta.dirname, '../../../../../');
|
|
59
|
+
|
|
60
|
+
export function getComponentsData(): PlatformComponents {
|
|
61
|
+
return {
|
|
62
|
+
web: buildComponents([
|
|
63
|
+
{
|
|
64
|
+
importPath: '@preply/ds-web-lib',
|
|
65
|
+
entrypoint: path.resolve(PACKAGES_DIR, 'web-lib/src/index.ts'),
|
|
66
|
+
ignoreErrors: [/^IntlFormatted/],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
importPath: '@preply/ds-web-lib/components/deprecated',
|
|
70
|
+
entrypoint: path.resolve(
|
|
71
|
+
PACKAGES_DIR,
|
|
72
|
+
'web-lib/src/components/deprecated/index.tsx',
|
|
73
|
+
),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
importPath: '@preply/ds-web-root',
|
|
77
|
+
entrypoint: path.resolve(PACKAGES_DIR, 'web-root/src/index.ts'),
|
|
78
|
+
ignoreErrors: ['themes'],
|
|
79
|
+
},
|
|
80
|
+
]),
|
|
81
|
+
'react-native': buildComponents([
|
|
82
|
+
{
|
|
83
|
+
importPath: '@preply/ds-rn-lib',
|
|
84
|
+
entrypoint: path.resolve(PACKAGES_DIR, 'rn-lib/src/index.ts'),
|
|
85
|
+
ignoreErrors: [/^IntlFormatted/, 'DS_ICON_FONT_NAME'],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
importPath: '@preply/ds-rn-root',
|
|
89
|
+
entrypoint: path.resolve(PACKAGES_DIR, 'rn-root/src/index.ts'),
|
|
90
|
+
ignoreErrors: ['themes', 'useDisableAnimations'],
|
|
91
|
+
},
|
|
92
|
+
]),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ComponentDoc, PropItem } from 'react-docgen-typescript';
|
|
2
|
+
import { md } from '../../../utils/md';
|
|
3
|
+
|
|
4
|
+
export function renderComponentDoc(docgenInfo: ComponentDoc, importPath: string) {
|
|
5
|
+
const { displayName, description, tags, props } = docgenInfo;
|
|
6
|
+
|
|
7
|
+
return md.joinBlocks([
|
|
8
|
+
md.heading(displayName),
|
|
9
|
+
md.codeBlock(renderImportExample(docgenInfo, importPath)),
|
|
10
|
+
description,
|
|
11
|
+
renderTags(tags),
|
|
12
|
+
renderProps(props),
|
|
13
|
+
]);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function renderImportExample(docgenInfo: ComponentDoc, importPath: string) {
|
|
17
|
+
const { displayName } = docgenInfo;
|
|
18
|
+
return `import { ${displayName} } from '${importPath}';`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function renderTags(tags?: Record<string, string>, exclude: string[] = []) {
|
|
22
|
+
if (!tags) return '';
|
|
23
|
+
|
|
24
|
+
const blocks: string[] = [];
|
|
25
|
+
|
|
26
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
27
|
+
if (['default', ...exclude].includes(key)) continue;
|
|
28
|
+
|
|
29
|
+
blocks.push(`@${key}`);
|
|
30
|
+
|
|
31
|
+
if (key === 'example') {
|
|
32
|
+
blocks.push(md.codeBlock(value));
|
|
33
|
+
} else {
|
|
34
|
+
blocks.push(value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return md.joinBlocks(blocks);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderProps(props?: Record<string, PropItem>) {
|
|
42
|
+
if (!props || Object.keys(props).length === 0) return '';
|
|
43
|
+
|
|
44
|
+
const blocks = [md.heading('Props', { level: 2 })];
|
|
45
|
+
|
|
46
|
+
for (const prop of Object.values(props)) {
|
|
47
|
+
blocks.push(renderProp(prop));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return md.joinBlocks(blocks);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const IGNORE_UNWRAPPED_UNION_TYPES = ['ReactNode', 'boolean', 'boolean | undefined'];
|
|
54
|
+
|
|
55
|
+
function renderPropDataList(prop: PropItem) {
|
|
56
|
+
const list: (string | undefined)[] = [];
|
|
57
|
+
|
|
58
|
+
let type = prop.type.name;
|
|
59
|
+
let detailedType: string | undefined = undefined;
|
|
60
|
+
|
|
61
|
+
if (prop.type.name === 'enum') {
|
|
62
|
+
type = prop.type.raw ?? 'unknown';
|
|
63
|
+
|
|
64
|
+
const isIgnoreUnwrappedUnionType = IGNORE_UNWRAPPED_UNION_TYPES.includes(type);
|
|
65
|
+
const isDuplicatesUnwrappedUnionType = type.split(' | ').length === prop.type.value.length;
|
|
66
|
+
|
|
67
|
+
if (!isIgnoreUnwrappedUnionType && !isDuplicatesUnwrappedUnionType) {
|
|
68
|
+
detailedType = prop.type.value.map((v: { value: string }) => v.value).join(' | ');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
list.push(
|
|
73
|
+
`Type: ${md.code(type)}`,
|
|
74
|
+
detailedType && `Detailed type: ${md.code(detailedType)}`,
|
|
75
|
+
prop.defaultValue && `Default value: ${md.code(`${prop.defaultValue.value}` || '""')}`,
|
|
76
|
+
prop.required ? 'Required' : undefined,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return md.list(list.filter((item): item is string => item !== undefined && item !== null));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function renderProp(prop: PropItem) {
|
|
83
|
+
return md.joinBlocks([
|
|
84
|
+
md.heading(md.code(prop.name), { level: 3 }),
|
|
85
|
+
renderPropDataList(prop),
|
|
86
|
+
prop.description,
|
|
87
|
+
renderTags(prop.tags),
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PLATFORMS } from '../../types';
|
|
2
|
+
import { defineTool } from '../../utils/define-tool';
|
|
3
|
+
import data from './context';
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
|
|
6
|
+
export const getComponentDocs = defineTool({
|
|
7
|
+
name: 'get_component_docs',
|
|
8
|
+
description: 'Returns the documentation for a specific component in the Preply design system.',
|
|
9
|
+
arguments: z.object({
|
|
10
|
+
name: z.string().describe('The name of the component'),
|
|
11
|
+
}),
|
|
12
|
+
options: z.object({
|
|
13
|
+
platform: z
|
|
14
|
+
.enum(PLATFORMS)
|
|
15
|
+
.describe('Target platform for the component documentation. Required.'),
|
|
16
|
+
}),
|
|
17
|
+
callback: ({ name, platform }) => {
|
|
18
|
+
const docs = data[platform][name]?.docs;
|
|
19
|
+
|
|
20
|
+
if (!docs) {
|
|
21
|
+
return [`Component "${name}" not found`];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return [docs];
|
|
25
|
+
},
|
|
26
|
+
});
|