Skip to content
This repository has been archived by the owner on Jun 22, 2020. It is now read-only.

Commit

Permalink
refactor(*): make mapping classes generic
Browse files Browse the repository at this point in the history
We need to be able to re-map the TypeScript declaration files as well as the generated ECMAscript
output files. This will require almost exactly the same process as the ECMAscript re-mapping. The
classes that handle the re-mapping have been made generic so that we can re-use the same framework
for the declaration re-mapping. See #2

BREAKING CHANGE: The re-mapping classes have been made generic and there are ES derived variants of
each. This is required so that we can nicely re-map TypeScript declaration files using the same
re-mapping framework. The demo library code in the `README` will still work, however, the modules
have all moved around hence the breaking change 💥 If you do not do any module loading of the
library, then you will require no code changes other than upgrading your dependency
  • Loading branch information
mattyclarkson committed Feb 7, 2018
1 parent 6f08580 commit 3e3df9d
Show file tree
Hide file tree
Showing 17 changed files with 262 additions and 170 deletions.
56 changes: 21 additions & 35 deletions lib/Declaration.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
// tslint:disable-next-line:no-implicit-dependencies
import { ExportAllDeclaration, ExportNamedDeclaration, ImportDeclaration, SimpleLiteral } from 'estree';
import * as fs from 'fs';
import { basename, dirname, join, relative } from 'path';
import * as ts from 'typescript';

import ResolutionError from '@lib/error/Resolution';
import File from '@lib/File';
import ResolutionError from '@error/Resolution';
import { DeclarationInterface as Interface, File } from '@lib/convert';
import Path from '@lib/Path';

