From 888322fefdb30f30cc7e9f95ee812536404c5368 Mon Sep 17 00:00:00 2001 From: Ahn Date: Sun, 20 Sep 2020 15:47:58 +0200 Subject: [PATCH] feat(transformer): add remove-vue-import transformer Closes #4 --- src/transformers/no-duplicated-vue-import.ts | 118 +++++++++++++++++++ tests/no-duplicated-vue-import.spec.ts | 80 +++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 src/transformers/no-duplicated-vue-import.ts create mode 100644 tests/no-duplicated-vue-import.spec.ts diff --git a/src/transformers/no-duplicated-vue-import.ts b/src/transformers/no-duplicated-vue-import.ts new file mode 100644 index 0000000..c01b548 --- /dev/null +++ b/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)), + ) +} diff --git a/tests/no-duplicated-vue-import.spec.ts b/tests/no-duplicated-vue-import.spec.ts new file mode 100644 index 0000000..c8b16bf --- /dev/null +++ b/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(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, + } + `); + }); +});