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

Commit

Permalink
feat(*): add support for rewriting require calls
Browse files Browse the repository at this point in the history
Only works for **top level** calls. Need to implement walking the whole ES AST tree for that
  • Loading branch information
mattyclarkson committed Mar 18, 2018
1 parent 7f37921 commit d1ea076
Show file tree
Hide file tree
Showing 15 changed files with 140 additions and 58 deletions.
11 changes: 2 additions & 9 deletions lib/Declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,15 @@ import ResolutionError from '@error/Resolution';
import { DeclarationInterface as Interface, File } from '@lib/convert';
import Path from '@lib/Path';

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

export interface IOptionsFile {
file: File;
}

export interface IOptionsPath {
export interface IOptions<T extends Interface> extends IDerivedOptions<T> {
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
14 changes: 2 additions & 12 deletions lib/File.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,16 @@ import FileNotFoundError from '@error/FileNotFound';
import { Declaration } from '@lib/convert';
import Path from '@lib/Path';

export interface IOptionsPath {
export interface IDerivedOptions {
path: string | Path;
}

export interface IOptionsOptions {
options: ICompilerOptions;
}

export interface IOptionsConfig {
config: ICompilerConfig;
}

export interface IOptionsExtension {
export interface IOptions extends IDerivedOptions {
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 Down
14 changes: 4 additions & 10 deletions lib/Mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ import { existsSync as fileExistsSync, readFileSync as fileReadSync } from 'fs';
import { dirname, resolve } from 'path';
import * as ts from 'typescript';

import UnsupportedError from '@error/Unsupported';
import EsExport from '@es/Export';
import { Declaration, File } from '@lib/convert';

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';

export interface IOptions {
tsconfig: string;
Expand Down Expand Up @@ -49,22 +46,19 @@ export default class Mapper {
}));
}

if (parsed.options.module !== ts.ModuleKind.ES2015) {
throw new UnsupportedError('Only ECMA2015 module outputs are supported at the moment');
}
parsed.options.rootDir = parsed.options.rootDir || projectRoot;
this.parsed = parsed;
}

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

export type Declaration =
EsImport |
EsExport |
TsImport |
TsExport |
EsRequire;

export type File =
EsFile |
TsFile;

export type IOptions = IMapperOptions;

Expand Down
37 changes: 28 additions & 9 deletions lib/es/Declaration.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,46 @@
import { ExportAllDeclaration, ExportNamedDeclaration, ImportDeclaration, SimpleLiteral } from 'estree';

import isExportNamedDeclaration from '@es/isExportNamedDeclaration';
import isImportDeclaration from '@es/isImportDeclaration';
import isRequireCallExpression from '@es/isRequireCallExpression';
import isSimpleLiteral from '@es/isSimpleLiteral';
import RequireCallExpression from '@es/RequireCallExpression';
import { File } from '@lib/convert';

import Base, { IDerivedOptions as IBaseOptions } from '@lib/Declaration';

export type Interface = (ExportAllDeclaration | ExportNamedDeclaration) | ImportDeclaration;
export type Interface = (ExportAllDeclaration | ExportNamedDeclaration) | ImportDeclaration | RequireCallExpression;

export type IOptions<T extends Interface> = IBaseOptions<T>;

function getLiteral(declaration: Interface, file: File): SimpleLiteral {
const literal =
isExportNamedDeclaration(declaration) ? declaration.source :
isImportDeclaration(declaration) ? declaration.source :
isRequireCallExpression(declaration) ? declaration.arguments[0] :
undefined;
if (literal === undefined) {
throw new TypeError(`Failed to find ES declaration in '${file.source}'`);
} else if (!isSimpleLiteral(literal)) {
const type = literal ? literal.type : 'unknown';
throw new TypeError(`Invalid ES declaration source type '${type}' in '${file.source}'`);
}
return literal;
}

export default class Declaration<T extends Interface> extends Base<T> {
constructor({ declaration, file, ...rest}: IOptions<T>) {
// RAII checks
const { type, value } = (declaration.source as SimpleLiteral);
if (type !== 'Literal') {
throw new TypeError(`Invalid ES declaration source type '${type}' in '${file.source}'`);
}
if (typeof value !== 'string') {
throw new TypeError(`The type '${typeof value}' of the ES source value was not a 'string' for '${file.source}'`);
const literal = getLiteral(declaration, file);
if (typeof literal.value !== 'string') {
throw new TypeError(`The type '${typeof literal.value}' of the ES source must be 'string' for '${file.source}'`);
}

super({declaration, file, ...rest, path: value});
super({declaration, file, ...rest, path: literal.value});
}

private get literal(): SimpleLiteral {
return (this.declaration.source as SimpleLiteral);
return getLiteral(this.declaration, this.file);
}

get path(): string {
Expand Down
36 changes: 21 additions & 15 deletions lib/es/File.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { ExportNamedDeclaration, ImportDeclaration, Program } from 'estree';
import { Program } from 'estree';
import { PathLike, readFile as readFileSync, writeFile as writeFileSync } from 'fs';
import { ModuleKind } from 'typescript';
import { promisify } from 'util';

import ParseError from '@error/Parse';
import Export from '@es/Export';
import Import from '@es/Import';
import isExportNamedDeclaration from '@es/isExportNamedDeclaration';
import isImportDeclaration from '@es/isImportDeclaration';
import isRequireCallExpression from '@es/isRequireCallExpression';
import isVariableDeclaration from '@es/isVariableDeclaration';
import { attachComments, Comment, generate, parse, plugins, Token } from '@es/parser';
import Require from '@es/Require';
import Base, { IDerivedOptions as IBaseOptions } from '@lib/File';

const readFile = promisify(readFileSync);
const writeFile = promisify(writeFileSync);

export type IOptions = IBaseOptions;

export default class File extends Base<Import, Export> {
export default class File extends Base<Import | Require, Export> {
private program: Program | undefined;

constructor({ ...options }: IOptions) {
Expand All @@ -35,7 +41,7 @@ export default class File extends Base<Import, Export> {
ranges: true,
onComment: comments,
onToken: tokens,
sourceType: 'module',
sourceType: this.options.module === ModuleKind.ES2015 ? 'module' : 'script',
ecmaVersion: 8,
plugins
});
Expand All @@ -52,25 +58,25 @@ export default class File extends Base<Import, Export> {
}
}

async *imports(): AsyncIterableIterator<Import> {
async *imports(): AsyncIterableIterator<Import | Require> {
const { body } = await this.ast;
yield* body
.filter(({type}) => type === 'ImportDeclaration')
.map(n => new Import({file: this, declaration: n as ImportDeclaration}));
.filter(isImportDeclaration)
.map(declaration => new Import({file: this, declaration}));
yield* body
.filter(isVariableDeclaration)
.map(({declarations}) => declarations)
.reduce((a, n) => a.concat(n), [])
.map(({init}) => init)
.filter(isRequireCallExpression)
.map(declaration => new Require({file: this, declaration}));
}

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 ExportNamedDeclaration
}));
.filter(isExportNamedDeclaration)
.map(declaration => new Export({ file: this, declaration }));
}

async write(path?: PathLike | number, options?: {
Expand Down
9 changes: 9 additions & 0 deletions lib/es/Require.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import RequireCallExpression from '@es/RequireCallExpression';

import Declaration, { IOptions as IDeclarationOptions } from '@es/Declaration';

export type Interface = RequireCallExpression;

export type IOptions = IDeclarationOptions<Interface>;

export default class Require extends Declaration<Interface> {}
7 changes: 7 additions & 0 deletions lib/es/RequireCallExpression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Identifier, SimpleCallExpression, SimpleLiteral } from 'estree';

// tslint:disable-next-line:interface-name
export default interface RequireCallExpression extends SimpleCallExpression {
callee: Identifier;
arguments: [ SimpleLiteral ];
}
8 changes: 8 additions & 0 deletions lib/es/isExportNamedDeclaration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ExportNamedDeclaration, Node } from 'estree';

export default function isExportNamedDeclaration(data: Node | null | undefined): data is ExportNamedDeclaration {
return data !== null &&
data !== undefined &&
(data.type === 'ExportAllDeclaration'
|| (data.type === 'ExportNamedDeclaration' && data.source !== null));
}
5 changes: 5 additions & 0 deletions lib/es/isIdentifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Identifier, Node } from 'estree';

export default function isIdentifier(data: Node | null | undefined): data is Identifier {
return data !== null && data !== undefined && data.type === 'Identifier';
}
5 changes: 5 additions & 0 deletions lib/es/isImportDeclaration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ImportDeclaration, Node } from 'estree';

export default function isImportDeclaration(data: Node | null | undefined): data is ImportDeclaration {
return data !== null && data !== undefined && data.type === 'ImportDeclaration';
}
15 changes: 15 additions & 0 deletions lib/es/isRequireCallExpression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import isIdentifier from '@es/isIdentifier';
import isSimpleCallExpression from '@es/isSimpleCallExpression';
import isSimpleLiteral from '@es/isSimpleLiteral';
import RequireCallExpression from '@es/RequireCallExpression';
import { Node } from 'estree';

export default function isRequireCallExpression(data: Node | undefined | null): data is RequireCallExpression {
return data !== null &&
data !== undefined &&
isSimpleCallExpression(data) &&
isIdentifier(data.callee) &&
data.callee.name === 'require' &&
data.arguments.length === 1 &&
isSimpleLiteral(data.arguments[0]);
}
5 changes: 5 additions & 0 deletions lib/es/isSimpleCallExpression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Node, SimpleCallExpression } from 'estree';

export default function isSimpleCallExpression(data: Node | undefined | null): data is SimpleCallExpression {
return data !== null && data !== undefined && data.type === 'CallExpression';
}
5 changes: 5 additions & 0 deletions lib/es/isSimpleLiteral.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Node, SimpleLiteral } from 'estree';

export default function isIdentifier(data: Node | null | undefined): data is SimpleLiteral {
return data !== null && data !== undefined && data.type === 'Literal';
}
5 changes: 5 additions & 0 deletions lib/es/isVariableDeclaration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Node, VariableDeclaration } from 'estree';

export default function isVariableDeclaration(data: Node | null | undefined): data is VariableDeclaration {
return data !== null && data !== undefined && data.type === 'VariableDeclaration';
}

0 comments on commit d1ea076

Please sign in to comment.