@discourser/design-system 0.15.1 → 0.17.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.
Files changed (179) hide show
  1. package/dist/{chunk-UNWXE6UB.cjs → chunk-2P7Z5PVP.cjs} +817 -16
  2. package/dist/chunk-2P7Z5PVP.cjs.map +1 -0
  3. package/dist/{chunk-ABC7N32K.cjs → chunk-PFWU7QSM.cjs} +464 -8
  4. package/dist/chunk-PFWU7QSM.cjs.map +1 -0
  5. package/dist/{chunk-GD6Q2FUE.js → chunk-QC7LGFM3.js} +808 -18
  6. package/dist/chunk-QC7LGFM3.js.map +1 -0
  7. package/dist/{chunk-SBKRSXSZ.js → chunk-SNUJBT5R.js} +464 -8
  8. package/dist/chunk-SNUJBT5R.js.map +1 -0
  9. package/dist/components/Accordion.figma.d.ts +2 -0
  10. package/dist/components/Accordion.figma.d.ts.map +1 -0
  11. package/dist/components/Breadcrumb.d.ts +2 -0
  12. package/dist/components/Breadcrumb.d.ts.map +1 -1
  13. package/dist/components/Breadcrumb.figma.d.ts +2 -0
  14. package/dist/components/Breadcrumb.figma.d.ts.map +1 -0
  15. package/dist/components/ContentCard/ContentCard.d.ts +13 -0
  16. package/dist/components/ContentCard/ContentCard.d.ts.map +1 -0
  17. package/dist/components/ContentCard/ContentCard.figma.d.ts +2 -0
  18. package/dist/components/ContentCard/ContentCard.figma.d.ts.map +1 -0
  19. package/dist/components/ContentCard/index.d.ts +2 -0
  20. package/dist/components/ContentCard/index.d.ts.map +1 -0
  21. package/dist/components/{Heading.d.ts → Header.d.ts} +3 -3
  22. package/dist/components/Header.d.ts.map +1 -0
  23. package/dist/components/Header.figma.d.ts +2 -0
  24. package/dist/components/Header.figma.d.ts.map +1 -0
  25. package/dist/components/Icons/AccountIcon.d.ts +6 -0
  26. package/dist/components/Icons/AccountIcon.d.ts.map +1 -0
  27. package/dist/components/Icons/ChevronUpIcon.d.ts +6 -0
  28. package/dist/components/Icons/ChevronUpIcon.d.ts.map +1 -0
  29. package/dist/components/Icons/ClockIcon.d.ts.map +1 -1
  30. package/dist/components/Icons/DashboardIcon.d.ts +6 -0
  31. package/dist/components/Icons/DashboardIcon.d.ts.map +1 -0
  32. package/dist/components/Icons/DiscourserLogo.d.ts +6 -0
  33. package/dist/components/Icons/DiscourserLogo.d.ts.map +1 -0
  34. package/dist/components/Icons/DiscourserLogo.figma.d.ts +2 -0
  35. package/dist/components/Icons/DiscourserLogo.figma.d.ts.map +1 -0
  36. package/dist/components/Icons/GripDotsVerticalIcon.d.ts.map +1 -1
  37. package/dist/components/Icons/HelpIcon.d.ts +6 -0
  38. package/dist/components/Icons/HelpIcon.d.ts.map +1 -0
  39. package/dist/components/Icons/NotebookIcon.d.ts +6 -0
  40. package/dist/components/Icons/NotebookIcon.d.ts.map +1 -0
  41. package/dist/components/Icons/RightArrowIcon.d.ts +6 -0
  42. package/dist/components/Icons/RightArrowIcon.d.ts.map +1 -0
  43. package/dist/components/Icons/ScenarioIcon.d.ts +6 -0
  44. package/dist/components/Icons/ScenarioIcon.d.ts.map +1 -0
  45. package/dist/components/Icons/index.d.ts +9 -1
  46. package/dist/components/Icons/index.d.ts.map +1 -1
  47. package/dist/components/NavigationMenu/NavigationMenu.d.ts +3 -0
  48. package/dist/components/NavigationMenu/NavigationMenu.d.ts.map +1 -0
  49. package/dist/components/NavigationMenu/NavigationMenu.figma.d.ts +2 -0
  50. package/dist/components/NavigationMenu/NavigationMenu.figma.d.ts.map +1 -0
  51. package/dist/components/NavigationMenu/index.d.ts +3 -0
  52. package/dist/components/NavigationMenu/index.d.ts.map +1 -0
  53. package/dist/components/NavigationMenu/types.d.ts +25 -0
  54. package/dist/components/NavigationMenu/types.d.ts.map +1 -0
  55. package/dist/components/QuickStartPage/QuickStartPage.d.ts +21 -0
  56. package/dist/components/QuickStartPage/QuickStartPage.d.ts.map +1 -0
  57. package/dist/components/QuickStartPage/index.d.ts +3 -0
  58. package/dist/components/QuickStartPage/index.d.ts.map +1 -0
  59. package/dist/components/ScenarioQueue/ScenarioQueue.figma.d.ts +2 -0
  60. package/dist/components/ScenarioQueue/ScenarioQueue.figma.d.ts.map +1 -0
  61. package/dist/components/ScenarioSettings/ScenarioSettings.d.ts +3 -0
  62. package/dist/components/ScenarioSettings/ScenarioSettings.d.ts.map +1 -0
  63. package/dist/components/ScenarioSettings/ScenarioSettings.figma.d.ts +2 -0
  64. package/dist/components/ScenarioSettings/ScenarioSettings.figma.d.ts.map +1 -0
  65. package/dist/components/ScenarioSettings/index.d.ts +3 -0
  66. package/dist/components/ScenarioSettings/index.d.ts.map +1 -0
  67. package/dist/components/ScenarioSettings/types.d.ts +54 -0
  68. package/dist/components/ScenarioSettings/types.d.ts.map +1 -0
  69. package/dist/components/index.cjs +86 -42
  70. package/dist/components/index.d.ts +14 -3
  71. package/dist/components/index.d.ts.map +1 -1
  72. package/dist/components/index.js +1 -1
  73. package/dist/figma-codex/config.d.ts +8 -0
  74. package/dist/figma-codex/config.d.ts.map +1 -0
  75. package/dist/figma-codex/fixtures/CompoundComponent/CompoundComponent.d.ts +6 -0
  76. package/dist/figma-codex/fixtures/CompoundComponent/CompoundComponent.d.ts.map +1 -0
  77. package/dist/figma-codex/fixtures/CompoundComponent/index.d.ts +2 -0
  78. package/dist/figma-codex/fixtures/CompoundComponent/index.d.ts.map +1 -0
  79. package/dist/figma-codex/fixtures/CompoundComponent.figma.d.ts +2 -0
  80. package/dist/figma-codex/fixtures/CompoundComponent.figma.d.ts.map +1 -0
  81. package/dist/figma-codex/fixtures/SimpleComponent.d.ts +8 -0
  82. package/dist/figma-codex/fixtures/SimpleComponent.d.ts.map +1 -0
  83. package/dist/figma-codex/fixtures/SimpleComponent.figma.d.ts +2 -0
  84. package/dist/figma-codex/fixtures/SimpleComponent.figma.d.ts.map +1 -0
  85. package/dist/figma-codex/generate.d.ts +6 -0
  86. package/dist/figma-codex/generate.d.ts.map +1 -0
  87. package/dist/figma-codex/parser.d.ts +18 -0
  88. package/dist/figma-codex/parser.d.ts.map +1 -0
  89. package/dist/figma-codex/resolver.d.ts +5 -0
  90. package/dist/figma-codex/resolver.d.ts.map +1 -0
  91. package/dist/figma-codex/schema.d.ts +60 -0
  92. package/dist/figma-codex/schema.d.ts.map +1 -0
  93. package/dist/figma-codex/writer.d.ts +8 -0
  94. package/dist/figma-codex/writer.d.ts.map +1 -0
  95. package/dist/figma-codex.json +373 -0
  96. package/dist/index.cjs +90 -46
  97. package/dist/index.js +2 -2
  98. package/dist/preset/index.cjs +2 -2
  99. package/dist/preset/index.d.ts.map +1 -1
  100. package/dist/preset/index.js +1 -1
  101. package/dist/preset/recipes/accordion.d.ts.map +1 -1
  102. package/dist/preset/recipes/breadcrumb.d.ts.map +1 -1
  103. package/dist/preset/recipes/content-card.d.ts +2 -0
  104. package/dist/preset/recipes/content-card.d.ts.map +1 -0
  105. package/dist/preset/recipes/index.d.ts +4 -0
  106. package/dist/preset/recipes/index.d.ts.map +1 -1
  107. package/dist/preset/recipes/navigation-menu.d.ts +2 -0
  108. package/dist/preset/recipes/navigation-menu.d.ts.map +1 -0
  109. package/dist/preset/recipes/scenario-settings.d.ts +2 -0
  110. package/dist/preset/recipes/scenario-settings.d.ts.map +1 -0
  111. package/package.json +26 -2
  112. package/src/components/Accordion.figma.tsx +20 -0
  113. package/src/components/Breadcrumb.figma.tsx +18 -0
  114. package/src/components/Breadcrumb.tsx +33 -15
  115. package/src/components/ContentCard/ContentCard.figma.tsx +21 -0
  116. package/src/components/ContentCard/ContentCard.test.tsx +197 -0
  117. package/src/components/ContentCard/ContentCard.tsx +19 -0
  118. package/src/components/ContentCard/index.ts +13 -0
  119. package/src/components/Header.figma.tsx +25 -0
  120. package/src/components/{Heading.tsx → Header.tsx} +2 -2
  121. package/src/components/Icons/AccountIcon.tsx +26 -0
  122. package/src/components/Icons/ChevronUpIcon.tsx +24 -0
  123. package/src/components/Icons/ClockIcon.tsx +6 -6
  124. package/src/components/Icons/DashboardIcon.tsx +47 -0
  125. package/src/components/Icons/Discourser-Logo.svg +14 -0
  126. package/src/components/Icons/DiscourserLogo.figma.tsx +10 -0
  127. package/src/components/Icons/DiscourserLogo.tsx +72 -0
  128. package/src/components/Icons/GripDotsVerticalIcon.tsx +6 -6
  129. package/src/components/Icons/HelpIcon.tsx +26 -0
  130. package/src/components/Icons/NotebookIcon.tsx +26 -0
  131. package/src/components/Icons/RightArrowIcon.tsx +23 -0
  132. package/src/components/Icons/ScenarioIcon.tsx +26 -0
  133. package/src/components/Icons/index.ts +13 -2
  134. package/src/components/NavigationMenu/NavigationMenu.figma.tsx +26 -0
  135. package/src/components/NavigationMenu/NavigationMenu.test.tsx +524 -0
  136. package/src/components/NavigationMenu/NavigationMenu.tsx +102 -0
  137. package/src/components/NavigationMenu/index.ts +2 -0
  138. package/src/components/NavigationMenu/types.ts +27 -0
  139. package/src/components/QuickStartPage/QuickStartPage.tsx +627 -0
  140. package/src/components/QuickStartPage/index.ts +2 -0
  141. package/src/components/ScenarioQueue/ScenarioQueue.figma.tsx +37 -0
  142. package/src/components/ScenarioSettings/ScenarioSettings.figma.tsx +12 -0
  143. package/src/components/ScenarioSettings/ScenarioSettings.test.tsx +406 -0
  144. package/src/components/ScenarioSettings/ScenarioSettings.tsx +386 -0
  145. package/src/components/ScenarioSettings/index.ts +11 -0
  146. package/src/components/ScenarioSettings/types.ts +70 -0
  147. package/src/components/__tests__/Breadcrumb.test.tsx +94 -0
  148. package/src/components/index.ts +38 -4
  149. package/src/figma-codex/README.md +186 -0
  150. package/src/figma-codex/__tests__/config.test.ts +63 -0
  151. package/src/figma-codex/__tests__/generate.test.ts +78 -0
  152. package/src/figma-codex/__tests__/parser.test.ts +138 -0
  153. package/src/figma-codex/__tests__/resolver.test.ts +196 -0
  154. package/src/figma-codex/__tests__/writer.test.ts +111 -0
  155. package/src/figma-codex/config.ts +42 -0
  156. package/src/figma-codex/fixtures/CompoundComponent/CompoundComponent.tsx +17 -0
  157. package/src/figma-codex/fixtures/CompoundComponent/index.ts +1 -0
  158. package/src/figma-codex/fixtures/CompoundComponent.figma.tsx +14 -0
  159. package/src/figma-codex/fixtures/SimpleComponent.figma.tsx +10 -0
  160. package/src/figma-codex/fixtures/SimpleComponent.tsx +10 -0
  161. package/src/figma-codex/fixtures/expected-output.json +78 -0
  162. package/src/figma-codex/generate.ts +106 -0
  163. package/src/figma-codex/parser.ts +138 -0
  164. package/src/figma-codex/resolver.ts +280 -0
  165. package/src/figma-codex/schema.ts +79 -0
  166. package/src/figma-codex/writer.ts +54 -0
  167. package/src/preset/index.ts +6 -0
  168. package/src/preset/recipes/accordion.ts +8 -5
  169. package/src/preset/recipes/breadcrumb.ts +34 -2
  170. package/src/preset/recipes/content-card.ts +124 -0
  171. package/src/preset/recipes/index.ts +4 -0
  172. package/src/preset/recipes/navigation-menu.ts +97 -0
  173. package/src/preset/recipes/scenario-settings.ts +182 -0
  174. package/src/test/setup.ts +12 -9
  175. package/dist/chunk-ABC7N32K.cjs.map +0 -1
  176. package/dist/chunk-GD6Q2FUE.js.map +0 -1
  177. package/dist/chunk-SBKRSXSZ.js.map +0 -1
  178. package/dist/chunk-UNWXE6UB.cjs.map +0 -1
  179. package/dist/components/Heading.d.ts.map +0 -1
