Skip to content

Commit f25dd3d

Browse files
committed
chore: wip
1 parent 9e465a7 commit f25dd3d

File tree

2 files changed

+295
-14
lines changed

2 files changed

+295
-14
lines changed

src/extractor.ts

Lines changed: 235 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,6 @@
11
import * as ts from 'typescript'
22
import type { Declaration } from './types'
33

4-
// Performance optimization: Cache compiled regexes
5-
const DECLARATION_PATTERNS = {
6-
import: /^import\s+/m,
7-
export: /^export\s+/m,
8-
function: /^(export\s+)?(async\s+)?function\s+/m,
9-
variable: /^(export\s+)?(const|let|var)\s+/m,
10-
interface: /^(export\s+)?interface\s+/m,
11-
type: /^(export\s+)?type\s+/m,
12-
class: /^(export\s+)?(abstract\s+)?class\s+/m,
13-
enum: /^(export\s+)?(const\s+)?enum\s+/m,
14-
module: /^(export\s+)?(declare\s+)?(module|namespace)\s+/m
15-
} as const
16-
174
/**
185
* Extract only public API declarations from TypeScript source code
196
* This focuses on what should be in .d.ts files, not implementation details
@@ -643,7 +630,9 @@ function extractEnumDeclaration(node: ts.EnumDeclaration, sourceCode: string): D
643630
function extractModuleDeclaration(node: ts.ModuleDeclaration, sourceCode: string): Declaration {
644631
const name = node.name.getText()
645632
const isExported = hasExportModifier(node)
646-
const text = getNodeText(node, sourceCode)
633+
634+
// Build clean module declaration for DTS
635+
const text = buildModuleDeclaration(node, isExported)
647636

648637
// Check if this is an ambient module (quoted name)
649638
const isAmbient = ts.isStringLiteral(node.name)
@@ -659,6 +648,238 @@ function extractModuleDeclaration(node: ts.ModuleDeclaration, sourceCode: string
659648
}
660649
}
661650

651+
/**
652+
* Build clean module declaration for DTS
653+
*/
654+
function buildModuleDeclaration(node: ts.ModuleDeclaration, isExported: boolean): string {
655+
let result = ''
656+
657+
// Add export if needed
658+
if (isExported) {
659+
result += 'export '
660+
}
661+
662+
// Add declare keyword
663+
result += 'declare '
664+
665+
// Check if this is a namespace or module
666+
const isNamespace = node.flags & ts.NodeFlags.Namespace
667+
if (isNamespace) {
668+
result += 'namespace '
669+
} else {
670+
result += 'module '
671+
}
672+
673+
// Add module name
674+
result += node.name.getText()
675+
676+
// Build module body with only signatures
677+
result += ' ' + buildModuleBody(node)
678+
679+
return result
680+
}
681+
682+
/**
683+
* Build clean module body for DTS (signatures only, no implementations)
684+
*/
685+
function buildModuleBody(node: ts.ModuleDeclaration): string {
686+
if (!node.body) return '{}'
687+
688+
const members: string[] = []
689+
690+
function processModuleElement(element: ts.Node) {
691+
if (ts.isFunctionDeclaration(element)) {
692+
// Function signature without implementation (no declare keyword in ambient context)
693+
const isExported = hasExportModifier(element)
694+
const name = element.name?.getText() || ''
695+
696+
let signature = ' '
697+
if (isExported) signature += 'export '
698+
signature += 'function '
699+
signature += name
700+
701+
// Add generics
702+
if (element.typeParameters) {
703+
const generics = element.typeParameters.map(tp => tp.getText()).join(', ')
704+
signature += `<${generics}>`
705+
}
706+
707+
// Add parameters
708+
const params = element.parameters.map(param => {
709+
const paramName = getParameterName(param)
710+
const paramType = param.type?.getText() || 'any'
711+
const optional = param.questionToken || param.initializer ? '?' : ''
712+
return `${paramName}${optional}: ${paramType}`
713+
}).join(', ')
714+
signature += `(${params})`
715+
716+
// Add return type
717+
const returnType = element.type?.getText() || 'void'
718+
signature += `: ${returnType};`
719+
720+
members.push(signature)
721+
} else if (ts.isVariableStatement(element)) {
722+
// Variable declarations
723+
const isExported = hasExportModifier(element)
724+
for (const declaration of element.declarationList.declarations) {
725+
if (declaration.name && ts.isIdentifier(declaration.name)) {
726+
const name = declaration.name.getText()
727+
const typeAnnotation = declaration.type?.getText()
728+
const initializer = declaration.initializer?.getText()
729+
const kind = element.declarationList.flags & ts.NodeFlags.Const ? 'const' :
730+
element.declarationList.flags & ts.NodeFlags.Let ? 'let' : 'var'
731+
732+
let varDecl = ' '
733+
if (isExported) varDecl += 'export '
734+
varDecl += kind + ' '
735+
varDecl += name
736+
737+
// Use type annotation if available, otherwise infer from initializer
738+
if (typeAnnotation) {
739+
varDecl += `: ${typeAnnotation}`
740+
} else if (initializer) {
741+
// Simple type inference for common cases
742+
if (initializer.startsWith("'") || initializer.startsWith('"') || initializer.startsWith('`')) {
743+
varDecl += ': string'
744+
} else if (/^\d+$/.test(initializer)) {
745+
varDecl += ': number'
746+
} else if (initializer === 'true' || initializer === 'false') {
747+
varDecl += ': boolean'
748+
} else {
749+
varDecl += ': any'
750+
}
751+
} else {
752+
varDecl += ': any'
753+
}
754+
755+
varDecl += ';'
756+
members.push(varDecl)
757+
}
758+
}
759+
} else if (ts.isInterfaceDeclaration(element)) {
760+
// Interface declaration (no declare keyword in ambient context)
761+
const isExported = hasExportModifier(element)
762+
const name = element.name.getText()
763+
764+
let interfaceDecl = ' '
765+
if (isExported) interfaceDecl += 'export '
766+
interfaceDecl += 'interface '
767+
interfaceDecl += name
768+
769+
// Add generics
770+
if (element.typeParameters) {
771+
const generics = element.typeParameters.map(tp => tp.getText()).join(', ')
772+
interfaceDecl += `<${generics}>`
773+
}
774+
775+
// Add extends
776+
if (element.heritageClauses) {
777+
const extendsClause = element.heritageClauses.find(clause =>
778+
clause.token === ts.SyntaxKind.ExtendsKeyword
779+
)
780+
if (extendsClause) {
781+
const types = extendsClause.types.map(type => type.getText()).join(', ')
782+
interfaceDecl += ` extends ${types}`
783+
}
784+
}
785+
786+
// Add body
787+
const body = getInterfaceBody(element)
788+
interfaceDecl += ' ' + body
789+
790+
members.push(interfaceDecl)
791+
} else if (ts.isTypeAliasDeclaration(element)) {
792+
// Type alias declaration (no declare keyword in ambient context)
793+
const isExported = hasExportModifier(element)
794+
const name = element.name.getText()
795+
796+
let typeDecl = ' '
797+
if (isExported) typeDecl += 'export '
798+
typeDecl += 'type '
799+
typeDecl += name
800+
801+
// Add generics
802+
if (element.typeParameters) {
803+
const generics = element.typeParameters.map(tp => tp.getText()).join(', ')
804+
typeDecl += `<${generics}>`
805+
}
806+
807+
typeDecl += ' = '
808+
typeDecl += element.type.getText()
809+
810+
members.push(typeDecl)
811+
} else if (ts.isEnumDeclaration(element)) {
812+
// Enum declaration
813+
const isExported = hasExportModifier(element)
814+
const name = element.name.getText()
815+
const isConst = element.modifiers?.some(mod => mod.kind === ts.SyntaxKind.ConstKeyword)
816+
817+
let enumDecl = ' '
818+
if (isExported) enumDecl += 'export '
819+
if (isConst) enumDecl += 'const '
820+
enumDecl += 'enum '
821+
enumDecl += name
822+
823+
// Build enum body
824+
const enumMembers: string[] = []
825+
for (const member of element.members) {
826+
if (ts.isEnumMember(member)) {
827+
const memberName = member.name.getText()
828+
if (member.initializer) {
829+
const value = member.initializer.getText()
830+
enumMembers.push(` ${memberName} = ${value}`)
831+
} else {
832+
enumMembers.push(` ${memberName}`)
833+
}
834+
}
835+
}
836+
837+
enumDecl += ` {\n${enumMembers.join(',\n')}\n }`
838+
members.push(enumDecl)
839+
} else if (ts.isModuleDeclaration(element)) {
840+
// Nested namespace/module (no declare keyword in ambient context)
841+
const isExported = hasExportModifier(element)
842+
const name = element.name.getText()
843+
844+
let nestedDecl = ' '
845+
if (isExported) nestedDecl += 'export '
846+
847+
// Check if this is a namespace or module
848+
const isNamespace = element.flags & ts.NodeFlags.Namespace
849+
if (isNamespace) {
850+
nestedDecl += 'namespace '
851+
} else {
852+
nestedDecl += 'module '
853+
}
854+
855+
nestedDecl += name
856+
nestedDecl += ' ' + buildModuleBody(element)
857+
858+
members.push(nestedDecl)
859+
} else if (ts.isExportAssignment(element)) {
860+
// Export default statement
861+
let exportDecl = ' export default '
862+
if (element.expression) {
863+
exportDecl += element.expression.getText()
864+
}
865+
exportDecl += ';'
866+
members.push(exportDecl)
867+
}
868+
}
869+
870+
if (ts.isModuleBlock(node.body)) {
871+
// Module block with statements
872+
for (const statement of node.body.statements) {
873+
processModuleElement(statement)
874+
}
875+
} else if (ts.isModuleDeclaration(node.body)) {
876+
// Nested module
877+
processModuleElement(node.body)
878+
}
879+
880+
return `{\n${members.join('\n')}\n}`
881+
}
882+
662883
/**
663884
* Get the text of a node from source code
664885
*/

