Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(transformer): add remove-vue-import transformer #7

Merged
merged 1 commit into from
Oct 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
140 changes: 140 additions & 0 deletions src/transformers/remove-vue-import.ts
Original file line number Diff line number Diff line change
@@ -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: {
* before: ['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)),
)
}
50 changes: 50 additions & 0 deletions tests/__snapshots__/remove-vue-import.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`remove-vue-import should remove all vue imports 1`] = `
"\\"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++; };
return {
count: count,
inc: inc
};
}
});
"
`;

exports[`remove-vue-import should remove all vue imports 2`] = `
"\\"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++; };
return {
count: count,
inc: inc
};
}
});
"
`;

exports[`remove-vue-import should remove all vue imports 3`] = `
"\\"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.bar();
return {
count: count,
inc: inc
};
}
});
"
`;
77 changes: 77 additions & 0 deletions tests/remove-vue-import.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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_NAME_IMPORT = `
import { defineComponent, ref } from 'vue'

export default defineComponent({
setup() {
const count = ref<string>(5)
const inc = () => count.value++

return {
count,
inc
}
}
})
`;
const CODE_WITH_START_IMPORT = `
import * as vue1 from 'vue'

export default vue1.defineComponent({
setup() {
const count = vue1.ref<string>(5)
const inc = () => count.value++

return {
count,
inc
}
}
})
`;
const CODE_WITH_REQUIRE = `
const vue2 = require('vue')

export default vue2.defineComponent({
setup() {
const count = vue2.ref<string>(5)
const inc = () => count.value++
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.each([
CODE_WITH_NAME_IMPORT,
CODE_WITH_START_IMPORT,
CODE_WITH_REQUIRE,
])("should remove all vue imports", data => {
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(data);

expect(out.outputText).toMatchSnapshot()
});
});