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 20, 2020
1 parent 7a44efd commit 888322f
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 0 deletions.
118 changes: 118 additions & 0 deletions src/transformers/no-duplicated-vue-import.ts
@@ -0,0 +1,118 @@
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 = 'no-duplicated-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/vue-no-duplicated-import']
* }
* }
* }
* }
*/
export function factory(cs: ConfigSet): (ctx: _ts.TransformationContext) => _ts.Transformer<_ts.SourceFile> {
const logger = cs.logger.child({ namespace: 'vue-no-duplicated-import' })
const ts = cs.compilerModule
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 isDuplicatedGlobalVueImport = (node: _ts.Node) => ts.isVariableStatement(node)
&& node.declarationList.declarations[0].initializer
&& isRequire(node.declarationList.declarations[0].initializer)
&& (node.declarationList.declarations[0].initializer.arguments[0] as _ts.StringLiteral).text === VUE_GLOBAL_NAME
&& ts.isIdentifier(node.declarationList.declarations[0].name)
/**
* Handle import declaration import { defineComponent, ref } from 'vue' which doesn't generate Identifier text when
* this transformer is executed
*/
&& !node.declarationList.declarations[0].name.text

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 toBeDeletedNodes: _ts.Statement[][] = []
/**
* Called when we enter a block to increase the level
*/
const enter = () => {
level++
// reuse arrays
if (toBeDeletedNodes[level]) {
toBeDeletedNodes[level].splice(0, toBeDeletedNodes[level].length)
}
}
/**
* Called when we leave a block to decrease the level
*/
const exit = () => level--
/**
* Adds a node to the list of nodes to be deleted in the current level
*/
const gatherDeleteNodes = (node: _ts.Statement) => {
if (toBeDeletedNodes[level]) {
toBeDeletedNodes[level].push(node)
} else {
toBeDeletedNodes[level] = [node]
}
}
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 (toBeDeletedNodes[level]?.length) {
const newNode = ts.getMutableClone(resultNode) as _ts.Block
const otherStmts = (resultNode as _ts.Block).statements.filter(
(s) => !toBeDeletedNodes[level].includes(s) && !isDuplicatedGlobalVueImport(s),
)
resultNode = {
...newNode,
statements: ts.createNodeArray([...otherStmts]),
} as _ts.Statement
}

// exit the level
exit()

if (isDuplicatedGlobalVueImport(resultNode) && level === ROOT_LEVEL_AST) {
gatherDeleteNodes(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(): vue no duplicated import',
(sf: _ts.SourceFile) => ts.visitNode(sf, createVisitor(ctx, sf)),
)
}
80 changes: 80 additions & 0 deletions tests/no-duplicated-vue-import.spec.ts
@@ -0,0 +1,80 @@
import { ConfigSet } from "ts-jest/dist/config/config-set";
import tsc from "typescript";

import * as removeVueImport from "../src/transformers/no-duplicated-vue-import";

const CODE_WITH_VUE_IMPORT = `
import { defineComponent, ref } from 'vue'
const vue_1 = require('vue')
import * as vue1 from 'vue'
const vue2 = require('vue')
import vue3 = require('vue')
export default defineComponent({
setup() {
const count = ref<string>(5)
const inc = () => count.value++
const vue4 = require('vue')
vue1.foo(1)
vue2.foo(2)
vue3.foo(3)
vue4.foo(4)
vue_1.foo(5)
return {
count,
inc
}
}
})
`;

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

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

const out = transpile(CODE_WITH_VUE_IMPORT);

expect(out).toMatchInlineSnapshot(`
Object {
"diagnostics": Array [],
"outputText": "\\"use strict\\";
Object.defineProperty(exports, \\"__esModule\\", { value: true });
var vue_1 = require('vue');
var vue1 = require(\\"vue\\");
var vue2 = require('vue');
var vue3 = require(\\"vue\\");
exports.default = vue_2.defineComponent({
setup: function () {
var count = vue_2.ref(5);
var inc = function () { return count.value++; };
var vue4 = require('vue');
vue1.foo(1);
vue2.foo(2);
vue3.foo(3);
vue4.foo(4);
vue_1.foo(5);
return {
count: count,
inc: inc
};
}
});
",
"sourceMapText": undefined,
}
`);
});
});

0 comments on commit 888322f

Please sign in to comment.