From b0834da1a2ad9dd5babac5d93e18f7251eac1dc7 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/remove-vue-import.ts | 123 ++++++++++++++++++++++++++ tests/remove-vue-import.spec.ts | 59 ++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 src/transformers/remove-vue-import.ts create mode 100644 tests/remove-vue-import.spec.ts diff --git a/src/transformers/remove-vue-import.ts b/src/transformers/remove-vue-import.ts new file mode 100644 index 0000000..6be051c --- /dev/null +++ b/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 = new Set(); + 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)), + ) +} diff --git a/tests/remove-vue-import.spec.ts b/tests/remove-vue-import.spec.ts new file mode 100644 index 0000000..2eaa91d --- /dev/null +++ b/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(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 + }; + } + }); + " + `); + }); +});