test/fixtures/output/namespace.d.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
export declare namespace Utils {
2+
export function formatDate(date: Date): string;
3+
export interface Options {
4+
locale: string
5+
timezone: string
6+
}
7+
export const VERSION: string;
8+
export namespace Validators {
9+
export function isEmail(value: string): boolean;
10+
export function isURL(value: string): boolean;
11+
}
12+
}
13+
declare module 'custom-module' {
14+
export interface CustomType {
15+
id: string
16+
data: any
17+
}
18+
export function process(input: CustomType): Promise<CustomType>;
19+
}
20+
declare module 'existing-module' {
21+
interface ExistingInterface {
22+
newProperty: string
23+
newMethod(): void
24+
}
25+
export function newFunction(): void;
26+
}
27+
declare namespace global {
28+
interface Window {
29+
customProperty: string
30+
customMethod(): void
31+
}
32+
namespace NodeJS {
33+
interface ProcessEnv {
34+
CUSTOM_ENV_VAR: string
35+
}
36+
}
37+
}
38+
declare module '*.css' {
39+
const content: { [className: string]: string };
40+
export default content;
41+
}
42+
declare module '*.svg' {
43+
const content: string;
44+
export default content;
45+
}
46+
export declare namespace Types {
47+
export type ID = string | number
48+
export type Nullable<T> = T | null
49+
export type Optional<T> = T | undefined
50+
export interface User {
51+
id: ID
52+
name: string
53+
email: string
54+
}
55+
export enum Status {
56+
Active = 'ACTIVE',
57+
Inactive = 'INACTIVE',
58+
Pending = 'PENDING'
59+
}
60+
}

0 commit comments

Comments
 (0)