@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.
- package/dist/{chunk-UNWXE6UB.cjs → chunk-2P7Z5PVP.cjs} +817 -16
- package/dist/chunk-2P7Z5PVP.cjs.map +1 -0
- package/dist/{chunk-ABC7N32K.cjs → chunk-PFWU7QSM.cjs} +464 -8
- package/dist/chunk-PFWU7QSM.cjs.map +1 -0
- package/dist/{chunk-GD6Q2FUE.js → chunk-QC7LGFM3.js} +808 -18
- package/dist/chunk-QC7LGFM3.js.map +1 -0
- package/dist/{chunk-SBKRSXSZ.js → chunk-SNUJBT5R.js} +464 -8
- package/dist/chunk-SNUJBT5R.js.map +1 -0
- package/dist/components/Accordion.figma.d.ts +2 -0
- package/dist/components/Accordion.figma.d.ts.map +1 -0
- package/dist/components/Breadcrumb.d.ts +2 -0
- package/dist/components/Breadcrumb.d.ts.map +1 -1
- package/dist/components/Breadcrumb.figma.d.ts +2 -0
- package/dist/components/Breadcrumb.figma.d.ts.map +1 -0
- package/dist/components/ContentCard/ContentCard.d.ts +13 -0
- package/dist/components/ContentCard/ContentCard.d.ts.map +1 -0
- package/dist/components/ContentCard/ContentCard.figma.d.ts +2 -0
- package/dist/components/ContentCard/ContentCard.figma.d.ts.map +1 -0
- package/dist/components/ContentCard/index.d.ts +2 -0
- package/dist/components/ContentCard/index.d.ts.map +1 -0
- package/dist/components/{Heading.d.ts → Header.d.ts} +3 -3
- package/dist/components/Header.d.ts.map +1 -0
- package/dist/components/Header.figma.d.ts +2 -0
- package/dist/components/Header.figma.d.ts.map +1 -0
- package/dist/components/Icons/AccountIcon.d.ts +6 -0
- package/dist/components/Icons/AccountIcon.d.ts.map +1 -0
- package/dist/components/Icons/ChevronUpIcon.d.ts +6 -0
- package/dist/components/Icons/ChevronUpIcon.d.ts.map +1 -0
- package/dist/components/Icons/ClockIcon.d.ts.map +1 -1
- package/dist/components/Icons/DashboardIcon.d.ts +6 -0
- package/dist/components/Icons/DashboardIcon.d.ts.map +1 -0
- package/dist/components/Icons/DiscourserLogo.d.ts +6 -0
- package/dist/components/Icons/DiscourserLogo.d.ts.map +1 -0
- package/dist/components/Icons/DiscourserLogo.figma.d.ts +2 -0
- package/dist/components/Icons/DiscourserLogo.figma.d.ts.map +1 -0
- package/dist/components/Icons/GripDotsVerticalIcon.d.ts.map +1 -1
- package/dist/components/Icons/HelpIcon.d.ts +6 -0
- package/dist/components/Icons/HelpIcon.d.ts.map +1 -0
- package/dist/components/Icons/NotebookIcon.d.ts +6 -0
- package/dist/components/Icons/NotebookIcon.d.ts.map +1 -0
- package/dist/components/Icons/RightArrowIcon.d.ts +6 -0
- package/dist/components/Icons/RightArrowIcon.d.ts.map +1 -0
- package/dist/components/Icons/ScenarioIcon.d.ts +6 -0
- package/dist/components/Icons/ScenarioIcon.d.ts.map +1 -0
- package/dist/components/Icons/index.d.ts +9 -1
- package/dist/components/Icons/index.d.ts.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.d.ts +3 -0
- package/dist/components/NavigationMenu/NavigationMenu.d.ts.map +1 -0
- package/dist/components/NavigationMenu/NavigationMenu.figma.d.ts +2 -0
- package/dist/components/NavigationMenu/NavigationMenu.figma.d.ts.map +1 -0
- package/dist/components/NavigationMenu/index.d.ts +3 -0
- package/dist/components/NavigationMenu/index.d.ts.map +1 -0
- package/dist/components/NavigationMenu/types.d.ts +25 -0
- package/dist/components/NavigationMenu/types.d.ts.map +1 -0
- package/dist/components/QuickStartPage/QuickStartPage.d.ts +21 -0
- package/dist/components/QuickStartPage/QuickStartPage.d.ts.map +1 -0
- package/dist/components/QuickStartPage/index.d.ts +3 -0
- package/dist/components/QuickStartPage/index.d.ts.map +1 -0
- package/dist/components/ScenarioQueue/ScenarioQueue.figma.d.ts +2 -0
- package/dist/components/ScenarioQueue/ScenarioQueue.figma.d.ts.map +1 -0
- package/dist/components/ScenarioSettings/ScenarioSettings.d.ts +3 -0
- package/dist/components/ScenarioSettings/ScenarioSettings.d.ts.map +1 -0
- package/dist/components/ScenarioSettings/ScenarioSettings.figma.d.ts +2 -0
- package/dist/components/ScenarioSettings/ScenarioSettings.figma.d.ts.map +1 -0
- package/dist/components/ScenarioSettings/index.d.ts +3 -0
- package/dist/components/ScenarioSettings/index.d.ts.map +1 -0
- package/dist/components/ScenarioSettings/types.d.ts +54 -0
- package/dist/components/ScenarioSettings/types.d.ts.map +1 -0
- package/dist/components/index.cjs +86 -42
- package/dist/components/index.d.ts +14 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/figma-codex/config.d.ts +8 -0
- package/dist/figma-codex/config.d.ts.map +1 -0
- package/dist/figma-codex/fixtures/CompoundComponent/CompoundComponent.d.ts +6 -0
- package/dist/figma-codex/fixtures/CompoundComponent/CompoundComponent.d.ts.map +1 -0
- package/dist/figma-codex/fixtures/CompoundComponent/index.d.ts +2 -0
- package/dist/figma-codex/fixtures/CompoundComponent/index.d.ts.map +1 -0
- package/dist/figma-codex/fixtures/CompoundComponent.figma.d.ts +2 -0
- package/dist/figma-codex/fixtures/CompoundComponent.figma.d.ts.map +1 -0
- package/dist/figma-codex/fixtures/SimpleComponent.d.ts +8 -0
- package/dist/figma-codex/fixtures/SimpleComponent.d.ts.map +1 -0
- package/dist/figma-codex/fixtures/SimpleComponent.figma.d.ts +2 -0
- package/dist/figma-codex/fixtures/SimpleComponent.figma.d.ts.map +1 -0
- package/dist/figma-codex/generate.d.ts +6 -0
- package/dist/figma-codex/generate.d.ts.map +1 -0
- package/dist/figma-codex/parser.d.ts +18 -0
- package/dist/figma-codex/parser.d.ts.map +1 -0
- package/dist/figma-codex/resolver.d.ts +5 -0
- package/dist/figma-codex/resolver.d.ts.map +1 -0
- package/dist/figma-codex/schema.d.ts +60 -0
- package/dist/figma-codex/schema.d.ts.map +1 -0
- package/dist/figma-codex/writer.d.ts +8 -0
- package/dist/figma-codex/writer.d.ts.map +1 -0
- package/dist/figma-codex.json +373 -0
- package/dist/index.cjs +90 -46
- package/dist/index.js +2 -2
- package/dist/preset/index.cjs +2 -2
- package/dist/preset/index.d.ts.map +1 -1
- package/dist/preset/index.js +1 -1
- package/dist/preset/recipes/accordion.d.ts.map +1 -1
- package/dist/preset/recipes/breadcrumb.d.ts.map +1 -1
- package/dist/preset/recipes/content-card.d.ts +2 -0
- package/dist/preset/recipes/content-card.d.ts.map +1 -0
- package/dist/preset/recipes/index.d.ts +4 -0
- package/dist/preset/recipes/index.d.ts.map +1 -1
- package/dist/preset/recipes/navigation-menu.d.ts +2 -0
- package/dist/preset/recipes/navigation-menu.d.ts.map +1 -0
- package/dist/preset/recipes/scenario-settings.d.ts +2 -0
- package/dist/preset/recipes/scenario-settings.d.ts.map +1 -0
- package/package.json +26 -2
- package/src/components/Accordion.figma.tsx +20 -0
- package/src/components/Breadcrumb.figma.tsx +18 -0
- package/src/components/Breadcrumb.tsx +33 -15
- package/src/components/ContentCard/ContentCard.figma.tsx +21 -0
- package/src/components/ContentCard/ContentCard.test.tsx +197 -0
- package/src/components/ContentCard/ContentCard.tsx +19 -0
- package/src/components/ContentCard/index.ts +13 -0
- package/src/components/Header.figma.tsx +25 -0
- package/src/components/{Heading.tsx → Header.tsx} +2 -2
- package/src/components/Icons/AccountIcon.tsx +26 -0
- package/src/components/Icons/ChevronUpIcon.tsx +24 -0
- package/src/components/Icons/ClockIcon.tsx +6 -6
- package/src/components/Icons/DashboardIcon.tsx +47 -0
- package/src/components/Icons/Discourser-Logo.svg +14 -0
- package/src/components/Icons/DiscourserLogo.figma.tsx +10 -0
- package/src/components/Icons/DiscourserLogo.tsx +72 -0
- package/src/components/Icons/GripDotsVerticalIcon.tsx +6 -6
- package/src/components/Icons/HelpIcon.tsx +26 -0
- package/src/components/Icons/NotebookIcon.tsx +26 -0
- package/src/components/Icons/RightArrowIcon.tsx +23 -0
- package/src/components/Icons/ScenarioIcon.tsx +26 -0
- package/src/components/Icons/index.ts +13 -2
- package/src/components/NavigationMenu/NavigationMenu.figma.tsx +26 -0
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +524 -0
- package/src/components/NavigationMenu/NavigationMenu.tsx +102 -0
- package/src/components/NavigationMenu/index.ts +2 -0
- package/src/components/NavigationMenu/types.ts +27 -0
- package/src/components/QuickStartPage/QuickStartPage.tsx +627 -0
- package/src/components/QuickStartPage/index.ts +2 -0
- package/src/components/ScenarioQueue/ScenarioQueue.figma.tsx +37 -0
- package/src/components/ScenarioSettings/ScenarioSettings.figma.tsx +12 -0
- package/src/components/ScenarioSettings/ScenarioSettings.test.tsx +406 -0
- package/src/components/ScenarioSettings/ScenarioSettings.tsx +386 -0
- package/src/components/ScenarioSettings/index.ts +11 -0
- package/src/components/ScenarioSettings/types.ts +70 -0
- package/src/components/__tests__/Breadcrumb.test.tsx +94 -0
- package/src/components/index.ts +38 -4
- package/src/figma-codex/README.md +186 -0
- package/src/figma-codex/__tests__/config.test.ts +63 -0
- package/src/figma-codex/__tests__/generate.test.ts +78 -0
- package/src/figma-codex/__tests__/parser.test.ts +138 -0
- package/src/figma-codex/__tests__/resolver.test.ts +196 -0
- package/src/figma-codex/__tests__/writer.test.ts +111 -0
- package/src/figma-codex/config.ts +42 -0
- package/src/figma-codex/fixtures/CompoundComponent/CompoundComponent.tsx +17 -0
- package/src/figma-codex/fixtures/CompoundComponent/index.ts +1 -0
- package/src/figma-codex/fixtures/CompoundComponent.figma.tsx +14 -0
- package/src/figma-codex/fixtures/SimpleComponent.figma.tsx +10 -0
- package/src/figma-codex/fixtures/SimpleComponent.tsx +10 -0
- package/src/figma-codex/fixtures/expected-output.json +78 -0
- package/src/figma-codex/generate.ts +106 -0
- package/src/figma-codex/parser.ts +138 -0
- package/src/figma-codex/resolver.ts +280 -0
- package/src/figma-codex/schema.ts +79 -0
- package/src/figma-codex/writer.ts +54 -0
- package/src/preset/index.ts +6 -0
- package/src/preset/recipes/accordion.ts +8 -5
- package/src/preset/recipes/breadcrumb.ts +34 -2
- package/src/preset/recipes/content-card.ts +124 -0
- package/src/preset/recipes/index.ts +4 -0
- package/src/preset/recipes/navigation-menu.ts +97 -0
- package/src/preset/recipes/scenario-settings.ts +182 -0
- package/src/test/setup.ts +12 -9
- package/dist/chunk-ABC7N32K.cjs.map +0 -1
- package/dist/chunk-GD6Q2FUE.js.map +0 -1
- package/dist/chunk-SBKRSXSZ.js.map +0 -1
- package/dist/chunk-UNWXE6UB.cjs.map +0 -1
- 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,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
|
+
}
|