export interface IOptions<T> {
export interface IOptionsDeclaration<T extends Interface> {
declaration: T;
}

export interface IOptionsFile {
file: File;
}

export interface IOptionsPath {
path: string;
}

export type IDerivedOptions<T extends Interface> = IOptionsDeclaration<T> & IOptionsFile;

export type IOptions<T extends Interface> = IDerivedOptions<T> & IOptionsPath;

function isBuiltinModule(module: string): boolean {
// TODO: change to use 'is-builtin-module', need to submit @types/is-builtin-module
const builtin = [
Expand Down Expand Up @@ -57,28 +66,16 @@ function isBuiltinModule(module: string): boolean {
return builtin.indexOf(module) !== -1;
}

export type Base = (ExportAllDeclaration | ExportNamedDeclaration) | ImportDeclaration;

export default abstract class Declaration<T extends Base> {
export default abstract class Declaration<T extends Interface> {
protected readonly declaration: T;
readonly file: File;
private processed: boolean = false;
readonly original: string;

constructor(options: IOptions<T>) {
this.declaration = options.declaration;
this.file = options.file;

// RAII checks
const { type, value } = this.literal;
if (type !== 'Literal') {
throw new TypeError(`Invalid export declaration source type: ${type}`);
}
if (typeof value !== 'string') {
throw new TypeError(`The type of the export source value was not a 'string': ${typeof value}`);
}

this.original = this.path;
constructor({ file, declaration, path }: IOptions<T>) {
this.declaration = declaration;
this.file = file;
this.original = path;
}

get isMapped(): Promise<boolean> {
Expand All @@ -89,20 +86,9 @@ export default abstract class Declaration<T extends Base> {
return this.file.destination;
}

get literal(): SimpleLiteral {
return (this.declaration.source as SimpleLiteral);
}

get path(): string {
return this.literal.value as string;
}
abstract get path(): string;

private update(value: string): void {
if (this.literal.raw) {
this.literal.raw = this.literal.raw.replace(this.path, value);
}
this.literal.value = value;
}
protected abstract update(value: string): void;

toString(): string {
return `${this.module}: ${this.path}`;
Expand Down
19 changes: 0 additions & 19 deletions lib/Export.ts

This file was deleted.

116 changes: 37 additions & 79 deletions lib/File.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
import * as acorn from 'acorn';
import * as escodegen from 'escodegen';
// tslint:disable-next-line:no-implicit-dependencies
import * as estree from 'estree';
import * as fs from 'fs';
import * as ts from 'typescript';
import { promisify } from 'util';

import ParseError from '@lib/error/Parse';
import Export from '@lib/Export';
import Import from '@lib/Import';
import Path from '@lib/Path';
import { existsSync as fileExistsSync, PathLike } from 'fs';
import { CompilerOptions as ICompilerOptions, ParsedCommandLine as ICompilerConfig } from 'typescript';

const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
import FileNotFoundError from '@error/FileNotFound';
import { Declaration } from '@lib/convert';
import Path from '@lib/Path';

export interface IOptions {
export interface IOptionsPath {
path: string | Path;
options: ts.CompilerOptions;
config: ts.ParsedCommandLine;
}

export interface IOptionsOptions {
options: ICompilerOptions;
}

export interface IOptionsConfig {
config: ICompilerConfig;
}

export interface IOptionsExtension {
extension: string;
}

export type IDerivedOptions = IOptionsPath & IOptionsOptions & IOptionsConfig;

export type IOptions = IDerivedOptions & IOptionsExtension;

function commonPathPrefix(paths: IterableIterator<string>): string {
const { done, value: left } = paths.next();
if (done) {
Expand All @@ -37,16 +42,20 @@ function commonPathPrefix(paths: IterableIterator<string>): string {
return left.substring(0, index);
}

export default class File {
export default abstract class File<I extends Declaration, E extends Declaration> {
readonly source: Path;
readonly root: Path;
readonly options: ts.CompilerOptions;
private program: estree.Program | undefined;
protected readonly root: Path;
protected readonly options: ICompilerOptions;
protected readonly extension: string;

constructor({ path, options, config: { fileNames } }: IOptions) {
constructor({ path, options, config: { fileNames }, extension }: IOptions) {
this.source = new Path(path.toString());
this.root = new Path(commonPathPrefix(fileNames[Symbol.iterator]()));
this.options = options;
this.extension = extension;
if (!fileExistsSync(this.destination.toString())) {
throw new FileNotFoundError({path: this.destination});
}
}

get isMapped(): Promise<boolean> {
Expand All @@ -62,56 +71,9 @@ export default class File {
})();
}

get ast(): Promise<estree.Program> {
if (this.program) {
return Promise.resolve(this.program);
} else {
return (async () => {
const data = await readFile(this.destination.toString(), 'utf-8');
try {
const comments: Array<acorn.Comment> = [];
const tokens: Array<acorn.Token> = [];
const program = acorn.parse(data, {
allowHashBang: true,
ranges: true,
onComment: comments,
onToken: tokens,
sourceType: 'module',
ecmaVersion: 8,
});
escodegen.attachComments(program, comments, tokens);
return this.program = program;
} catch (error) {
if (error instanceof SyntaxError) {
throw new ParseError({file: this, error, data});
} else {
throw error;
}
}
})();
}
}

async *imports(): AsyncIterableIterator<Import> {
const { body } = await this.ast;
yield* body
.filter(({type}) => type === 'ImportDeclaration')
.map(n => new Import({file: this, declaration: n as estree.ImportDeclaration}));
}
abstract imports(): AsyncIterableIterator<I>;

async *exports(): AsyncIterableIterator<Export> {
const { body } = await this.ast;
yield* body
.filter(n =>
n.type === 'ExportAllDeclaration' ||
(n.type === 'ExportNamedDeclaration' && (n).source !== null))
.map(n => new Export({
file: this,
declaration: n.type === 'ExportAllDeclaration' ?
n :
n as estree.ExportNamedDeclaration,
}));
}
abstract exports(): AsyncIterableIterator<E>;

get destination(): Path {
const { outDir } = this.options;
Expand All @@ -122,21 +84,17 @@ export default class File {

const out = new Path(outDir);
const destination = out.join(this.source.relative(this.root));
destination.extension = '.js';
destination.extension = this.extension;
return destination;
}

async write(path?: fs.PathLike | number, options?: {
abstract write(path?: PathLike | number, options?: {
encoding?: string | null;
mode?: number | string;
flag?: string;
} | string | null): Promise<void> {
const ast = await this.ast;
const data = escodegen.generate(ast, {comment: true});
return writeFile((path === undefined) ? this.destination.toString() : path, data, options);
}
} | string | null): Promise<void>;

async *map(options: ts.CompilerOptions): AsyncIterableIterator<Import | Export> {
async *map(options: ICompilerOptions): AsyncIterableIterator<I | E> {
for await (const imprt of this.imports()) {
const mapped = await imprt.map(options);
if (mapped) {
Expand Down
8 changes: 0 additions & 8 deletions lib/Import.ts

This file was deleted.

20 changes: 10 additions & 10 deletions lib/Mapper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Export from '@lib/Export';
import File from '@lib/File';
import Import from '@lib/Import';
import * as fs from 'fs';
import EsExport from '@es/Export';
import EsFile from '@es/File';
import EsImport from '@es/Import';
import { existsSync as fileExistsSync, readFileSync as fileReadSync } from 'fs';
import { dirname, resolve } from 'path';
import * as ts from 'typescript';

Expand All @@ -14,7 +14,7 @@ export default class Mapper {
private readonly parsed: ts.ParsedCommandLine;

constructor({ tsconfig, projectRoot }: IOptions) {
const config = ts.readConfigFile(tsconfig, path => fs.readFileSync(path, 'utf-8'));
const config = ts.readConfigFile(tsconfig, path => fileReadSync(path, 'utf-8'));
if (config.error) {
throw new TypeError(ts.formatDiagnostics([config.error], {
getCanonicalFileName: f => f,
Expand All @@ -24,9 +24,9 @@ export default class Mapper {
}

const parseConfig: ts.ParseConfigHost = {
fileExists: fs.existsSync,
fileExists: fileExistsSync,
readDirectory: ts.sys.readDirectory,
readFile: file => fs.readFileSync(file, 'utf8'),
readFile: f => fileReadSync(f, 'utf8'),
useCaseSensitiveFileNames: true,
};

Expand All @@ -48,12 +48,12 @@ export default class Mapper {
this.parsed = parsed;
}

async *files(): AsyncIterableIterator<File> {
async *files(): AsyncIterableIterator<EsFile> {
const { options } = this.parsed;
yield* this.parsed.fileNames.map(path => new File({ path, options, config: this.parsed }));
yield* this.parsed.fileNames.map(path => new EsFile({ path, options, config: this.parsed }));
}

async *map(): AsyncIterableIterator<Import | Export> {
async *map(): AsyncIterableIterator<EsImport | EsExport> {
for await (const file of this.files()) {
yield* file.map(this.parsed.options);
}
Expand Down
13 changes: 10 additions & 3 deletions lib/convert.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import Export from '@lib/Export';
import Import from '@lib/Import';
import EsImport, { Interface as EsImportInterface } from '@es/Export';
import EsFile from '@es/File';
import EsExport, { Interface as EsExportInterface } from '@es/Import';
import Mapper, { IOptions as IMapperOptions } from '@lib/Mapper';
import TsImport, { Interface as TsImportInterface } from '@ts/Export';
import TsFile from '@ts/File';
import TsExport, { Interface as TsExportInterface } from '@ts/Import';
export type DeclarationInterface = EsImportInterface | EsExportInterface | TsImportInterface | TsExportInterface;
export type Declaration = EsImport | EsExport | TsImport | TsExport;
export type File = EsFile | TsFile;

export type IOptions = IMapperOptions;

export default async function *convert({ ...other}: IOptions): AsyncIterableIterator<Import | Export> {
export default async function *convert({ ...other}: IOptions): AsyncIterableIterator<Declaration> {
const mapper = new Mapper({ ...other });
yield* mapper.map();
}
16 changes: 16 additions & 0 deletions lib/error/FileNotFound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

import TspmError from '@lib/Error';
import Path from '@lib/Path';

export interface IOptions {
path: Path;
}

export default class ResolutionError extends TspmError {
readonly path: Path;

constructor({ path }: IOptions) {
super(`File not found: '${path}'`);
this.path = path;
}
}
Loading

0 comments on commit 3e3df9d

Please sign in to comment.