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
182 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,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<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 | ||
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)), | ||
) | ||
} |
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,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<string>(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 | ||
}; | ||
} | ||
}); | ||
" | ||
`); | ||
}); | ||
}); |