Skip to content

Commit

Permalink
feat(transformer): add remove-vue-import transformer
Browse files Browse the repository at this point in the history
Closes #4
  • Loading branch information
ahnpnl committed Sep 29, 2020
1 parent afdc8d1 commit b0834da
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 0 deletions.
123 changes: 123 additions & 0 deletions src/transformers/remove-vue-import.ts
@@ -0,0 +1,123 @@
import { LogContexts, LogLevels } from 'bs-logger'
import type { ConfigSet } from 'ts-jest/dist/config/config-set';
import type * as _ts from 'typescript'

// this is a unique identifier for your transformer. `ts-jest` uses this for jest cache key
export const name = 'remove-vue-import'
// increment this each time you change the behavior of your transformer. `ts-jest` uses this for jest cache key
export const version = 1
const VUE_GLOBAL_NAME = 'vue'
const ROOT_LEVEL_AST = 1

/**
* @usage In jest config, define as
* // jest.config.js
* module.exports = {
* // other configs
* globals: {
* 'ts-jest': {
* astTransformers: {
* after: ['vue-ts-jest/dist/transformers/remove-vue-import']
* }
* }
* }
* }
*/
export function factory(cs: ConfigSet): (ctx: _ts.TransformationContext) => _ts.Transformer<_ts.SourceFile> {
const logger = cs.logger.child({ namespace: 'remove-vue-import' })
const ts = cs.compilerModule
const importNames: Set<string> = new Set<string>();
const isRequire = (node: _ts.Node): node is _ts.CallExpression =>
ts.isCallExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === 'require' &&
ts.isStringLiteral(node.arguments[0]) &&
node.arguments.length === 1
const isVueRequireStmt = (node: _ts.Node) => isRequire(node)
&& (node.arguments[0] as _ts.StringLiteral).text === VUE_GLOBAL_NAME
const isVueNamespaceImport = (node: _ts.Node): node is _ts.ImportDeclaration => ts.isImportDeclaration(node)
&& ts.isStringLiteral(node.moduleSpecifier)
&& node.moduleSpecifier.text === VUE_GLOBAL_NAME

const createVisitor = (ctx: _ts.TransformationContext, _sf: _ts.SourceFile) => {
/**
* Current block level
*/
let level = 0
/**
* List of nodes which needs to be deleted, indexed by their owning level
*/
const removingImportNodes: _ts.Statement[][] = []
/**
* Called when we enter a block to increase the level
*/
const enter = () => {
level++
// reuse arrays
if (removingImportNodes[level]) {
removingImportNodes[level].splice(0, removingImportNodes[level].length)
}
}
/**
* Called when we leave a block to decrease the level
*/
const exit = () => level--
const visitor: _ts.Visitor = node => {
// enter this level
enter()

// visit each child
let resultNode = ts.visitEachChild(node, visitor, ctx)
// check if we have something to delete in this level
if (removingImportNodes[level]?.length) {
const newNode = ts.getMutableClone(resultNode) as _ts.Block
const otherStmts = (resultNode as _ts.Block).statements.filter(
(s) => !removingImportNodes[level].includes(s) && !isVueRequireStmt(s) && !isVueNamespaceImport(s),
)
resultNode = {
...newNode,
statements: ts.createNodeArray([...otherStmts]),
} as _ts.Statement
}
if (ts.isCallExpression(resultNode)
&& ts.isPropertyAccessExpression(resultNode.expression)
&& ts.isIdentifier(resultNode.expression.expression) && importNames.has(resultNode.expression.expression.text)) {
const newNode = ts.getMutableClone(resultNode)

resultNode = {
...newNode,
expression: {
...newNode.expression,
expression: ts.createIdentifier('vue_1')
}
} as _ts.CallExpression
}

// exit the level
exit()

if ((isVueRequireStmt(resultNode) || isVueNamespaceImport(resultNode)) && level === ROOT_LEVEL_AST) {
if (isVueNamespaceImport(resultNode)
&& resultNode.importClause?.namedBindings
&& ts.isNamespaceImport(resultNode.importClause.namedBindings)
&& resultNode.importClause.namedBindings.name) {
importNames.add(resultNode.importClause.namedBindings.name.text)
}
removingImportNodes[ level ] = [ resultNode as _ts.Statement ]
}

// finally returns the currently visited node
return resultNode
}

return visitor
}

// returns the transformer factory
return (ctx: _ts.TransformationContext): _ts.Transformer<_ts.SourceFile> =>
logger.wrap(
{ [LogContexts.logLevel]: LogLevels.debug, call: null },
'visitSourceFileNode(): remove vue import',
(sf: _ts.SourceFile) => ts.visitNode(sf, createVisitor(ctx, sf)),
)
}
59 changes: 59 additions & 0 deletions tests/remove-vue-import.spec.ts
@@ -0,0 +1,59 @@
import { ConfigSet } from 'ts-jest/dist/config/config-set';
import tsc from 'typescript';

import * as removeVueImport from '../src/transformers/remove-vue-import';

const CODE_WITH_VUE_IMPORTS = `
import { defineComponent, ref } from 'vue'
import * as vue1 from 'vue'
export default defineComponent({
setup() {
const count = ref<string>(5)
const inc = () => count.value++
vue1.foo()
return {
count,
inc
}
}
})
`;

describe('remove-vue-import', () => {
test('should have correct signature', () => {
expect(removeVueImport.name).toBe('remove-vue-import');
expect(typeof removeVueImport.version).toBe('number');
expect(removeVueImport.version).toBeGreaterThan(0);
expect(typeof removeVueImport.factory).toBe('function');
});

test('should remove all vue imports', () => {
const configSet = new ConfigSet(Object.create(null));
const createFactory = () => removeVueImport.factory(configSet);
const transpile = (source: string) =>
tsc.transpileModule(source, {
transformers: { before: [ createFactory() ] },
});

const out = transpile(CODE_WITH_VUE_IMPORTS);

expect(out.outputText).toMatchInlineSnapshot(`
"\\"use strict\\";
Object.defineProperty(exports, \\"__esModule\\", { value: true });
exports.default = vue_1.defineComponent({
setup: function () {
var count = vue_1.ref(5);
var inc = function () { return count.value++; };
vue_1.foo();
return {
count: count,
inc: inc
};
}
});
"
`);
});
});

0 comments on commit b0834da

Please sign in to comment.