Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(transformer): add remove-vue-import transformer
Closes #4
- Loading branch information
Showing
2 changed files
with
198 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)), | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
} | ||
`); | ||
}); | ||
}); |