diff --git a/package-lock.json b/package-lock.json
index 435f16d19bf..6253b0dfdaf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6535,6 +6535,10 @@
"resolved": "packages/react",
"link": true
},
+ "node_modules/@primer/react-metadata": {
+ "resolved": "packages/react-metadata",
+ "link": true
+ },
"node_modules/@primer/stylelint-config": {
"version": "13.1.1",
"dev": true,
@@ -28412,9 +28416,9 @@
"license": "MIT"
},
"node_modules/typescript": {
- "version": "5.8.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
- "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -30769,6 +30773,13 @@
}
}
},
+ "packages/react-metadata": {
+ "name": "@primer/react-metadata",
+ "version": "0.1.0",
+ "devDependencies": {
+ "typescript": "^5.8.3"
+ }
+ },
"packages/react/node_modules/@figma/code-connect": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@figma/code-connect/-/code-connect-1.3.1.tgz",
diff --git a/packages/react-metadata/package.json b/packages/react-metadata/package.json
new file mode 100644
index 00000000000..38489d4b8c5
--- /dev/null
+++ b/packages/react-metadata/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@primer/react-metadata",
+ "version": "0.1.0",
+ "type": "module",
+ "private": true,
+ "exports": "./src/index.ts",
+ "devDependencies": {
+ "typescript": "^5.8.3"
+ }
+}
diff --git a/packages/react-metadata/src/__tests__/react-metadata.test.ts b/packages/react-metadata/src/__tests__/react-metadata.test.ts
new file mode 100644
index 00000000000..3881990789a
--- /dev/null
+++ b/packages/react-metadata/src/__tests__/react-metadata.test.ts
@@ -0,0 +1,1461 @@
+import ts from 'typescript'
+import {describe, test, expect} from 'vitest'
+import {getMetadataFromSourceFile} from '../'
+
+describe('keywords', () => {
+ test('any', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = any
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ type: {
+ kind: 'Any',
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+
+ test('boolean', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = boolean
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ type: {
+ kind: 'Boolean',
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+
+ test('never', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = never
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ type: {
+ kind: 'Never',
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+
+ test('null', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = null
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ type: {
+ kind: 'Null',
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+
+ test('number', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = number
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ type: {
+ kind: 'Number',
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+
+ test('string', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = string
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ type: {
+ kind: 'String',
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+
+ test('undefined', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = undefined
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ type: {
+ kind: 'Undefined',
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+
+ test('unknown', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = unknown
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ type: {
+ kind: 'Unknown',
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+})
+
+describe('literals', () => {
+ test('boolean literal', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t1 = true
+ export type t2 = false
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't1',
+ type: {
+ kind: 'BooleanLiteral',
+ value: 'true',
+ },
+ typeParameters: [],
+ },
+ {
+ kind: 'TypeAlias',
+ name: 't2',
+ type: {
+ kind: 'BooleanLiteral',
+ value: 'false',
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+
+ test('numeric literal', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = 1
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ type: {
+ kind: 'NumericLiteral',
+ value: '1',
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+
+ test('string literal', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = 'test'
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ type: {
+ kind: 'StringLiteral',
+ value: 'test',
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+})
+
+describe('functions', () => {
+ test('function declaration', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export function t(): void {}
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'FunctionDeclaration',
+ name: 't',
+ signatures: [
+ {
+ kind: 'Signature',
+ parameters: [],
+ returnType: {
+ kind: 'Void',
+ },
+ typeParameters: [],
+ },
+ ],
+ },
+ ],
+ })
+ })
+
+ test('function declaration with inferred return type', () => {
+ const program = getTypeScriptProgram({
+ 'test.tsx': `
+ import React from 'react'
+
+ export function t1() {}
+
+ export function t2() {
+ return 1
+ }
+
+ export function t3() {
+ return [1, 2, 3]
+ }
+
+ export function t4() {
+ return
+ }
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.tsx')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'FunctionDeclaration',
+ name: 't1',
+ signatures: [
+ {
+ kind: 'Signature',
+ parameters: [],
+ returnType: {
+ kind: 'Void',
+ },
+ typeParameters: [],
+ },
+ ],
+ },
+ {
+ kind: 'FunctionDeclaration',
+ name: 't2',
+ signatures: [
+ {
+ kind: 'Signature',
+ parameters: [],
+ returnType: {
+ kind: 'Number',
+ },
+ typeParameters: [],
+ },
+ ],
+ },
+ {
+ kind: 'FunctionDeclaration',
+ name: 't3',
+ signatures: [
+ {
+ kind: 'Signature',
+ parameters: [],
+ returnType: {
+ kind: 'TypeReference',
+ typeName: {
+ kind: 'Identifier',
+ name: 'Array',
+ },
+ typeArguments: [
+ {
+ kind: 'Number',
+ },
+ ],
+ },
+ typeParameters: [],
+ },
+ ],
+ },
+ {
+ kind: 'FunctionDeclaration',
+ name: 't4',
+ signatures: [
+ {
+ kind: 'Signature',
+ parameters: [],
+ returnType: {
+ kind: 'TypeReference',
+ typeName: {
+ kind: 'QualifiedName',
+ left: {
+ kind: 'QualifiedName',
+ left: {
+ kind: 'Identifier',
+ name: 'React',
+ },
+ right: {
+ kind: 'Identifier',
+ name: 'JSX',
+ },
+ },
+ right: {
+ kind: 'Identifier',
+ name: 'Element',
+ },
+ },
+ typeArguments: [],
+ },
+ typeParameters: [],
+ },
+ ],
+ },
+ ],
+ })
+ })
+
+ test('parameters', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export function t1(a: number) {}
+ export function t2(a?: number) {}
+ export function t3(a: React.ReactNode) {}
+ export function t4(a: number, ...rest: Array) {}
+ export function t5({a, b: renamed, ...rest}: {a: number; b: number; c: number}) {}
+ export function t6([a, ...rest]: [number, number]) {}
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'FunctionDeclaration',
+ name: 't1',
+ signatures: [
+ {
+ kind: 'Signature',
+ parameters: [
+ {
+ kind: 'Parameter',
+ name: {
+ kind: 'Identifier',
+ name: 'a',
+ },
+ type: {
+ kind: 'Number',
+ },
+ optional: false,
+ rest: false,
+ },
+ ],
+ returnType: {
+ kind: 'Void',
+ },
+ typeParameters: [],
+ },
+ ],
+ },
+ {
+ kind: 'FunctionDeclaration',
+ name: 't2',
+ signatures: [
+ {
+ kind: 'Signature',
+ parameters: [
+ {
+ kind: 'Parameter',
+ name: {
+ kind: 'Identifier',
+ name: 'a',
+ },
+ type: {
+ kind: 'Number',
+ },
+ optional: true,
+ rest: false,
+ },
+ ],
+ returnType: {
+ kind: 'Void',
+ },
+ typeParameters: [],
+ },
+ ],
+ },
+ {
+ kind: 'FunctionDeclaration',
+ name: 't3',
+ signatures: [
+ {
+ kind: 'Signature',
+ parameters: [
+ {
+ kind: 'Parameter',
+ name: {
+ kind: 'Identifier',
+ name: 'a',
+ },
+ type: {
+ kind: 'TypeReference',
+ typeName: {
+ kind: 'QualifiedName',
+ left: {
+ kind: 'Identifier',
+ name: 'React',
+ },
+ right: {
+ kind: 'Identifier',
+ name: 'ReactNode',
+ },
+ },
+ typeArguments: [],
+ },
+ optional: false,
+ rest: false,
+ },
+ ],
+ returnType: {
+ kind: 'Void',
+ },
+ typeParameters: [],
+ },
+ ],
+ },
+ {
+ kind: 'FunctionDeclaration',
+ name: 't4',
+ signatures: [
+ {
+ kind: 'Signature',
+ parameters: [
+ {
+ kind: 'Parameter',
+ name: {
+ kind: 'Identifier',
+ name: 'a',
+ },
+ type: {
+ kind: 'Number',
+ },
+ optional: false,
+ rest: false,
+ },
+ {
+ kind: 'Parameter',
+ name: {
+ kind: 'Identifier',
+ name: 'rest',
+ },
+ type: {
+ kind: 'TypeReference',
+ typeName: {
+ kind: 'Identifier',
+ name: 'Array',
+ },
+ typeArguments: [
+ {
+ kind: 'String',
+ },
+ ],
+ },
+ optional: false,
+ rest: true,
+ },
+ ],
+ returnType: {
+ kind: 'Void',
+ },
+ typeParameters: [],
+ },
+ ],
+ },
+ {
+ kind: 'FunctionDeclaration',
+ name: 't5',
+ signatures: [
+ {
+ kind: 'Signature',
+ parameters: [
+ {
+ kind: 'Parameter',
+ name: {
+ kind: 'ObjectBindingPattern',
+ elements: [
+ {
+ kind: 'BindingElement',
+ name: {
+ kind: 'Identifier',
+ name: 'a',
+ },
+ restProperty: false,
+ },
+ {
+ kind: 'BindingElement',
+ name: {
+ kind: 'Identifier',
+ name: 'renamed',
+ },
+ propertyName: {
+ kind: 'Identifier',
+ name: 'b',
+ },
+ restProperty: false,
+ },
+ {
+ kind: 'BindingElement',
+ name: {
+ kind: 'Identifier',
+ name: 'rest',
+ },
+ restProperty: true,
+ },
+ ],
+ },
+ type: {
+ kind: 'TypeLiteral',
+ members: [
+ {
+ kind: 'PropertySignature',
+ name: 'a',
+ optional: false,
+ type: {
+ kind: 'Number',
+ },
+ },
+ {
+ kind: 'PropertySignature',
+ name: 'b',
+ optional: false,
+ type: {
+ kind: 'Number',
+ },
+ },
+ {
+ kind: 'PropertySignature',
+ name: 'c',
+ optional: false,
+ type: {
+ kind: 'Number',
+ },
+ },
+ ],
+ },
+ optional: false,
+ rest: false,
+ },
+ ],
+ returnType: {
+ kind: 'Void',
+ },
+ typeParameters: [],
+ },
+ ],
+ },
+ {
+ kind: 'FunctionDeclaration',
+ name: 't6',
+ signatures: [
+ {
+ kind: 'Signature',
+ parameters: [
+ {
+ kind: 'Parameter',
+ name: {
+ kind: 'ArrayBindingPattern',
+ elements: [
+ {
+ kind: 'BindingElement',
+ name: {
+ kind: 'Identifier',
+ name: 'a',
+ },
+ restProperty: false,
+ },
+ {
+ kind: 'BindingElement',
+ name: {
+ kind: 'Identifier',
+ name: 'rest',
+ },
+ restProperty: true,
+ },
+ ],
+ },
+ type: {
+ kind: 'Tuple',
+ elements: [
+ {
+ kind: 'Number',
+ },
+ {
+ kind: 'Number',
+ },
+ ],
+ },
+ optional: false,
+ rest: false,
+ },
+ ],
+ returnType: {
+ kind: 'Void',
+ },
+ typeParameters: [],
+ },
+ ],
+ },
+ ],
+ })
+ })
+
+ test('type parameters', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export function t(a: T) {}
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'FunctionDeclaration',
+ name: 't',
+ signatures: [
+ {
+ kind: 'Signature',
+ parameters: [
+ {
+ kind: 'Parameter',
+ name: {
+ kind: 'Identifier',
+ name: 'a',
+ },
+ type: {
+ kind: 'TypeReference',
+ typeName: {
+ kind: 'Identifier',
+ name: 'T',
+ },
+ typeArguments: [],
+ },
+ optional: false,
+ rest: false,
+ },
+ ],
+ returnType: {
+ kind: 'Void',
+ },
+ typeParameters: [
+ {
+ kind: 'TypeParameter',
+ name: 'T',
+ constraint: {
+ kind: 'String',
+ },
+ default: {
+ kind: 'String',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ })
+ })
+
+ test('function type', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t1 = () => void
+ export type t2 = (a: number) => void
+ export type t3 = (a?: number) => void
+ export type t4 = (a: number, ...rest: Array) => void
+ export type t5 = ({ a, b, ...rest}: { a: number; b: number; c: number; }) => void
+ export type t6 = ([a, ...rest]: [number, number]) => void
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't1',
+ type: {
+ kind: 'Function',
+ parameters: [],
+ returnType: {
+ kind: 'Void',
+ },
+ typeParameters: [],
+ },
+ typeParameters: [],
+ },
+ {
+ kind: 'TypeAlias',
+ name: 't2',
+ type: {
+ kind: 'Function',
+ parameters: [
+ {
+ kind: 'Parameter',
+ name: {
+ kind: 'Identifier',
+ name: 'a',
+ },
+ type: {
+ kind: 'Number',
+ },
+ optional: false,
+ rest: false,
+ },
+ ],
+ returnType: {
+ kind: 'Void',
+ },
+ typeParameters: [],
+ },
+ typeParameters: [],
+ },
+ {
+ kind: 'TypeAlias',
+ name: 't3',
+ type: {
+ kind: 'Function',
+ parameters: [
+ {
+ kind: 'Parameter',
+ name: {
+ kind: 'Identifier',
+ name: 'a',
+ },
+ type: {
+ kind: 'Number',
+ },
+ optional: true,
+ rest: false,
+ },
+ ],
+ returnType: {
+ kind: 'Void',
+ },
+ typeParameters: [],
+ },
+ typeParameters: [],
+ },
+ {
+ kind: 'TypeAlias',
+ name: 't4',
+ type: {
+ kind: 'Function',
+ parameters: [
+ {
+ kind: 'Parameter',
+ name: {
+ kind: 'Identifier',
+ name: 'a',
+ },
+ type: {
+ kind: 'Number',
+ },
+ optional: false,
+ rest: false,
+ },
+ {
+ kind: 'Parameter',
+ name: {
+ kind: 'Identifier',
+ name: 'rest',
+ },
+ type: {
+ kind: 'TypeReference',
+ typeName: {
+ kind: 'Identifier',
+ name: 'Array',
+ },
+ typeArguments: [
+ {
+ kind: 'String',
+ },
+ ],
+ },
+ optional: false,
+ rest: true,
+ },
+ ],
+ returnType: {
+ kind: 'Void',
+ },
+ typeParameters: [],
+ },
+ typeParameters: [],
+ },
+ {
+ kind: 'TypeAlias',
+ name: 't5',
+ type: {
+ kind: 'Function',
+ parameters: [
+ {
+ kind: 'Parameter',
+ optional: false,
+ rest: false,
+ name: {
+ kind: 'ObjectBindingPattern',
+ elements: [
+ {
+ kind: 'BindingElement',
+ name: {
+ kind: 'Identifier',
+ name: 'a',
+ },
+ restProperty: false,
+ },
+ {
+ kind: 'BindingElement',
+ name: {
+ kind: 'Identifier',
+ name: 'b',
+ },
+ restProperty: false,
+ },
+ {
+ kind: 'BindingElement',
+ name: {
+ kind: 'Identifier',
+ name: 'rest',
+ },
+ restProperty: true,
+ },
+ ],
+ },
+ type: {
+ kind: 'TypeLiteral',
+ members: [
+ {
+ kind: 'PropertySignature',
+ name: 'a',
+ optional: false,
+ type: {
+ kind: 'Number',
+ },
+ },
+ {
+ kind: 'PropertySignature',
+ name: 'b',
+ optional: false,
+ type: {
+ kind: 'Number',
+ },
+ },
+ {
+ kind: 'PropertySignature',
+ name: 'c',
+ optional: false,
+ type: {
+ kind: 'Number',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ returnType: {
+ kind: 'Void',
+ },
+ typeParameters: [],
+ },
+ typeParameters: [],
+ },
+ {
+ kind: 'TypeAlias',
+ name: 't6',
+ type: {
+ kind: 'Function',
+ parameters: [
+ {
+ kind: 'Parameter',
+ name: {
+ kind: 'ArrayBindingPattern',
+ elements: [
+ {
+ kind: 'BindingElement',
+ name: {
+ kind: 'Identifier',
+ name: 'a',
+ },
+ restProperty: false,
+ },
+ {
+ kind: 'BindingElement',
+ name: {
+ kind: 'Identifier',
+ name: 'rest',
+ },
+ restProperty: true,
+ },
+ ],
+ },
+ type: {
+ kind: 'Tuple',
+ elements: [
+ {
+ kind: 'Number',
+ },
+ {
+ kind: 'Number',
+ },
+ ],
+ },
+ optional: false,
+ rest: false,
+ },
+ ],
+ returnType: {
+ kind: 'Void',
+ },
+ typeParameters: [],
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+})
+
+describe('type references', () => {
+ test('identifier', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = Array
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ type: {
+ kind: 'TypeReference',
+ typeName: {
+ kind: 'Identifier',
+ name: 'Array',
+ },
+ typeArguments: [
+ {
+ kind: 'String',
+ },
+ ],
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+
+ test('qualified name', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = React.ComponentPropsWithoutRef<'div'>
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ type: {
+ kind: 'TypeReference',
+ typeName: {
+ kind: 'QualifiedName',
+ left: {
+ kind: 'Identifier',
+ name: 'React',
+ },
+ right: {
+ kind: 'Identifier',
+ name: 'ComponentPropsWithoutRef',
+ },
+ },
+ typeArguments: [
+ {
+ kind: 'StringLiteral',
+ value: 'div',
+ },
+ ],
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+})
+
+describe('type aliases', () => {
+ test('type alias', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = string
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ type: {
+ kind: 'String',
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+
+ test('type alias with generics', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = string
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ typeParameters: [
+ {
+ kind: 'TypeParameter',
+ name: 'A',
+ constraint: undefined,
+ default: undefined,
+ },
+ {
+ kind: 'TypeParameter',
+ name: 'B',
+ constraint: {
+ kind: 'String',
+ },
+ default: undefined,
+ },
+ {
+ kind: 'TypeParameter',
+ name: 'C',
+ constraint: undefined,
+ default: {
+ kind: 'String',
+ },
+ },
+ ],
+ type: {
+ kind: 'String',
+ },
+ },
+ ],
+ })
+ })
+})
+
+describe('type literals', () => {
+ test('type literal', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = {
+ className: string
+ }
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ type: {
+ kind: 'TypeLiteral',
+ members: [
+ {
+ kind: 'PropertySignature',
+ name: 'className',
+ optional: false,
+ type: {
+ kind: 'String',
+ },
+ },
+ ],
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+
+ test('type literal with optional member', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = {
+ className?: string
+ }
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ type: {
+ kind: 'TypeLiteral',
+ members: [
+ {
+ kind: 'PropertySignature',
+ name: 'className',
+ optional: true,
+ type: {
+ kind: 'String',
+ },
+ },
+ ],
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+})
+
+describe('intersections', () => {
+ test('intersection', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = { open: boolean } & { className: string }
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ type: {
+ kind: 'Intersection',
+ members: [
+ {
+ kind: 'TypeLiteral',
+ members: [
+ {
+ kind: 'PropertySignature',
+ name: 'open',
+ optional: false,
+ type: {
+ kind: 'Boolean',
+ },
+ },
+ ],
+ },
+ {
+ kind: 'TypeLiteral',
+ members: [
+ {
+ kind: 'PropertySignature',
+ name: 'className',
+ optional: false,
+ type: {
+ kind: 'String',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+})
+
+describe('unions', () => {
+ test('union', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `
+ export type t = { type: 'a' } | { type: 'b' }
+ `,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'TypeAlias',
+ name: 't',
+ type: {
+ kind: 'Union',
+ members: [
+ {
+ kind: 'TypeLiteral',
+ members: [
+ {
+ kind: 'PropertySignature',
+ name: 'type',
+ optional: false,
+ type: {
+ kind: 'StringLiteral',
+ value: 'a',
+ },
+ },
+ ],
+ },
+ {
+ kind: 'TypeLiteral',
+ members: [
+ {
+ kind: 'PropertySignature',
+ name: 'type',
+ optional: false,
+ type: {
+ kind: 'StringLiteral',
+ value: 'b',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ typeParameters: [],
+ },
+ ],
+ })
+ })
+})
+
+describe('variable declarations', () => {
+ test('variable declaration', () => {
+ const program = getTypeScriptProgram({
+ 'test.ts': `export const t = 1;`,
+ })
+
+ const typeChecker = program.getTypeChecker()
+ const sourceFile = program.getSourceFile('test.ts')!
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+ expect(metadata).toEqual({
+ exports: [
+ {
+ kind: 'VariableDeclaration',
+ name: {
+ kind: 'Identifier',
+ name: 't',
+ },
+ type: {
+ kind: 'NumericLiteral',
+ value: '1',
+ },
+ },
+ ],
+ })
+ })
+})
+
+describe.skip('interfaces', () => {
+ //
+})
+
+describe.skip('export specifiers', () => {
+ //
+})
+
+describe.skip('unsupported', () => {
+ //
+})
+
+function getTypeScriptProgram(fsMap: Record): ts.Program {
+ const memfs = new Map()
+ for (const [fileName, content] of Object.entries(fsMap)) {
+ memfs.set(fileName, ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true))
+ }
+
+ const compilerOptions: ts.CompilerOptions = {
+ module: ts.ModuleKind.ESNext,
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
+ target: ts.ScriptTarget.Latest,
+ skipLibCheck: true,
+ strict: true,
+ isolatedModules: true,
+ esModuleInterop: true,
+ jsx: ts.JsxEmit.Preserve,
+ }
+ const host = ts.createCompilerHost(compilerOptions)
+ const program = ts.createProgram({
+ rootNames: Array.from(memfs.keys()),
+ options: compilerOptions,
+ host: {
+ ...host,
+ getSourceFile(fileName, ...args) {
+ if (memfs.has(fileName)) {
+ return memfs.get(fileName)
+ }
+ return host.getSourceFile(fileName, ...args)
+ },
+ },
+ })
+ const diagnostics = [
+ ...program.getSyntacticDiagnostics(),
+ ...program.getGlobalDiagnostics(),
+ ...program.getSemanticDiagnostics(),
+ ...program.getDeclarationDiagnostics(),
+ ]
+ if (diagnostics.length > 0) {
+ throw new Error(diagnostics.map(d => d.messageText).join('\n'))
+ }
+
+ return program
+}
diff --git a/packages/react-metadata/src/index.ts b/packages/react-metadata/src/index.ts
new file mode 100644
index 00000000000..82382f71b08
--- /dev/null
+++ b/packages/react-metadata/src/index.ts
@@ -0,0 +1,1203 @@
+import ts from 'typescript'
+import {isNodeExported} from './isNodeExported.ts'
+
+function getMetadataFromSourceFile(typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile): Metadata {
+ const metadata: Metadata = {
+ exports: [],
+ }
+
+ ts.forEachChild(sourceFile, node => {
+ if (!isNodeExported(node)) {
+ return
+ }
+
+ if (ts.isExportDeclaration(node)) {
+ if (ts.isExportDeclaration(node)) {
+ const exportClause = node.exportClause
+ if (!exportClause) {
+ return
+ }
+
+ if (ts.isNamedExports(exportClause)) {
+ for (const element of exportClause.elements) {
+ if (!ts.isExportSpecifier(element)) {
+ continue
+ }
+
+ // Get node for export specifier
+ const symbol = typeChecker.getExportSpecifierLocalTargetSymbol(element)
+ if (!symbol) {
+ continue
+ }
+
+ if (!symbol.declarations) {
+ continue
+ }
+
+ for (const declaration of symbol.declarations) {
+ const type = getTypeInfo(typeChecker, declaration)
+ metadata.exports.push({
+ kind: 'ExportSpecifier',
+ name: element.name.getText(),
+ type,
+ })
+ break
+ }
+ }
+ }
+ }
+ } else if (ts.isVariableStatement(node)) {
+ for (const declaration of node.declarationList.declarations) {
+ const type = getTypeInfo(typeChecker, declaration)
+ metadata.exports.push(type)
+ }
+ } else {
+ const type = getTypeInfo(typeChecker, node)
+ metadata.exports.push(type)
+ }
+ })
+
+ return metadata
+}
+
+type Metadata = {
+ exports: Array
+}
+
+type Type =
+ | {
+ kind: 'ExportSpecifier'
+ name: string
+ type: Type
+ }
+ | {
+ kind: 'TypeAlias'
+ name: string
+ typeParameters: Array
+ type: Type
+ }
+ | CallExpression
+ | PropertyAccessExpression
+ | TypeParameter
+ | VariableDeclaration
+ | {
+ kind: 'TypeLiteral'
+ members: Array
+ }
+ | PropertySignature
+ | {
+ kind: 'Intersection'
+ members: Array
+ }
+ | {
+ kind: 'Union'
+ members: Array
+ }
+ | {
+ kind: 'TypeReference'
+ typeName: QualifiedName | Identifier
+ typeArguments: Array
+ }
+ | FunctionDeclaration
+ | Function
+ | Parameter
+ | Identifier
+ | QualifiedName
+ | Tuple
+ | {
+ kind: 'BooleanLiteral'
+ value: string
+ }
+ | {
+ kind: 'StringLiteral'
+ value: string
+ }
+ | {
+ kind: 'NumericLiteral'
+ value: string
+ }
+ | {
+ kind: 'Boolean'
+ }
+ | {
+ kind: 'Number'
+ }
+ | {
+ kind: 'String'
+ }
+ | {
+ kind: 'Never'
+ }
+ | {
+ kind: 'Null'
+ }
+ | {
+ kind: 'Undefined'
+ }
+ | {
+ kind: 'Any'
+ }
+ | {
+ kind: 'Void'
+ }
+ | {
+ kind: 'Unknown'
+ }
+ | {
+ kind: 'Unsupported'
+ typeKind?: string
+ type: string
+ }
+
+type VariableDeclaration = {
+ kind: 'VariableDeclaration'
+ name: Identifier
+ type: Type
+}
+
+type PropertySignature = {
+ kind: 'PropertySignature'
+ name: string
+ optional: boolean
+ type: Type
+}
+
+type Identifier = {
+ kind: 'Identifier'
+ name: string
+}
+
+type QualifiedName = {
+ kind: 'QualifiedName'
+ left: QualifiedName | Identifier
+ right: Identifier
+}
+
+type TypeParameter = {
+ kind: 'TypeParameter'
+ name: string
+ constraint?: Type
+ default?: Type
+}
+
+type CallExpression = {
+ kind: 'CallExpression'
+ expression: Identifier | PropertyAccessExpression
+ typeArguments: Array
+ arguments: Array
+}
+
+type PropertyAccessExpression = {
+ kind: 'PropertyAccessExpression'
+ expression: Identifier | PropertyAccessExpression
+ name: Identifier
+}
+
+type FunctionDeclaration = {
+ kind: 'FunctionDeclaration'
+ name: string
+ signatures: Array
+}
+
+type Function = {
+ kind: 'Function'
+ parameters: Array
+ returnType: Type
+ typeParameters: Array
+}
+
+type Signature = {
+ kind: 'Signature'
+ parameters: Array
+ returnType: Type
+ typeParameters: Array
+}
+
+type Parameter = {
+ kind: 'Parameter'
+ name: Identifier | ObjectBindingPattern | ArrayBindingPattern
+ type: Type
+ optional: boolean
+ rest: boolean
+}
+
+type ObjectBindingPattern = {
+ kind: 'ObjectBindingPattern'
+ elements: Array
+}
+
+type BindingElement = {
+ kind: 'BindingElement'
+ propertyName?: Identifier
+ name: Identifier
+ restProperty: boolean
+}
+
+type ArrayBindingPattern = {
+ kind: 'ArrayBindingPattern'
+ elements: Array
+}
+
+type OmittedExpression = {
+ kind: 'OmittedExpression'
+}
+
+type Tuple = {
+ kind: 'Tuple'
+ elements: Array
+}
+
+function getTypeInfo(typeChecker: ts.TypeChecker, node: ts.Node): Type {
+ if (ts.isVariableDeclaration(node)) {
+ const type = node.type
+ ? getTypeInfo(typeChecker, node.type)
+ : node.initializer
+ ? getTypeInfo(typeChecker, node.initializer)
+ : ({kind: 'Any'} as const)
+
+ return {
+ kind: 'VariableDeclaration',
+ name: {
+ kind: 'Identifier',
+ name: node.name.getText(),
+ },
+ type,
+ }
+ }
+
+ if (ts.isFunctionDeclaration(node)) {
+ if (!node.name) {
+ return unsupported(typeChecker, node)
+ }
+
+ const symbol = typeChecker.getSymbolAtLocation(node.name)
+ if (!symbol) {
+ return unsupported(typeChecker, node)
+ }
+
+ const type = typeChecker.getTypeOfSymbolAtLocation(symbol, node)
+ const signatures: Array = []
+
+ for (const signature of type.getCallSignatures()) {
+ const returnType = getTypeInfoFromType(typeChecker, signature.getReturnType())
+ const parameters: Array = []
+ const typeParameters: Array = []
+
+ for (const parameter of signature.getParameters()) {
+ const valueDeclaration = parameter.valueDeclaration
+ if (!valueDeclaration) {
+ continue
+ }
+
+ const parameterDeclaration = valueDeclaration as ts.ParameterDeclaration
+ const parameterType = getTypeInfo(typeChecker, parameterDeclaration)
+
+ if (parameterType.kind === 'Parameter') {
+ parameters.push(parameterType)
+ }
+ }
+
+ if (signature.typeParameters) {
+ for (const typeParameter of signature.typeParameters) {
+ const typeParameterType = getTypeInfoFromType(typeChecker, typeParameter)
+ if (typeParameterType.kind === 'TypeParameter') {
+ typeParameters.push(typeParameterType)
+ }
+ }
+ }
+
+ signatures.push({
+ kind: 'Signature',
+ parameters,
+ returnType,
+ typeParameters,
+ })
+ }
+
+ return {
+ kind: 'FunctionDeclaration',
+ name: node.name.getText(),
+ signatures,
+ }
+ }
+
+ if (ts.isFunctionTypeNode(node)) {
+ const parameters: Array = []
+
+ for (const parameter of node.parameters) {
+ const parameterType = getTypeInfo(typeChecker, parameter)
+ if (parameterType.kind === 'Parameter') {
+ parameters.push(parameterType)
+ }
+ }
+
+ const typeParameters: Array = []
+
+ if (node.typeParameters) {
+ for (const typeParameter of node.typeParameters) {
+ const typeParameterType = getTypeInfo(typeChecker, typeParameter)
+ if (typeParameterType.kind === 'TypeParameter') {
+ typeParameters.push(typeParameterType)
+ }
+ }
+ }
+
+ return {
+ kind: 'Function',
+ parameters,
+ returnType: getTypeInfo(typeChecker, node.type),
+ typeParameters,
+ }
+ }
+
+ if (ts.isFunctionExpression(node)) {
+ if (!node.name) {
+ return unsupported(typeChecker, node)
+ }
+
+ const symbol = typeChecker.getSymbolAtLocation(node.name)
+ if (!symbol) {
+ return unsupported(typeChecker, node)
+ }
+
+ const type = typeChecker.getTypeOfSymbolAtLocation(symbol, node)
+ const signatures: Array = []
+
+ for (const signature of type.getCallSignatures()) {
+ const returnType = getTypeInfoFromType(typeChecker, signature.getReturnType())
+ const parameters: Array = []
+ const typeParameters: Array = []
+
+ for (const parameter of signature.getParameters()) {
+ const valueDeclaration = parameter.valueDeclaration
+ if (!valueDeclaration) {
+ continue
+ }
+
+ const parameterDeclaration = valueDeclaration as ts.ParameterDeclaration
+ const parameterType = getTypeInfo(typeChecker, parameterDeclaration)
+
+ if (parameterType.kind === 'Parameter') {
+ parameters.push(parameterType)
+ }
+ }
+
+ if (signature.typeParameters) {
+ for (const typeParameter of signature.typeParameters) {
+ const typeParameterType = getTypeInfoFromType(typeChecker, typeParameter)
+ if (typeParameterType.kind === 'TypeParameter') {
+ typeParameters.push(typeParameterType)
+ }
+ }
+ }
+
+ signatures.push({
+ kind: 'Signature',
+ parameters,
+ returnType,
+ typeParameters,
+ })
+ }
+
+ return {
+ kind: 'FunctionDeclaration',
+ name: node.name.getText(),
+ signatures,
+ }
+ }
+
+ if (ts.isArrowFunction(node)) {
+ const symbol = typeChecker.getSymbolAtLocation(node)
+ if (!symbol) {
+ return unsupported(typeChecker, node)
+ }
+
+ const type = typeChecker.getTypeOfSymbolAtLocation(symbol, node)
+ const signatures: Array = []
+
+ for (const signature of type.getCallSignatures()) {
+ const returnType = getTypeInfoFromType(typeChecker, signature.getReturnType())
+ const parameters: Array = []
+ const typeParameters: Array = []
+
+ for (const parameter of signature.getParameters()) {
+ const valueDeclaration = parameter.valueDeclaration
+ if (!valueDeclaration) {
+ continue
+ }
+
+ const parameterDeclaration = valueDeclaration as ts.ParameterDeclaration
+ const parameterType = getTypeInfo(typeChecker, parameterDeclaration)
+
+ if (parameterType.kind === 'Parameter') {
+ parameters.push(parameterType)
+ }
+ }
+
+ if (signature.typeParameters) {
+ for (const typeParameter of signature.typeParameters) {
+ const typeParameterType = getTypeInfoFromType(typeChecker, typeParameter)
+ if (typeParameterType.kind === 'TypeParameter') {
+ typeParameters.push(typeParameterType)
+ }
+ }
+ }
+
+ signatures.push({
+ kind: 'Signature',
+ parameters,
+ returnType,
+ typeParameters,
+ })
+ }
+
+ return {
+ kind: 'FunctionDeclaration',
+ name: node.name.getText(),
+ signatures,
+ }
+ }
+
+ if (ts.isInterfaceDeclaration(node)) {
+ // ...
+ }
+
+ if (ts.isTypeAliasDeclaration(node)) {
+ const typeParameters: Array = []
+
+ if (node.typeParameters) {
+ for (const typeParameter of node.typeParameters) {
+ const type = getTypeInfo(typeChecker, typeParameter)
+ if (type.kind === 'TypeParameter') {
+ typeParameters.push(type)
+ }
+ }
+ }
+
+ return {
+ kind: 'TypeAlias',
+ name: node.name.getText(),
+ typeParameters,
+ type: getTypeInfo(typeChecker, node.type),
+ }
+ }
+
+ if (ts.isTypeParameterDeclaration(node)) {
+ return {
+ kind: 'TypeParameter',
+ name: node.name.getText(),
+ constraint: node.constraint ? getTypeInfo(typeChecker, node.constraint) : undefined,
+ default: node.default ? getTypeInfo(typeChecker, node.default) : undefined,
+ }
+ }
+
+ if (ts.isIntersectionTypeNode(node)) {
+ return {
+ kind: 'Intersection',
+ members: node.types.map(type => getTypeInfo(typeChecker, type)),
+ }
+ }
+
+ if (ts.isUnionTypeNode(node)) {
+ return {
+ kind: 'Union',
+ members: node.types.map(type => getTypeInfo(typeChecker, type)),
+ }
+ }
+
+ if (ts.isTypeLiteralNode(node)) {
+ const typeNode = node as ts.TypeLiteralNode
+ const members: Array = []
+
+ ts.forEachChild(typeNode, child => {
+ if (ts.isPropertySignature(child)) {
+ const type = getTypeInfo(typeChecker, child)
+ if (type.kind === 'PropertySignature') {
+ members.push(type)
+ }
+ }
+ })
+
+ return {
+ kind: 'TypeLiteral',
+ members,
+ }
+ }
+
+ if (ts.isPropertySignature(node)) {
+ const propertySignature = node as ts.PropertySignature
+ if (!propertySignature.type) {
+ return {
+ kind: 'Unsupported',
+ type: typeChecker.typeToString(
+ typeChecker.getTypeAtLocation(propertySignature),
+ undefined,
+ ts.TypeFormatFlags.NoTypeReduction | ts.TypeFormatFlags.WriteArrayAsGenericType,
+ ),
+ }
+ }
+
+ return {
+ kind: 'PropertySignature',
+ name: propertySignature.name.getText(),
+ optional: !!propertySignature.questionToken,
+ type: getTypeInfo(typeChecker, propertySignature.type),
+ }
+ }
+
+ if (ts.isCallExpression(node)) {
+ const expression = getTypeInfo(typeChecker, node.expression)
+ if (expression.kind === 'Identifier' || expression.kind === 'PropertyAccessExpression') {
+ const args: Array = []
+
+ for (const arg of node.arguments) {
+ const type = getTypeInfo(typeChecker, arg)
+ args.push(type)
+ }
+
+ const typeArguments: Array = []
+
+ if (node.typeArguments) {
+ for (const typeArgument of node.typeArguments) {
+ const type = getTypeInfo(typeChecker, typeArgument)
+ if (type.kind === 'TypeReference') {
+ typeArguments.push(type)
+ }
+ }
+ }
+
+ return {
+ kind: 'CallExpression',
+ expression,
+ typeArguments,
+ arguments: args,
+ }
+ }
+ }
+
+ if (ts.isPropertyAccessExpression(node)) {
+ const expression = getTypeInfo(typeChecker, node.expression)
+ if (expression.kind === 'Identifier' || expression.kind === 'PropertyAccessExpression') {
+ return {
+ kind: 'PropertyAccessExpression',
+ expression,
+ name: {
+ kind: 'Identifier',
+ name: node.name.getText(),
+ },
+ }
+ }
+ }
+
+ if (ts.isParameter(node)) {
+ if (node.name.kind === ts.SyntaxKind.ObjectBindingPattern) {
+ return {
+ kind: 'Parameter',
+ name: {
+ kind: 'ObjectBindingPattern',
+ elements: node.name.elements.map(element => {
+ if (element.propertyName) {
+ return {
+ kind: 'BindingElement',
+ propertyName: {
+ kind: 'Identifier',
+ name: element.propertyName.getText(),
+ },
+ name: {
+ kind: 'Identifier',
+ name: element.name.getText(),
+ },
+ restProperty: !!element.dotDotDotToken,
+ }
+ }
+
+ return {
+ kind: 'BindingElement',
+ name: {
+ kind: 'Identifier',
+ name: element.name.getText(),
+ },
+ restProperty: !!element.dotDotDotToken,
+ }
+ }),
+ },
+ type: node.type ? getTypeInfo(typeChecker, node.type) : {kind: 'Any'},
+ optional: !!node.questionToken,
+ rest: !!node.dotDotDotToken,
+ }
+ }
+
+ if (node.name.kind === ts.SyntaxKind.ArrayBindingPattern) {
+ return {
+ kind: 'Parameter',
+ name: {
+ kind: 'ArrayBindingPattern',
+ elements: node.name.elements.map(element => {
+ if (ts.isOmittedExpression(element)) {
+ return {
+ kind: 'OmittedExpression',
+ }
+ }
+ return {
+ kind: 'BindingElement',
+ name: {
+ kind: 'Identifier',
+ name: element.name.getText(),
+ },
+ restProperty: !!element.dotDotDotToken,
+ }
+ }),
+ },
+ type: node.type ? getTypeInfo(typeChecker, node.type) : {kind: 'Any'},
+ optional: !!node.questionToken,
+ rest: !!node.dotDotDotToken,
+ }
+ }
+
+ return {
+ kind: 'Parameter',
+ name: {
+ kind: 'Identifier',
+ name: node.name.getText(),
+ },
+ type: node.type ? getTypeInfo(typeChecker, node.type) : {kind: 'Any'},
+ optional: !!node.questionToken,
+ rest: !!node.dotDotDotToken,
+ }
+ }
+
+ if (ts.isTypeReferenceNode(node)) {
+ const typeName = getTypeInfo(typeChecker, node.typeName)
+ if (typeName.kind === 'Identifier' || typeName.kind === 'QualifiedName') {
+ return {
+ kind: 'TypeReference',
+ typeName,
+ typeArguments: node.typeArguments ? node.typeArguments.map(type => getTypeInfo(typeChecker, type)) : [],
+ }
+ }
+ }
+
+ if (ts.isTupleTypeNode(node)) {
+ return {
+ kind: 'Tuple',
+ elements: node.elements.map(element => {
+ return getTypeInfo(typeChecker, element)
+ }),
+ }
+ }
+
+ if (ts.isIdentifier(node)) {
+ return {
+ kind: 'Identifier',
+ name: node.text,
+ }
+ }
+
+ if (ts.isQualifiedName(node)) {
+ const left = getTypeInfo(typeChecker, node.left)
+ const right = getTypeInfo(typeChecker, node.right)
+
+ if (left.kind === 'Identifier' || left.kind === 'QualifiedName') {
+ if (right.kind === 'Identifier') {
+ return {
+ kind: 'QualifiedName',
+ left,
+ right,
+ }
+ }
+ }
+ }
+
+ if (ts.isLiteralTypeNode(node)) {
+ return getTypeInfo(typeChecker, node.literal)
+ }
+
+ if (ts.isStringLiteral(node)) {
+ return {
+ kind: 'StringLiteral',
+ value: node.text,
+ }
+ }
+
+ if (ts.isNumericLiteral(node)) {
+ return {
+ kind: 'NumericLiteral',
+ value: node.text,
+ }
+ }
+
+ if (node.kind === ts.SyntaxKind.TrueKeyword) {
+ return {
+ kind: 'BooleanLiteral',
+ value: 'true',
+ }
+ }
+
+ if (node.kind === ts.SyntaxKind.FalseKeyword) {
+ return {
+ kind: 'BooleanLiteral',
+ value: 'false',
+ }
+ }
+
+ if (node.kind === ts.SyntaxKind.NullKeyword) {
+ return {
+ kind: 'Null',
+ }
+ }
+
+ if (node.kind === ts.SyntaxKind.UndefinedKeyword) {
+ return {
+ kind: 'Undefined',
+ }
+ }
+
+ if (node.kind === ts.SyntaxKind.AnyKeyword) {
+ return {
+ kind: 'Any',
+ }
+ }
+
+ if (node.kind === ts.SyntaxKind.UnknownKeyword) {
+ return {
+ kind: 'Unknown',
+ }
+ }
+
+ if (node.kind === ts.SyntaxKind.NeverKeyword) {
+ return {
+ kind: 'Never',
+ }
+ }
+
+ if (node.kind === ts.SyntaxKind.StringKeyword) {
+ return {
+ kind: 'String',
+ }
+ }
+
+ if (node.kind === ts.SyntaxKind.NumberKeyword) {
+ return {
+ kind: 'Number',
+ }
+ }
+
+ if (node.kind === ts.SyntaxKind.BooleanKeyword) {
+ return {
+ kind: 'Boolean',
+ }
+ }
+
+ if (node.kind === ts.SyntaxKind.VoidKeyword) {
+ return {
+ kind: 'Void',
+ }
+ }
+
+ return {
+ kind: 'Unsupported',
+ typeKind: ts.SyntaxKind[node.kind],
+ type: typeChecker.typeToString(
+ typeChecker.getTypeAtLocation(node),
+ undefined,
+ ts.TypeFormatFlags.WriteArrayAsGenericType |
+ ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope |
+ ts.TypeFormatFlags.NoTypeReduction,
+ ),
+ }
+}
+
+function unsupported(typeChecker: ts.TypeChecker, node: ts.Node) {
+ return {
+ kind: 'Unsupported',
+ typeKind: ts.SyntaxKind[node.kind],
+ type: typeChecker.typeToString(
+ typeChecker.getTypeAtLocation(node),
+ undefined,
+ ts.TypeFormatFlags.WriteArrayAsGenericType |
+ ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope |
+ ts.TypeFormatFlags.NoTypeReduction,
+ ),
+ } as const
+}
+
+function getTypeInfoFromType(typeChecker: ts.TypeChecker, type: ts.Type): Type {
+ if (type.flags & ts.TypeFlags.BooleanLiteral) {
+ return {
+ kind: 'BooleanLiteral',
+ value: typeChecker.typeToString(type),
+ }
+ }
+
+ if (type.flags & ts.TypeFlags.StringLiteral) {
+ return {
+ kind: 'StringLiteral',
+ // When using typeToString, the string literal is wrapped in quotes
+ value: typeChecker.typeToString(type).slice(1, -1),
+ }
+ }
+
+ if (type.flags & ts.TypeFlags.NumberLiteral) {
+ return {
+ kind: 'NumericLiteral',
+ value: typeChecker.typeToString(type),
+ }
+ }
+
+ if (type.flags & ts.TypeFlags.Boolean) {
+ return {
+ kind: 'Boolean',
+ }
+ }
+
+ if (type.flags & ts.TypeFlags.Number) {
+ return {
+ kind: 'Number',
+ }
+ }
+
+ if (type.flags & ts.TypeFlags.String) {
+ return {
+ kind: 'String',
+ }
+ }
+
+ if (type.flags & ts.TypeFlags.Any) {
+ return {
+ kind: 'Any',
+ }
+ }
+
+ if (type.flags & ts.TypeFlags.Never) {
+ return {
+ kind: 'Never',
+ }
+ }
+
+ if (type.flags & ts.TypeFlags.Null) {
+ return {
+ kind: 'Null',
+ }
+ }
+
+ if (type.flags & ts.TypeFlags.Undefined) {
+ return {
+ kind: 'Undefined',
+ }
+ }
+
+ if (type.flags & ts.TypeFlags.Unknown) {
+ return {
+ kind: 'Unknown',
+ }
+ }
+
+ if (type.flags & ts.TypeFlags.Void) {
+ return {
+ kind: 'Void',
+ }
+ }
+
+ if (type.flags & ts.TypeFlags.Union) {
+ const unionType = type as ts.UnionType
+ return {
+ kind: 'Union',
+ members: unionType.types.map(type => getTypeInfoFromType(typeChecker, type)),
+ }
+ }
+
+ if (type.flags & ts.TypeFlags.Intersection) {
+ const intersectionType = type as ts.IntersectionType
+ return {
+ kind: 'Intersection',
+ members: intersectionType.types.map(type => getTypeInfoFromType(typeChecker, type)),
+ }
+ }
+
+ if (type.flags & ts.TypeFlags.TypeParameter) {
+ const constraint = type.getConstraint()
+ const defaultType = type.getDefault()
+
+ return {
+ kind: 'TypeParameter',
+ name: typeChecker.typeToString(type),
+ constraint: constraint ? getTypeInfoFromType(typeChecker, constraint) : undefined,
+ default: defaultType ? getTypeInfoFromType(typeChecker, defaultType) : undefined,
+ }
+ }
+
+ if (type.flags & ts.TypeFlags.Object) {
+ const objectType = type as ts.ObjectType
+
+ if (objectType.objectFlags & ts.ObjectFlags.Reference) {
+ const typeReference = objectType as ts.TypeReference
+ const target = typeReference.target
+
+ return {
+ kind: 'TypeReference',
+ typeName: getSymbolName(typeChecker, target.symbol),
+ typeArguments: Array.isArray(typeReference.typeArguments)
+ ? typeReference.typeArguments.map(type => {
+ return getTypeInfoFromType(typeChecker, type)
+ })
+ : [],
+ }
+ } else if (objectType.objectFlags & ts.ObjectFlags.Anonymous) {
+ const members: Array = []
+
+ objectType.getProperties().forEach(property => {
+ const declaration = property.valueDeclaration
+ if (!declaration) {
+ return
+ }
+
+ const type = getTypeInfoFromType(typeChecker, typeChecker.getTypeOfSymbolAtLocation(property, declaration))
+ members.push({
+ kind: 'PropertySignature',
+ name: property.name,
+ optional: !!(declaration as ts.PropertySignature).questionToken,
+ type,
+ })
+ })
+
+ return {
+ kind: 'TypeLiteral',
+ members,
+ }
+ }
+ }
+
+ return unsupportedType(typeChecker, type)
+}
+
+function getSymbolName(typeChecker: ts.TypeChecker, symbol: ts.Symbol): QualifiedName | Identifier {
+ const name = typeChecker.getFullyQualifiedName(symbol)
+ const parts = name.split('.')
+
+ return getQualifiedName(parts)
+}
+
+function getQualifiedName(parts: Array): QualifiedName | Identifier {
+ if (parts.length === 1) {
+ return {
+ kind: 'Identifier',
+ name: parts[0],
+ }
+ }
+
+ if (parts.length > 1) {
+ return {
+ kind: 'QualifiedName',
+ left: getQualifiedName(parts.slice(0, -1)),
+ right: {
+ kind: 'Identifier',
+ name: parts[parts.length - 1],
+ },
+ }
+ }
+
+ throw new Error('Invalid qualified name')
+}
+
+function unsupportedType(typeChecker: ts.TypeChecker, type: ts.Type) {
+ return {
+ kind: 'Unsupported',
+ typeKind: ts.TypeFlags[type.flags],
+ type: typeChecker.typeToString(
+ type,
+ undefined,
+ ts.TypeFormatFlags.WriteArrayAsGenericType |
+ ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope |
+ ts.TypeFormatFlags.NoTypeReduction,
+ ),
+ } as const
+}
+
+function format(type: Type): string {
+ if (type.kind === 'ExportSpecifier') {
+ return `${type.name}: ${format(type.type)}`
+ }
+
+ if (type.kind === 'TypeAlias') {
+ return `type ${type.name}${formatTypeParameters(type.typeParameters)} = ${format(type.type)}`
+ }
+
+ if (type.kind === 'VariableDeclaration') {
+ return `const ${type.name.name}: ${format(type.type)}`
+ }
+
+ if (type.kind === 'TypeReference') {
+ const typeArguments = type.typeArguments.length > 0 ? formatTypeParameters(type.typeArguments) : ''
+ return `${format(type.typeName)}${typeArguments}`
+ }
+
+ if (type.kind === 'CallExpression') {
+ const typeArguments = type.typeArguments.length > 0 ? formatTypeParameters(type.typeArguments) : ''
+ const args = type.arguments.length > 0 ? `(${type.arguments.map(format).join(', ')})` : ''
+ return `${format(type.expression)}${typeArguments}${args}`
+ }
+
+ if (type.kind === 'PropertyAccessExpression') {
+ return `${format(type.expression)}.${type.name.name}`
+ }
+
+ if (type.kind === 'Identifier') {
+ return type.name
+ }
+
+ if (type.kind === 'QualifiedName') {
+ return `${format(type.left)}.${type.right.name}`
+ }
+
+ if (type.kind === 'TypeParameter') {
+ const constraint = type.constraint ? ` extends ${format(type.constraint)}` : ''
+ const defaultType = type.default ? ` = ${format(type.default)}` : ''
+ return `${type.name}${constraint}${defaultType}`
+ }
+
+ if (type.kind === 'PropertySignature') {
+ return `${type.name}${type.optional ? '?' : ''}: ${format(type.type)}`
+ }
+
+ if (type.kind === 'TypeLiteral') {
+ return `{ ${type.members.map(format).join(', ')} }`
+ }
+
+ if (type.kind === 'Intersection') {
+ return type.members.map(format).join(' & ')
+ }
+
+ if (type.kind === 'Union') {
+ return type.members.map(format).join(' | ')
+ }
+
+ if (type.kind === 'Tuple') {
+ return `[${type.elements.map(format).join(', ')}]`
+ }
+
+ if (type.kind === 'BooleanLiteral') {
+ return type.value
+ }
+
+ if (type.kind === 'StringLiteral') {
+ return `"${type.value}"`
+ }
+
+ if (type.kind === 'NumericLiteral') {
+ return type.value
+ }
+
+ if (type.kind === 'Boolean') {
+ return 'boolean'
+ }
+
+ if (type.kind === 'Number') {
+ return 'number'
+ }
+
+ if (type.kind === 'String') {
+ return 'string'
+ }
+
+ if (type.kind === 'Never') {
+ return 'never'
+ }
+
+ if (type.kind === 'Null') {
+ return 'null'
+ }
+
+ if (type.kind === 'Undefined') {
+ return 'undefined'
+ }
+
+ if (type.kind === 'Any') {
+ return 'any'
+ }
+
+ if (type.kind === 'Void') {
+ return 'void'
+ }
+
+ if (type.kind === 'Unknown') {
+ return 'unknown'
+ }
+
+ if (type.kind === 'Function') {
+ const parameters = type.parameters.map(parameter => {
+ return `${parameter.name.name}${parameter.optional ? '?' : ''}: ${format(parameter.type)}`
+ })
+ const typeParameters = formatTypeParameters(type.typeParameters)
+ return `(${parameters.join(', ')}) => ${format(type.returnType)}`
+ }
+
+ if (type.kind === 'Signature') {
+ const parameters = type.parameters.map(parameter => {
+ return `${parameter.name.name}${parameter.optional ? '?' : ''}: ${format(parameter.type)}`
+ })
+ const typeParameters = formatTypeParameters(type.typeParameters)
+ return `(${parameters.join(', ')}) => ${format(type.returnType)}`
+ }
+
+ if (type.kind === 'Parameter') {
+ if (type.name.kind === 'ObjectBindingPattern') {
+ const elements = type.name.elements.map(element => {
+ if (element.propertyName) {
+ return `${element.propertyName.name}: ${element.name.name}`
+ }
+ return `${element.name.name}`
+ })
+ return `{ ${elements.join(', ')} }${type.optional ? '?' : ''}: ${format(type.type)}`
+ }
+
+ if (type.name.kind === 'ArrayBindingPattern') {
+ const elements = type.name.elements.map(element => {
+ if (element.kind === 'OmittedExpression') {
+ return `...`
+ }
+ return `${element.name.name}`
+ })
+ return `[${elements.join(', ')}]${type.optional ? '?' : ''}: ${format(type.type)}`
+ }
+
+ return `${type.name.name}${type.optional ? '?' : ''}: ${format(type.type)}`
+ }
+
+ if (type.kind === 'FunctionDeclaration') {
+ const signatures = type.signatures.map(signature => {
+ const parameters = signature.parameters.map(parameter => {
+ return `${parameter.name.name}${parameter.optional ? '?' : ''}: ${format(parameter.type)}`
+ })
+ const typeParameters = formatTypeParameters(signature.typeParameters)
+ return `(${parameters.join(', ')}) => ${format(signature.returnType)}`
+ })
+ return `function ${type.name}(${signatures.join(' | ')})`
+ }
+
+ if (type.kind === 'Unsupported') {
+ return type.type
+ }
+
+ exhaustiveCheck(type)
+}
+
+function formatTypeParameters(typeParameters: Array): string {
+ if (typeParameters.length === 0) {
+ return ''
+ }
+
+ const formattedTypeParameters = typeParameters.map(typeParameter => {
+ const constraint = typeParameter.constraint ? ` extends ${format(typeParameter.constraint)}` : ''
+ const defaultType = typeParameter.default ? ` = ${format(typeParameter.default)}` : ''
+ return `${typeParameter.name}${constraint}${defaultType}`
+ })
+
+ return `<${formattedTypeParameters.join(', ')}>`
+}
+
+function exhaustiveCheck(value: never): never {
+ throw new Error(`Unexpected value: ${value}`)
+}
+
+export {getMetadataFromSourceFile, format}
+export type {Metadata, Type}
diff --git a/packages/react-metadata/src/isNodeExported.ts b/packages/react-metadata/src/isNodeExported.ts
new file mode 100644
index 00000000000..02fb0833c9e
--- /dev/null
+++ b/packages/react-metadata/src/isNodeExported.ts
@@ -0,0 +1,40 @@
+import ts from 'typescript'
+
+function isNodeExported(node: ts.Node): boolean {
+ // export default
+ if (ts.isExportAssignment(node)) {
+ return true
+ }
+
+ // export { a }
+ // export { a as b }
+ // export { a } from 'mod'
+ // expor type { a }
+ if (ts.isExportDeclaration(node)) {
+ return true
+ }
+
+ // export const a = 1
+ // export function a() {}
+ // export class A {}
+ // export interface A {}
+ // export type A = {}
+ // export enum A {}
+ if (
+ ts.isVariableStatement(node) ||
+ ts.isFunctionDeclaration(node) ||
+ ts.isClassDeclaration(node) ||
+ ts.isInterfaceDeclaration(node) ||
+ ts.isTypeAliasDeclaration(node) ||
+ ts.isEnumDeclaration(node)
+ ) {
+ const exportKeyword = node.modifiers?.find(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
+ if (exportKeyword) {
+ return true
+ }
+ }
+
+ return false
+}
+
+export {isNodeExported}
diff --git a/packages/react-metadata/tsconfig.json b/packages/react-metadata/tsconfig.json
new file mode 100644
index 00000000000..564a5990051
--- /dev/null
+++ b/packages/react-metadata/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src"]
+}
diff --git a/packages/react-metadata/vitest.config.ts b/packages/react-metadata/vitest.config.ts
new file mode 100644
index 00000000000..e2d1588e1f2
--- /dev/null
+++ b/packages/react-metadata/vitest.config.ts
@@ -0,0 +1,7 @@
+import {defineConfig} from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ include: ['**/*.test.ts'],
+ },
+})
diff --git a/packages/react/script/generate-metadata.mts b/packages/react/script/generate-metadata.mts
new file mode 100644
index 00000000000..e37440e589d
--- /dev/null
+++ b/packages/react/script/generate-metadata.mts
@@ -0,0 +1,25 @@
+import path from 'node:path'
+import {getMetadataFromSourceFile, format} from '@primer/react-metadata'
+import ts from 'typescript'
+
+async function main() {
+ const config = ts.readConfigFile('tsconfig.json', ts.sys.readFile)
+ const parsedConfig = ts.parseJsonConfigFileContent(config.config, ts.sys, process.cwd(), undefined, 'tsconfig.json')
+ const program = ts.createProgram({
+ rootNames: parsedConfig.fileNames,
+ options: parsedConfig.options,
+ })
+ const typeChecker = program.getTypeChecker()
+ // const sourceFile = program.getSourceFile(path.resolve('./src/Banner/Banner.tsx'))
+ const sourceFile = program.getSourceFile(path.resolve('./src/index.ts'))
+ const metadata = getMetadataFromSourceFile(typeChecker, sourceFile)
+
+ for (const exportInfo of metadata.exports) {
+ console.log(format(exportInfo))
+ }
+}
+
+main().catch(error => {
+ console.log(error)
+ process.exit(1)
+})