@@ -0,0 +1,111 @@
1
+ // @vitest-environment node
2
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
3
+ import { mkdirSync, rmSync, readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import { writeManifest } from '../writer';
7
+ import type { ComponentEntry } from '../schema';
8
+
9
+ const sampleEntry: ComponentEntry = {
10
+ name: 'Button',
11
+ type: 'simple',
12
+ figma: {
13
+ fileKey: 'ABC123',
14
+ nodeId: '1:2',
15
+ url: 'https://www.figma.com/design/ABC123/F?node-id=1-2',
16
+ },
17
+ imports: {
18
+ primary: "import { Button } from '@discourser/design-system/Button'",
19
+ namedExports: ['Button'],
20
+ subpath: '@discourser/design-system/Button',
21
+ },
22
+ props: [{ name: 'label', type: 'string', required: true }],
23
+ example: '<Button label="Click me" />',
24
+ sourcePath: 'src/components/Button.tsx',
25
+ };
26
+
27
+ describe('writeManifest', () => {
28
+ let tmpDir: string;
29
+ let outputPath: string;
30
+
31
+ beforeEach(() => {
32
+ tmpDir = join(tmpdir(), `figma-codex-writer-${Date.now()}`);
33
+ mkdirSync(tmpDir, { recursive: true });
34
+ outputPath = join(tmpDir, 'figma-codex.json');
35
+ });
36
+
37
+ afterEach(() => {
38
+ rmSync(tmpDir, { recursive: true, force: true });
39
+ });
40
+
41
+ it('writes valid JSON to the output path', () => {
42
+ writeManifest(
43
+ { Button: sampleEntry },
44
+ { packageName: '@discourser/design-system', outputPath },
45
+ tmpDir,
46
+ );
47
+ const raw = readFileSync(outputPath, 'utf-8');
48
+ expect(() => JSON.parse(raw)).not.toThrow();
49
+ });
50
+
51
+ it('includes schema version 1.0.0', () => {
52
+ writeManifest(
53
+ { Button: sampleEntry },
54
+ { packageName: '@discourser/design-system', outputPath },
55
+ tmpDir,
56
+ );
57
+ const manifest = JSON.parse(readFileSync(outputPath, 'utf-8'));
58
+ expect(manifest.version).toBe('1.0.0');
59
+ });
60
+
61
+ it('includes generatedAt ISO timestamp', () => {
62
+ writeManifest(
63
+ { Button: sampleEntry },
64
+ { packageName: '@discourser/design-system', outputPath },
65
+ tmpDir,
66
+ );
67
+ const manifest = JSON.parse(readFileSync(outputPath, 'utf-8'));
68
+ expect(manifest.generatedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
69
+ });
70
+
71
+ it('includes all provided components', () => {
72
+ writeManifest(
73
+ { Button: sampleEntry },
74
+ { packageName: '@discourser/design-system', outputPath },
75
+ tmpDir,
76
+ );
77
+ const manifest = JSON.parse(readFileSync(outputPath, 'utf-8'));
78
+ expect(manifest.components).toHaveProperty('Button');
79
+ expect(manifest.components.Button.name).toBe('Button');
80
+ });
81
+
82
+ it('uses 2-space indentation', () => {
83
+ writeManifest(
84
+ { Button: sampleEntry },
85
+ { packageName: '@discourser/design-system', outputPath },
86
+ tmpDir,
87
+ );
88
+ const raw = readFileSync(outputPath, 'utf-8');
89
+ expect(raw).toContain(' "version"');
90
+ });
91
+
92
+ it('collects figmaFiles from all components', () => {
93
+ writeManifest(
94
+ { Button: sampleEntry },
95
+ { packageName: '@discourser/design-system', outputPath },
96
+ tmpDir,
97
+ );
98
+ const manifest = JSON.parse(readFileSync(outputPath, 'utf-8'));
99
+ expect(manifest.figmaFiles).toHaveProperty('ABC123');
100
+ });
101
+
102
+ it('handles empty component list without throwing', () => {
103
+ expect(() =>
104
+ writeManifest(
105
+ {},
106
+ { packageName: '@discourser/design-system', outputPath },
107
+ tmpDir,
108
+ ),
109
+ ).not.toThrow();
110
+ });
111
+ });
@@ -0,0 +1,42 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ export interface FigmaCodexConfig {
5
+ include: string[];
6
+ outputPath: string;
7
+ packageName: string;
8
+ tsconfig: string;
9
+ }
10
+
11
+ const DEFAULTS: FigmaCodexConfig = {
12
+ include: ['src/components/**/*.figma.tsx'],
13
+ outputPath: 'dist/figma-codex.json',
14
+ packageName: '',
15
+ tsconfig: 'tsconfig.json',
16
+ };
17
+
18
+ export function loadConfig(
19
+ projectRoot: string = process.cwd(),
20
+ ): FigmaCodexConfig {
21
+ let userConfig: Partial<FigmaCodexConfig> = {};
22
+
23
+ const configPath = join(projectRoot, 'figma-codex.config.json');
24
+ if (existsSync(configPath)) {
25
+ userConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
26
+ }
27
+
28
+ let packageName = userConfig.packageName ?? '';
29
+ if (!packageName) {
30
+ const pkgPath = join(projectRoot, 'package.json');
31
+ if (existsSync(pkgPath)) {
32
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
33
+ packageName = pkg.name ?? '';
34
+ }
35
+ }
36
+
37
+ return {
38
+ ...DEFAULTS,
39
+ ...userConfig,
40
+ packageName,
41
+ };
42
+ }
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+ // Test fixture — resolver uses text-based regex, imports not required to be valid.
3
+ // Globals declared below satisfy TypeScript; the resolver reads this file as raw text.
4
+ import type { FC, ComponentProps } from 'react';
5
+
6
+ declare const ark: {
7
+ div: FC<ComponentProps<'div'>>;
8
+ h2: FC<ComponentProps<'h2'>>;
9
+ span: FC<ComponentProps<'span'>>;
10
+ };
11
+ declare function withProvider<T>(el: T, slot: string): T;
12
+ declare function withContext<T>(el: T, slot: string): T;
13
+
14
+ export type RootProps = ComponentProps<'div'>;
15
+ export const Root = withProvider(ark.div, 'root');
16
+ export const Header = withContext(ark.div, 'header');
17
+ export const Body = withContext(ark.div, 'body');
@@ -0,0 +1 @@
1
+ export { Root, Header, Body, type RootProps } from './CompoundComponent';
@@ -0,0 +1,14 @@
1
+ import figma from '@figma/code-connect';
2
+ import * as CompoundComponent from './CompoundComponent/index';
3
+
4
+ figma.connect(
5
+ CompoundComponent.Root,
6
+ 'https://www.figma.com/design/ABC123/TestFile?node-id=5-10',
7
+ {
8
+ example: () => (
9
+ <CompoundComponent.Root>
10
+ <CompoundComponent.Header>Title</CompoundComponent.Header>
11
+ </CompoundComponent.Root>
12
+ ),
13
+ },
14
+ );
@@ -0,0 +1,10 @@
1
+ import figma from '@figma/code-connect';
2
+ import { SimpleComponent } from './SimpleComponent';
3
+
4
+ figma.connect(
5
+ SimpleComponent,
6
+ 'https://www.figma.com/design/ABC123/TestFile?node-id=1-2',
7
+ {
8
+ example: () => <SimpleComponent label="Hello" />,
9
+ },
10
+ );
@@ -0,0 +1,10 @@
1
+ export interface SimpleComponentProps {
2
+ /** The display label */
3
+ label: string;
4
+ /** Optional size variant */
5
+ size?: 'sm' | 'md' | 'lg';
6
+ }
7
+
8
+ export const SimpleComponent = (props: SimpleComponentProps) => (
9
+ <div>{props.label}</div>
10
+ );
@@ -0,0 +1,78 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "packageName": "@test/design-system",
4
+ "generatedAt": "2026-02-27T19:21:46.956Z",
5
+ "gitHash": "3fd0a961",
6
+ "figmaFiles": {
7
+ "ABC123": {
8
+ "fileKey": "ABC123"
9
+ }
10
+ },
11
+ "components": {
12
+ "CompoundComponent": {
13
+ "name": "CompoundComponent",
14
+ "type": "compound",
15
+ "figma": {
16
+ "fileKey": "ABC123",
17
+ "nodeId": "5:10",
18
+ "url": "https://www.figma.com/design/ABC123/TestFile?node-id=5-10"
19
+ },
20
+ "imports": {
21
+ "primary": "import * as CompoundComponent from '@test/design-system/CompoundComponent'",
22
+ "namedExports": [
23
+ "CompoundComponent.Root",
24
+ "CompoundComponent.Header",
25
+ "CompoundComponent.Body"
26
+ ]
27
+ },
28
+ "props": [],
29
+ "subComponents": [
30
+ {
31
+ "name": "Root",
32
+ "element": "div"
33
+ },
34
+ {
35
+ "name": "Header",
36
+ "element": "div"
37
+ },
38
+ {
39
+ "name": "Body",
40
+ "element": "div"
41
+ }
42
+ ],
43
+ "example": "<CompoundComponent.Root>\n <CompoundComponent.Header>Title</CompoundComponent.Header>\n </CompoundComponent.Root>",
44
+ "sourcePath": "src/figma-codex/fixtures/CompoundComponent/index.ts"
45
+ },
46
+ "SimpleComponent": {
47
+ "name": "SimpleComponent",
48
+ "type": "simple",
49
+ "figma": {
50
+ "fileKey": "ABC123",
51
+ "nodeId": "1:2",
52
+ "url": "https://www.figma.com/design/ABC123/TestFile?node-id=1-2"
53
+ },
54
+ "imports": {
55
+ "primary": "import { SimpleComponent } from '@test/design-system/SimpleComponent'",
56
+ "namedExports": [
57
+ "SimpleComponent"
58
+ ]
59
+ },
60
+ "props": [
61
+ {
62
+ "name": "label",
63
+ "type": "string",
64
+ "required": true,
65
+ "description": "The display label"
66
+ },
67
+ {
68
+ "name": "size",
69
+ "type": "'sm' | 'md' | 'lg'",
70
+ "required": false,
71
+ "description": "Optional size variant"
72
+ }
73
+ ],
74
+ "example": "<SimpleComponent label=\"Hello\" />",
75
+ "sourcePath": "src/figma-codex/fixtures/SimpleComponent.tsx"
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,106 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { join, resolve, isAbsolute } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { parseFigmaFile, ParseError } from './parser';
5
+ import { resolveComponent } from './resolver';
6
+ import { writeManifest } from './writer';
7
+ import { loadConfig, type FigmaCodexConfig } from './config';
8
+ import type { ComponentEntry } from './schema';
9
+
10
+ /** Recursively find all .figma.tsx files under a directory */
11
+ function findFigmaFiles(rootDir: string): string[] {
12
+ const files: string[] = [];
13
+
14
+ function walk(dir: string) {
15
+ let entries;
16
+ try {
17
+ entries = readdirSync(dir, { withFileTypes: true });
18
+ } catch {
19
+ return;
20
+ }
21
+ for (const entry of entries) {
22
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
23
+ const full = join(dir, entry.name);
24
+ if (entry.isDirectory()) walk(full);
25
+ else if (entry.name.endsWith('.figma.tsx')) files.push(full);
26
+ }
27
+ }
28
+
29
+ walk(rootDir);
30
+ return files;
31
+ }
32
+
33
+ export interface GenerateOptions extends FigmaCodexConfig {
34
+ projectRoot?: string;
35
+ }
36
+
37
+ export function generate(options: GenerateOptions): void {
38
+ const projectRoot = options.projectRoot ?? process.cwd();
39
+
40
+ // Resolve include patterns to base directories then scan recursively
41
+ const figmaFiles: string[] = [];
42
+ for (const pattern of options.include) {
43
+ // Extract the literal prefix before the first '*' as the base directory to walk.
44
+ // Patterns starting with '*' (e.g. '**/*.figma.tsx') fall back to projectRoot.
45
+ // Patterns like 'src/components/**/*.figma.tsx' correctly resolve to 'src/components/'.
46
+ const starIdx = pattern.indexOf('*');
47
+ const baseDir =
48
+ starIdx > 0
49
+ ? isAbsolute(pattern.slice(0, starIdx))
50
+ ? pattern.slice(0, starIdx).replace(/\/$/, '')
51
+ : join(projectRoot, pattern.slice(0, starIdx).replace(/\/$/, ''))
52
+ : projectRoot;
53
+ const found = findFigmaFiles(baseDir);
54
+ figmaFiles.push(...found);
55
+ }
56
+
57
+ if (figmaFiles.length === 0) {
58
+ console.warn(
59
+ '[figma-codex] No .figma.tsx files found. Check your include patterns.',
60
+ );
61
+ return;
62
+ }
63
+
64
+ const components: Record<string, ComponentEntry> = {};
65
+
66
+ for (const filePath of figmaFiles) {
67
+ try {
68
+ const content = readFileSync(filePath, 'utf-8');
69
+ const parsed = parseFigmaFile(content, filePath);
70
+ const entry = resolveComponent(parsed, options);
71
+ components[entry.name] = entry;
72
+ console.log(`[figma-codex] ✓ ${entry.name} (${entry.type})`);
73
+ } catch (err) {
74
+ if (err instanceof ParseError) {
75
+ console.warn(`[figma-codex] ⚠ Skipping ${filePath}: ${err.message}`);
76
+ } else {
77
+ console.warn(`[figma-codex] ⚠ Skipping ${filePath}: ${String(err)}`);
78
+ }
79
+ }
80
+ }
81
+
82
+ if (Object.keys(components).length === 0) {
83
+ throw new Error('[figma-codex] No components successfully parsed.');
84
+ }
85
+
86
+ writeManifest(
87
+ components,
88
+ { packageName: options.packageName, outputPath: options.outputPath },
89
+ projectRoot,
90
+ );
91
+ console.log(
92
+ `[figma-codex] Generated ${Object.keys(components).length} components → ${options.outputPath}`,
93
+ );
94
+ }
95
+
96
+ // CLI entry point
97
+ const currentFile = fileURLToPath(import.meta.url);
98
+ if (process.argv[1] && resolve(process.argv[1]) === resolve(currentFile)) {
99
+ try {
100
+ const config = loadConfig();
101
+ generate({ ...config, projectRoot: process.cwd() });
102
+ } catch (err) {
103
+ console.error(String(err));
104
+ process.exit(1);
105
+ }
106
+ }
@@ -0,0 +1,138 @@
1
+ export class ParseError extends Error {
2
+ constructor(
3
+ message: string,
4
+ public readonly filePath: string,
5
+ ) {
6
+ super(`ParseError: ${message} (${filePath})`);
7
+ this.name = 'ParseError';
8
+ }
9
+ }
10
+
11
+ export interface ParsedFigmaFile {
12
+ filePath: string;
13
+ importStyle: 'named' | 'namespace';
14
+ componentName: string;
15
+ importSource: string;
16
+ connectSubComponent?: string;
17
+ figmaUrl: string;
18
+ figmaFileKey: string;
19
+ figmaNodeId: string;
20
+ example: string;
21
+ propsMapping: Record<string, string>;
22
+ }
23
+
24
+ function parseNodeId(url: string): string {
25
+ const match = url.match(/node-id=([^&'"]+)/);
26
+ if (!match) return '';
27
+ return match[1].replace(/-/g, ':');
28
+ }
29
+
30
+ function parseFileKey(url: string): string {
31
+ const match = url.match(/figma\.com\/design\/([^/?]+)/);
32
+ return match?.[1] ?? '';
33
+ }
34
+
35
+ function extractExample(content: string): string {
36
+ const exampleIdx = content.indexOf('example:');
37
+ if (exampleIdx === -1) return '';
38
+
39
+ const arrowIdx = content.indexOf('=>', exampleIdx);
40
+ if (arrowIdx === -1) return '';
41
+
42
+ const afterArrow = content.slice(arrowIdx + 2).trimStart();
43
+
44
+ if (afterArrow.startsWith('(')) {
45
+ // Multi-line: track balanced parens
46
+ let depth = 0;
47
+ let i = 0;
48
+ for (; i < afterArrow.length; i++) {
49
+ if (afterArrow[i] === '(') depth++;
50
+ else if (afterArrow[i] === ')') {
51
+ depth--;
52
+ if (depth === 0) break;
53
+ }
54
+ }
55
+ const inner = afterArrow.slice(1, i).trim();
56
+ return inner;
57
+ } else {
58
+ // Single-line: grab to end of line, strip trailing comma
59
+ const lineEnd = afterArrow.indexOf('\n');
60
+ const line = lineEnd === -1 ? afterArrow : afterArrow.slice(0, lineEnd);
61
+ return line.replace(/,\s*$/, '').trim();
62
+ }
63
+ }
64
+
65
+ function extractPropsMapping(content: string): Record<string, string> {
66
+ const result: Record<string, string> = {};
67
+ const propsBlockMatch = content.match(/props:\s*\{([^}]+)\}/);
68
+ if (!propsBlockMatch) return result;
69
+
70
+ const block = propsBlockMatch[1];
71
+ const propNames = block.match(/(\w+)\s*:/g);
72
+ if (propNames) {
73
+ for (const p of propNames) {
74
+ result[p.replace(':', '').trim()] = 'figma.enum';
75
+ }
76
+ }
77
+ return result;
78
+ }
79
+
80
+ export function parseFigmaFile(
81
+ content: string,
82
+ filePath: string,
83
+ ): ParsedFigmaFile {
84
+ // Try namespace import: import * as X from './path'
85
+ const nsMatch = content.match(
86
+ /import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/,
87
+ );
88
+ // Try named import: import { X } from './path' (skip figma import itself)
89
+ const namedMatches = [
90
+ ...content.matchAll(/import\s+\{\s*(\w+)\s*\}\s+from\s+['"]([^'"]+)['"]/g),
91
+ ];
92
+ // Filter out the figma import itself
93
+ const namedMatch = namedMatches.find(
94
+ (m) => !m[2].includes('figma') && !m[2].includes('@figma'),
95
+ );
96
+
97
+ if (!nsMatch && !namedMatch) {
98
+ throw new ParseError('No component import found', filePath);
99
+ }
100
+
101
+ const connectMatch = content.match(
102
+ /figma\.connect\(\s*(\w+)(?:\.(\w+))?\s*,\s*['"]([^'"]+)['"]/,
103
+ );
104
+ if (!connectMatch) {
105
+ throw new ParseError('No figma.connect() call found', filePath);
106
+ }
107
+
108
+ const figmaUrl = connectMatch[3];
109
+ const connectSubComponent = connectMatch[2];
110
+
111
+ if (nsMatch) {
112
+ return {
113
+ filePath,
114
+ importStyle: 'namespace',
115
+ componentName: nsMatch[1],
116
+ importSource: nsMatch[2],
117
+ connectSubComponent,
118
+ figmaUrl,
119
+ figmaFileKey: parseFileKey(figmaUrl),
120
+ figmaNodeId: parseNodeId(figmaUrl),
121
+ example: extractExample(content),
122
+ propsMapping: extractPropsMapping(content),
123
+ };
124
+ }
125
+
126
+ return {
127
+ filePath,
128
+ importStyle: 'named',
129
+ componentName: namedMatch![1],
130
+ importSource: namedMatch![2],
131
+ connectSubComponent,
132
+ figmaUrl,
133
+ figmaFileKey: parseFileKey(figmaUrl),
134
+ figmaNodeId: parseNodeId(figmaUrl),
135
+ example: extractExample(content),
136
+ propsMapping: extractPropsMapping(content),
137
+ };
138
+ }