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

Commit

Permalink
feat: mocha to jest transform
Browse files Browse the repository at this point in the history
add mocha to jest transform (by example of jest-codemods). Also add get
and forEach methods to collection
  • Loading branch information
KnisterPeter committed May 31, 2017
1 parent 561e638 commit acf44d6
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 57 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"fs-extra": "^3.0.1",
"globby": "^6.1.0",
"meow": "^3.7.0",
"ts-emitter": "^0.2.1"
"ts-emitter": "^0.2.2"
},
"jest": {
"transform": {
Expand Down
16 changes: 16 additions & 0 deletions src/collection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,20 @@ describe('Collection', () => {
expect(actual).toBe(1);
});
});
describe('#get', () => {
it('should return a subtree of found nodes', () => {
const source = `
function fn1(a: string): void {}
function fn2(a: string): void {}
`;
const collection = Collection.fromSource(source);

const actual = collection
.find(ts.SyntaxKind.FunctionDeclaration)
.get(node => node.name)
.size();

expect(actual).toBe(2);
});
});
});
84 changes: 58 additions & 26 deletions src/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,54 @@ import * as emitter from 'ts-emitter';
import * as ts from 'typescript';
import { isPatternMatching } from './pattern-matcher';

export class Collection<T extends ts.Node> {
export class Collection<T extends ts.Node, R extends ts.Node> {

private root: T;
private _root: Collection<R, R>|undefined;

private collected: T[];
protected collected: T[];

public static fromSource(source: string): Collection<ts.SourceFile> {
public static fromSource(source: string): Collection<ts.SourceFile, ts.SourceFile> {
const file = emitter.fromSource(source);
return new Collection([file], file);
return new Collection<ts.SourceFile, ts.SourceFile>([file]);
}

public static fromNode<T extends ts.Node>(node: T): Collection<T> {
return new Collection([node], node);
public static fromNode<T extends ts.Node>(node: T): Collection<T, T> {
return new Collection<T, T>([node]);
}

private constructor(collected: T[], root: T) {
this.root = root;
private constructor(collected: T[], root?: Collection<R, R>) {
this._root = root;
this.collected = collected;
}

public find(kind: ts.SyntaxKind.Identifier, pattern?: IdentifierPattern): Collection<ts.Identifier>;
public find(kind: ts.SyntaxKind.FunctionDeclaration): Collection<ts.FunctionDeclaration>;
public find(kind: ts.SyntaxKind.FunctionExpression): Collection<ts.FunctionExpression>;
public find(kind: ts.SyntaxKind.CallExpression, pattern?: CallExpressionPattern): Collection<ts.CallExpression>;
public find(kind: ts.SyntaxKind.VariableDeclarationList): Collection<ts.VariableDeclarationList>;
public find(kind: ts.SyntaxKind.VariableDeclaration): Collection<ts.VariableDeclaration>;
public find(kind: ts.SyntaxKind, pattern?: any): Collection<ts.Node> {
private get root(): Collection<R, R> {
return this._root || (this as any);
}

protected get rootNode(): R {
return this.root.collected[0];
}

protected set rootNode(node: R) {
this.root.collected[0] = node;
}

public find(kind: ts.SyntaxKind.Identifier, pattern?: IdentifierPattern): Collection<ts.Identifier, R>;
public find(kind: ts.SyntaxKind.FunctionDeclaration): Collection<ts.FunctionDeclaration, R>;
public find(kind: ts.SyntaxKind.FunctionExpression): Collection<ts.FunctionExpression, R>;
public find(kind: ts.SyntaxKind.CallExpression, pattern?: CallExpressionPattern): Collection<ts.CallExpression, R>;
public find(kind: ts.SyntaxKind.VariableDeclarationList): Collection<ts.VariableDeclarationList, R>;
public find(kind: ts.SyntaxKind.VariableDeclaration): Collection<ts.VariableDeclaration, R>;
public find(kind: ts.SyntaxKind, pattern?: any): Collection<ts.Node, R> {
const marked: ts.Node[] = [];
const visitor = (node: ts.Node) => {
if (node.kind === kind) {
if (pattern && isPatternMatching(node, pattern)) {
marked.push(node);
} else if (!pattern) {
marked.push(node);
} else {
ts.forEachChild(node, visitor);
}
} else {
ts.forEachChild(node, visitor);
Expand All @@ -47,7 +61,18 @@ export class Collection<T extends ts.Node> {
return new Collection(marked, this.root);
}

public filter(fn: (node: T) => boolean): Collection<T> {
public get<U extends ts.Node>(fn: (node: T) => U|undefined): Collection<U, R> {
const marked: U[] = [];
this.collected.forEach(node => {
const result = fn(node);
if (result) {
marked.push(result);
}
});
return new Collection<U, R>(marked, this.root);
}

public filter(fn: (node: T) => boolean): Collection<T, R> {
const marked: T[] = [];
this.collected.forEach(node => {
if (fn(node)) {
Expand All @@ -57,23 +82,30 @@ export class Collection<T extends ts.Node> {
return new Collection(marked, this.root);
}

public replaceWith(fn: (node: T) => ts.Node): Collection<T> {
const replacer = (context: ts.TransformationContext) => (rootNode: T) => {
public replaceWith(fn: (node: T) => ts.Node): this {
const replacer = (context: ts.TransformationContext) => (rootNode: R) => {
const visitor = (node: ts.Node): ts.Node => {
const markedNode = this.collected.find(item => item === node);
if (markedNode) {
const replaced = fn(markedNode);
(replaced as any).original = markedNode;
if ((replaced as any).text && (replaced as any).text !== (markedNode as any).text) {
(replaced as any).newText = (replaced as any).text;
if (replaced !== markedNode) {
(replaced as any).original = markedNode;
if ((replaced as any).text && (replaced as any).text !== (markedNode as any).text) {
(replaced as any).newText = (replaced as any).text;
}
}
return replaced;
}
return ts.visitEachChild(node, visitor, context);
};
return ts.visitNode(rootNode, visitor);
};
this.root = ts.transform(this.root, [replacer]).transformed[0];
this.rootNode = ts.transform(this.rootNode, [replacer]).transformed[0];
return this;
}

public forEach(fn: (node: T) => void): this {
this.collected.forEach(node => fn(node));
return this;
}

Expand All @@ -82,11 +114,11 @@ export class Collection<T extends ts.Node> {
}

public toSource(): string {
if (this.root.kind !== ts.SyntaxKind.SourceFile) {
if (this.rootNode.kind !== ts.SyntaxKind.SourceFile) {
throw new Error(`toSource() could only be called on collections of type `
+ `'ts.SourceFile' but this is of type '${ts.SyntaxKind[this.root.kind]}'`);
+ `'SourceFile' but this is of type '${ts.SyntaxKind[this.rootNode.kind]}'`);
}
return emitter.toSource(this.root as any);
return emitter.toSource(this.rootNode as any);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { stripIndent } from 'common-tags';
import * as ts from 'typescript';
import { applyTransforms } from './index';
import { applyTransforms } from './transform';
import * as types from './types';

test('run identifiers transform', () => {
Expand Down
29 changes: 1 addition & 28 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,9 @@
import * as fs from 'fs-extra';
import globby = require('globby');
import meow = require('meow');
import * as ts from 'typescript';
import { Collection } from './collection';
import { applyTransforms } from './transform';
import * as types from './types';

const api = {
tscodeshift: (source: string|ts.Node) => {
if (typeof source === 'string') {
return Collection.fromSource(source);
} else {
return Collection.fromNode(source);
}
}
};

async function main(cli: meow.Result): Promise<void> {
const transformPaths = [].concat(
cli.flags['t'] || [],
Expand All @@ -32,22 +21,6 @@ async function main(cli: meow.Result): Promise<void> {
});
}

/* @internal */
export function applyTransforms(path: string, source: string, transforms: types.Transform[]): string {
const file: types.File = {
path,
source
};
return transforms
.reduce((file, transform) => {
return {
path: file.path,
source: transform(file, api)
};
}, file)
.source;
}

const cli = meow(`
Usage: tscodeshift <path>... [options]
Expand Down
29 changes: 29 additions & 0 deletions src/transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as ts from 'typescript';
import { Collection } from './collection';
import * as types from './types';

const api = {
tscodeshift: (source: string|ts.Node) => {
if (typeof source === 'string') {
return Collection.fromSource(source);
} else {
return Collection.fromNode(source);
}
}
};

/* @internal */
export function applyTransforms(path: string, source: string, transforms: types.Transform[]): string {
const file: types.File = {
path,
source
};
return transforms
.reduce((file, transform) => {
return {
path: file.path,
source: transform(file, api)
};
}, file)
.source;
}
37 changes: 37 additions & 0 deletions src/transforms/jest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { stripIndent } from 'common-tags';
import { applyTransforms } from '../transform';

import transform from './jest';

test('Convert mocha test to jest test', () => {
const source = stripIndent`
before(() => {
// something
});
suite('Array', function() {
setup(() => {
});
specify.skip('Array', function(done) {
done();
});
});
`;
const expected = stripIndent`
beforeAll(()=> {
// something
});
describe('Array', function() {
beforeEach(()=> {
});
it.skip('Array', function(done) {
done();
});
});
`;
const actual = applyTransforms('path.ts', source, [transform]);
expect(actual).toBe(expected);
});
97 changes: 97 additions & 0 deletions src/transforms/jest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import * as ts from 'typescript';
import { File, API } from '../types';

const methodMap: { [key: string]: string } = {
suite: 'describe',
context: 'describe',
specify: 'it',
test: 'it',
before: 'beforeAll',
beforeEach: 'beforeEach',
setup: 'beforeEach',
after: 'afterAll',
afterEach: 'afterEach',
teardown: 'afterEach',
suiteSetup: 'beforeAll',
suiteTeardown: 'afterAll'
};

// const jestMethodsWithDescriptionsAllowed = new Set(['it', 'describe']);

const methodModifiers = ['only', 'skip'];

// function hasBinding(name, scope) {
// if (!scope) {
// return false;
// }

// const bindings = Object.keys(scope.getBindings()) || [];
// if (bindings.indexOf(name) >= 0) {
// return true;
// }

// return scope.isGlobal ? false : hasBinding(name, scope.parent);
// }

export default function mochaToJest(file: File, api: API): string {
const t = api.tscodeshift;
const ast = t(file.source);

Object.keys(methodMap).forEach(mochaMethod => {
const jestMethod = methodMap[mochaMethod];

ast
.find(ts.SyntaxKind.CallExpression, {
expression: {
kind: ts.SyntaxKind.Identifier,
text: mochaMethod
}
})
// .filter(({ scope }) => !hasBinding(mochaMethod, scope))
.get(node => node.expression)
.replaceWith(() => {
// let args = node.arguments;
// if (!jestMethodsWithDescriptionsAllowed.has(jestMethod)) {
// args = args.filter(a => a.kind !== ts.SyntaxKind.LiteralType);
// }
return ts.createIdentifier(jestMethod);
});

methodModifiers.forEach(modifier => {
ast
.find(ts.SyntaxKind.CallExpression, {
expression: {
kind: ts.SyntaxKind.PropertyAccessExpression,
expression: {
kind: ts.SyntaxKind.Identifier,
text: mochaMethod
},
name: {
kind: ts.SyntaxKind.Identifier,
text: modifier
}
}
})
.get(node => (node.expression as ts.PropertyAccessExpression).expression)
.replaceWith(() => ts.createIdentifier(jestMethod));
ast
.find(ts.SyntaxKind.CallExpression, {
expression: {
kind: ts.SyntaxKind.PropertyAccessExpression,
expression: {
kind: ts.SyntaxKind.Identifier,
text: mochaMethod
},
name: {
kind: ts.SyntaxKind.Identifier,
text: modifier
}
}
})
.get(node => (node.expression as ts.PropertyAccessExpression).name)
.replaceWith(() => ts.createIdentifier(modifier));
});
});

return ast.toSource();
}
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ export interface API {
}

export interface TSCodeShift {
<T extends ts.Node>(source: string|T): Collection<T>;
<T extends ts.Node>(source: string|T): Collection<T, T>;
}

0 comments on commit acf44d6

Please sign in to comment.