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 0c7875d
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 0 deletions.
140 changes: 140 additions & 0 deletions src/transformers/remove-vue-import.ts
@@ -0,0 +1,140 @@
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 VUE_GLOBAL_VARIABLE_NAME = 'vue_1'
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
/**
* e.g. const vue2 = require('vue')
*/
const isVueRequireStmt = (node: _ts.Node) => isRequire(node)
&& (node.arguments[0] as _ts.StringLiteral).text === VUE_GLOBAL_NAME
const isVueRequireImportStmt = (node: _ts.Node) => ts.isVariableStatement(node) && node.declarationList.declarations.length === 1
&& ts.isVariableDeclaration(node.declarationList.declarations[0])
&& node.declarationList.declarations[0].initializer
&& isVueRequireStmt(node.declarationList.declarations[0].initializer)
/**
* e.g.
* - import { defineComponent, ref } from 'vue'
* - import * as vue1 from 'vue'
*/
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
/**
* Called when we enter a block to increase the level
*/
const enter = () => {
level++
}
/**
* 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 require syntax, e.g. `const vue2 = require('vue')`
* and get the variable name to replace with global name vue_1
*/
if (ts.isVariableDeclaration(resultNode) && resultNode.initializer && isVueRequireStmt(resultNode.initializer)
&& ts.isIdentifier(resultNode.name)) {
importNames.add(resultNode.name.text)
}
/**
* Check import namespace syntax's, e.g. `import * as vue1 from 'vue'`
* and get the import name to replace with global name vue_1 later.
*/
if (isVueNamespaceImport(resultNode)
&& resultNode.importClause?.namedBindings
&& ts.isNamespaceImport(resultNode.importClause.namedBindings)
&& resultNode.importClause.namedBindings.name) {
importNames.add(resultNode.importClause.namedBindings.name.text)
}

// Filter all vue import statements and remove them
if (level === ROOT_LEVEL_AST) {
const newNode = ts.getMutableClone(resultNode) as _ts.Block
const otherStmts = (resultNode as _ts.Block).statements.filter(
(s) => !isVueNamespaceImport(s) && !isVueRequireImportStmt(s),
)
resultNode = {
...newNode,
statements: ts.createNodeArray([...otherStmts]),
} as _ts.Statement
}
/**
* Replace all call expressions' identifier by global vue variable name `vue_1`
*/
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_GLOBAL_VARIABLE_NAME)
}
} as _ts.CallExpression
}

// exit the level
exit()

// 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)),
)
}
62 changes: 62 additions & 0 deletions tests/remove-vue-import.spec.ts
@@ -0,0 +1,62 @@
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'
const vue2 = require('vue')
export default defineComponent({
setup() {
const count = ref<string>(5)
const inc = () => count.value++
vue1.foo()
vue2.bar()
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();
vue_1.bar();
return {
count: count,
inc: inc
};
}
});
"
`);
});
});

0 comments on commit 0c7875d

Please sign in to comment.