diff --git a/lib/Mapper.ts b/lib/Mapper.ts index cc5b264..0e145a2 100644 --- a/lib/Mapper.ts +++ b/lib/Mapper.ts @@ -1,6 +1,9 @@ import EsExport from '@es/Export'; import EsFile from '@es/File'; import EsImport from '@es/Import'; +import TsExport from '@ts/Export'; +import TsFile from '@ts/File'; +import TsImport from '@ts/Import'; import { existsSync as fileExistsSync, readFileSync as fileReadSync } from 'fs'; import { dirname, resolve } from 'path'; import * as ts from 'typescript'; @@ -48,12 +51,15 @@ export default class Mapper { this.parsed = parsed; } - async *files(): AsyncIterableIterator { + async *files(): AsyncIterableIterator { const { options } = this.parsed; yield* this.parsed.fileNames.map(path => new EsFile({ path, options, config: this.parsed })); + if (options.declaration) { + yield* this.parsed.fileNames.map(path => new TsFile({ path, options, config: this.parsed })); + } } - async *map(): AsyncIterableIterator { + async *map(): AsyncIterableIterator { for await (const file of this.files()) { yield* file.map(this.parsed.options); } diff --git a/lib/ts/Declaration.ts b/lib/ts/Declaration.ts new file mode 100644 index 0000000..cdb21bb --- /dev/null +++ b/lib/ts/Declaration.ts @@ -0,0 +1,32 @@ +import { ExportDeclaration, ImportDeclaration, StringLiteral, SyntaxKind } from 'typescript'; + +import Base, { IDerivedOptions as IBaseOptions } from '@lib/Declaration'; + +export type Interface = ExportDeclaration | ImportDeclaration; + +export type IOptions = IBaseOptions; + +export default class Declaration extends Base { + constructor({ declaration, ...rest}: IOptions) { + + // RAII checks + const { kind, text } = (declaration.moduleSpecifier as StringLiteral); + if (kind !== SyntaxKind.StringLiteral) { + throw new TypeError(`Invalid TS declaration source type: ${kind}`); + } + + super({declaration, ...rest, path: text}); + } + + private get literal(): StringLiteral { + return (this.declaration.moduleSpecifier as StringLiteral); + } + + get path(): string { + return this.literal.text; + } + + protected update(value: string): void { + this.literal.text = value; + } +} diff --git a/lib/ts/Export.ts b/lib/ts/Export.ts new file mode 100644 index 0000000..4ed6685 --- /dev/null +++ b/lib/ts/Export.ts @@ -0,0 +1,9 @@ +import { ExportDeclaration } from 'typescript'; + +import Declaration, { IOptions as IDeclarationOptions } from '@ts/Declaration'; + +export type Interface = ExportDeclaration; + +export type IOptions = IDeclarationOptions; + +export default class Export extends Declaration {} diff --git a/lib/ts/File.ts b/lib/ts/File.ts new file mode 100644 index 0000000..dd641c8 --- /dev/null +++ b/lib/ts/File.ts @@ -0,0 +1,76 @@ +import { PathLike, readFile as readFileSync, writeFile as writeFileSync } from 'fs'; +import { + createPrinter, + createSourceFile, + EmitHint, + ExportDeclaration, + ImportDeclaration, + ScriptTarget, + SourceFile, + SyntaxKind, +} from 'typescript'; +import { promisify } from 'util'; + +import ParseError from '@error/Parse'; +import Base, { IDerivedOptions as IBaseOptions } from '@lib/File'; +import Export from '@ts/Export'; +import Import from '@ts/Import'; + +const readFile = promisify(readFileSync); +const writeFile = promisify(writeFileSync); + +export type IOptions = IBaseOptions; + +export default class File extends Base { + private sourceFile: SourceFile | undefined; + + constructor({ ...options }: IOptions) { + super({...options, extension: '.d.ts'}); + this.sourceFile = undefined; + } + + private get ast(): Promise { + if (this.sourceFile) { + return Promise.resolve(this.sourceFile); + } else { + return (async () => { + const data = await readFile(this.destination.toString(), 'utf-8'); + try { + return this.sourceFile = createSourceFile(this.destination.toString(), data, ScriptTarget.Latest); + } catch (error) { + if (error instanceof SyntaxError) { + throw new ParseError({file: this, error, data}); + } else { + throw error; + } + } + })(); + } + } + + async *imports(): AsyncIterableIterator { + const { statements } = await this.ast; + yield* statements + .filter(({kind}) => kind === SyntaxKind.ImportDeclaration) + .map(n => new Import({file: this, declaration: n as ImportDeclaration})); + } + + async *exports(): AsyncIterableIterator { + const { statements } = await this.ast; + yield* statements + .filter(({kind}) => kind === SyntaxKind.ExportDeclaration) + .map(n => new Export({file: this, declaration: n as ExportDeclaration})); + } + + async write(path?: PathLike | number, options?: { + encoding?: string | null; + mode?: number | string; + flag?: string; + } | string | null): Promise { + const sourceFile = await this.ast; + const { newLine } = this.options; + const printer = createPrinter({ newLine }); + const data = printer.printNode(EmitHint.SourceFile, sourceFile, sourceFile); + return writeFile((path === undefined) ? this.destination.toString() : path, data, options); + } +} diff --git a/lib/ts/Import.ts b/lib/ts/Import.ts new file mode 100644 index 0000000..f78e3a0 --- /dev/null +++ b/lib/ts/Import.ts @@ -0,0 +1,9 @@ +import { ImportDeclaration } from 'typescript'; + +import Declaration, { IOptions as IDeclarationOptions } from '@ts/Declaration'; + +export type Interface = ImportDeclaration; + +export type IOptions = IDeclarationOptions; + +export default class Import extends Declaration {} diff --git a/package.json b/package.json index 15ac939..8f708db 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,8 @@ "^@test(.*)$": "/test$1", "^@lib(.*)$": "/lib$1", "^@error(.*)$": "/lib/error$1", - "^@es(.*)$": "/lib/es$1" + "^@es(.*)$": "/lib/es$1", + "^@ts(.*)$": "/lib/es$1" } } } diff --git a/tsconfig.json b/tsconfig.json index 79e06ec..e28b89c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,7 @@ "paths": { "@lib/*": ["lib/*"], "@es/*": ["lib/es/*"], + "@ts/*": ["lib/ts/*"], "@error/*": ["lib/error/*"] }, "newLine": "LF",