diff --git a/packages/astro-component-docs/package.json b/packages/astro-component-docs/package.json index 83b17ce..0361d78 100644 --- a/packages/astro-component-docs/package.json +++ b/packages/astro-component-docs/package.json @@ -33,7 +33,8 @@ "scripts": { "build": "tsc -b", "dev": "tsc -b --watch", - "clean": "tsc -b --clean" + "clean": "tsc -b --clean", + "test": "bun test src/" }, "dependencies": { "esast-util-from-js": "^2.0.1", diff --git a/packages/astro-component-docs/src/props/extractor.test.ts b/packages/astro-component-docs/src/props/extractor.test.ts new file mode 100644 index 0000000..c0b7012 --- /dev/null +++ b/packages/astro-component-docs/src/props/extractor.test.ts @@ -0,0 +1,121 @@ +import { describe, it, expect } from 'bun:test'; +import { extractAllProps } from './extractor.js'; +import type { PackageConfig } from './types.js'; +import { resolve } from 'node:path'; + +const REACT_NAVER_MAPS_CONFIG: PackageConfig = { + name: 'react-naver-maps', + tsconfig: resolve( + import.meta.dirname, + '../../../react-naver-maps/tsconfig.json', + ), +}; + +describe('extractAllProps', () => { + const docs = extractAllProps(REACT_NAVER_MAPS_CONFIG); + const docMap = new Map(docs.map((d) => [d.displayName, d])); + + it('.d.ts 파일에서 컴포넌트를 감지한다', () => { + expect(docs.length).toBeGreaterThan(0); + }); + + it('주요 컴포넌트가 모두 포함된다', () => { + const expected = [ + 'NaverMap', + 'Marker', + 'Container', + 'Circle', + 'Polygon', + 'Polyline', + 'InfoWindow', + ]; + for (const name of expected) { + expect(docMap.has(name)).toBe(true); + } + }); + + describe('NaverMap', () => { + const naverMap = docMap.get('NaverMap')!; + + it('컴포넌트 문서가 존재한다', () => { + expect(naverMap).toBeDefined(); + }); + + it('Props가 추출된다', () => { + expect(naverMap.props.length).toBeGreaterThan(10); + }); + + it('center prop의 타입이 정확하다', () => { + const center = naverMap.props.find((p) => p.name === 'center'); + expect(center).toBeDefined(); + expect(center!.required).toBe(false); + expect(center!.type).toContain('Coord'); + }); + + it('onClick 이벤트 핸들러가 추출된다', () => { + const onClick = naverMap.props.find((p) => p.name === 'onClick'); + expect(onClick).toBeDefined(); + expect(onClick!.required).toBe(false); + expect(onClick!.type).toContain('PointerEvent'); + }); + + it('ref, key는 제외된다', () => { + const names = naverMap.props.map((p) => p.name); + expect(names).not.toContain('ref'); + expect(names).not.toContain('key'); + }); + }); + + describe('Marker', () => { + const marker = docMap.get('Marker')!; + + it('position prop이 있다', () => { + const position = marker.props.find((p) => p.name === 'position'); + expect(position).toBeDefined(); + expect(position!.type).toContain('Coord'); + }); + }); + + describe('propsOverrides', () => { + it('hidden override가 적용된다', () => { + const config: PackageConfig = { + ...REACT_NAVER_MAPS_CONFIG, + propsOverrides: { + NaverMap: { + center: { hidden: true }, + }, + }, + }; + const result = extractAllProps(config); + const naverMap = result.find((d) => d.displayName === 'NaverMap')!; + const names = naverMap.props.map((p) => p.name); + expect(names).not.toContain('center'); + }); + + it('description override가 적용된다', () => { + const config: PackageConfig = { + ...REACT_NAVER_MAPS_CONFIG, + propsOverrides: { + NaverMap: { + zoom: { description: '커스텀 설명' }, + }, + }, + }; + const result = extractAllProps(config); + const naverMap = result.find((d) => d.displayName === 'NaverMap')!; + const zoom = naverMap.props.find((p) => p.name === 'zoom'); + expect(zoom!.description).toBe('커스텀 설명'); + }); + }); + + describe('에러 처리', () => { + it('잘못된 tsconfig 경로에서 예외를 던진다', () => { + expect(() => + extractAllProps({ + name: 'nonexistent', + tsconfig: '/nonexistent/tsconfig.json', + }), + ).toThrow(); + }); + }); +}); diff --git a/packages/astro-component-docs/src/props/extractor.ts b/packages/astro-component-docs/src/props/extractor.ts index 9100a00..ae19084 100644 --- a/packages/astro-component-docs/src/props/extractor.ts +++ b/packages/astro-component-docs/src/props/extractor.ts @@ -1,3 +1,5 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; import { Project, Node, type Type, type Symbol as TsSymbol } from 'ts-morph'; import type { ComponentDoc, @@ -7,21 +9,37 @@ import type { } from './types.js'; /** - * Extract props from a React component's type declarations using ts-morph. + * Extract props from a React component's .d.ts declarations using ts-morph. * * Strategy: - * 1. Load the project from the package's tsconfig - * 2. Find the component's exported function declaration - * 3. Extract the first parameter's type (Props) - * 4. For each property, extract name, type, required, description, defaultValue - * 5. Apply user overrides + * 1. Read the package's tsconfig to find outDir and compiler options + * 2. Load .d.ts files from outDir (not source files) + * 3. Find the component's exported function declaration + * 4. Extract the first parameter's type (Props) + * 5. For each property, extract name, type, required, description, defaultValue + * 6. Apply user overrides */ export function extractAllProps(pkg: PackageConfig): ComponentDoc[] { + const tsconfigPath = resolve(pkg.tsconfig!); + const tsconfigDir = dirname(tsconfigPath); + const tsconfig = JSON.parse(readFileSync(tsconfigPath, 'utf-8')); + const outDir = tsconfig.compilerOptions?.outDir ?? 'dist'; + const dtsDir = resolve(tsconfigDir, outDir); + const project = new Project({ - tsConfigFilePath: pkg.tsconfig, - skipAddingFilesFromTsConfig: false, + tsConfigFilePath: tsconfigPath, + skipAddingFilesFromTsConfig: true, }); + const added = project.addSourceFilesAtPaths(`${dtsDir}/**/*.d.ts`); + if (added.length === 0) { + console.warn( + `[astro-component-docs] No .d.ts files found in "${dtsDir}". ` + + `Build the package first (e.g. tsc -b).`, + ); + return []; + } + const docs: ComponentDoc[] = []; // Find exported components across all source files diff --git a/packages/astro-component-docs/src/props/types.ts b/packages/astro-component-docs/src/props/types.ts index c194204..7edd68b 100644 --- a/packages/astro-component-docs/src/props/types.ts +++ b/packages/astro-component-docs/src/props/types.ts @@ -23,7 +23,6 @@ export interface PropsOverride { export interface PackageConfig { name: string; tsconfig?: string; - entrypoint?: string; propsOverrides?: Record>; } diff --git a/packages/astro-component-docs/src/vite/vite-plugin-props.ts b/packages/astro-component-docs/src/vite/vite-plugin-props.ts index ba237d7..19aa748 100644 --- a/packages/astro-component-docs/src/vite/vite-plugin-props.ts +++ b/packages/astro-component-docs/src/vite/vite-plugin-props.ts @@ -50,7 +50,12 @@ export function vitePluginProps(config: AstroComponentDocsConfig): VitePlugin { }, handleHotUpdate({ file }: { file: string }) { - if (file.endsWith('tsconfig.json') || file.endsWith('.d.ts')) { + if ( + file.endsWith('tsconfig.json') || + file.endsWith('.d.ts') || + file.endsWith('.ts') || + file.endsWith('.tsx') + ) { propsCache = null; } }, diff --git a/packages/astro-component-docs/tsconfig.json b/packages/astro-component-docs/tsconfig.json index ff4e4d9..fa08962 100644 --- a/packages/astro-component-docs/tsconfig.json +++ b/packages/astro-component-docs/tsconfig.json @@ -16,5 +16,5 @@ "types": [] }, "include": ["src"], - "exclude": ["node_modules", "dist", "src/content/schema.ts"] + "exclude": ["node_modules", "dist", "src/content/schema.ts", "**/*.test.ts"] }