From 76b3da205b1d37d2a3a7b77c4f3ac4d03adac866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90?= Date: Wed, 22 Jun 2022 14:06:20 +0800 Subject: [PATCH] refactor!: improve API --- package.json | 7 +- pnpm-lock.yaml | 7 +- src/core/options.ts | 19 +--- src/core/presets/remove-wrapper-function.ts | 22 ++++ src/core/transform.ts | 84 +++++++++++---- src/core/types.ts | 26 +++++ src/index.ts | 1 + src/presets.ts | 1 + tests/basic.test.ts | 112 +++++++++++--------- tests/remove-wrapper-function.test.ts | 28 +++++ 10 files changed, 211 insertions(+), 96 deletions(-) create mode 100644 src/core/presets/remove-wrapper-function.ts create mode 100644 src/core/types.ts create mode 100644 src/presets.ts create mode 100644 tests/remove-wrapper-function.test.ts diff --git a/package.json b/package.json index 41cf5c9..2c7337e 100644 --- a/package.json +++ b/package.json @@ -47,9 +47,9 @@ "require": "./dist/esbuild.js", "import": "./dist/esbuild.mjs" }, - "./cores": { - "require": "./dist/cores.js", - "import": "./dist/cores.mjs" + "./presets": { + "require": "./dist/presets.js", + "import": "./dist/presets.mjs" }, "./*": "./*" }, @@ -89,6 +89,7 @@ "estree-walker": "^2.0.2", "fast-glob": "^3.2.11", "prettier": "^2.7.1", + "rollup": "^2.75.7", "tsup": "^6.1.2", "tsx": "^3.4.3", "typescript": "^4.7.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 730148d..f000069 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,7 @@ specifiers: fast-glob: ^3.2.11 magic-string: ^0.26.2 prettier: ^2.7.1 + rollup: ^2.75.7 tsup: ^6.1.2 tsx: ^3.4.3 typescript: ^4.7.4 @@ -27,7 +28,7 @@ dependencies: '@babel/types': 7.18.4 '@rollup/pluginutils': 4.2.1 magic-string: 0.26.2 - unplugin: 0.7.0_vite@2.9.12 + unplugin: 0.7.0_rollup@2.75.7+vite@2.9.12 devDependencies: '@sxzz/eslint-config': 2.2.2_eslint@8.18.0 @@ -38,6 +39,7 @@ devDependencies: estree-walker: 2.0.2 fast-glob: 3.2.11 prettier: 2.7.1 + rollup: 2.75.7 tsup: 6.1.2_typescript@4.7.4 tsx: 3.4.3 typescript: 4.7.4 @@ -2946,7 +2948,7 @@ packages: '@types/unist': 2.0.6 dev: true - /unplugin/0.7.0_vite@2.9.12: + /unplugin/0.7.0_rollup@2.75.7+vite@2.9.12: resolution: {integrity: sha512-OsiFrgybmqm5bGuaodvbLYhqUrvGuRHRMZDhddKEXTDbuQ1x+hR7M1WpQguXj03whVYjEYChhFo738cZH5RNig==} peerDependencies: esbuild: '>=0.13' @@ -2965,6 +2967,7 @@ packages: dependencies: acorn: 8.7.1 chokidar: 3.5.3 + rollup: 2.75.7 vite: 2.9.12 webpack-sources: 3.2.3 webpack-virtual-modules: 0.4.3 diff --git a/src/core/options.ts b/src/core/options.ts index 391b83c..8d6a69a 100644 --- a/src/core/options.ts +++ b/src/core/options.ts @@ -1,8 +1,7 @@ import { toArray } from '@antfu/utils' -import type MagicString from 'magic-string' +import type { Transformer } from './types' import type { ParserOptions } from '@babel/parser' -import type { Arrayable, Awaitable } from '@antfu/utils' -import type { Node } from '@babel/types' +import type { Arrayable } from '@antfu/utils' import type { FilterPattern } from '@rollup/pluginutils' export interface Options { @@ -13,20 +12,6 @@ export interface Options { transformer?: Arrayable> } -export interface Transformer { - filterFile?: (id: string) => Awaitable - filterNode?: - | ((node: Node, parent: Node) => Awaitable) - | ((node: Node, parent: Node) => node is T) - transform: ( - node: T, - code: MagicString, - context: { - id: string - } - ) => Awaitable -} - export type OptionsResolved = Omit, 'transformer'> & { transformer: Transformer[] } diff --git a/src/core/presets/remove-wrapper-function.ts b/src/core/presets/remove-wrapper-function.ts new file mode 100644 index 0000000..cde2ce6 --- /dev/null +++ b/src/core/presets/remove-wrapper-function.ts @@ -0,0 +1,22 @@ +import { toArray } from '@antfu/utils' +import type { Arrayable } from '@antfu/utils' +import type { CallExpression } from '@babel/types' +import type { Transformer } from '../types' + +/** + * Removes wrapper function. e.g `defineComponent`, `defineConfig`... + * @param functionNames - function names to remove + * @returns Transformer + */ +export const RemoveWrapperFunction = ( + functionNames: Arrayable +): Transformer => ({ + onNode: (node) => + node.type === 'CallExpression' && + node.callee.type === 'Identifier' && + toArray(functionNames).includes(node.callee.name), + + transform(node) { + return node.arguments[0] + }, +}) diff --git a/src/core/transform.ts b/src/core/transform.ts index 768554c..2eb2f7a 100644 --- a/src/core/transform.ts +++ b/src/core/transform.ts @@ -1,33 +1,46 @@ import { walk } from 'estree-walker' import MagicString from 'magic-string' import { parseCode } from './parse' -import type { TransformResult } from 'unplugin' -import type { Node } from '@babel/types' -import type { OptionsResolved, Transformer } from './options' +import type { NodeRef, Transformer, TransformerParsed } from './types' +import type { SourceMap } from 'rollup' +import type { ExpressionStatement, Node } from '@babel/types' +import type { OptionsResolved } from './options' -export const transform = async ( - code: string, - id: string, - options: Pick -): Promise => { - interface TransformerParsed { - transformer: Transformer - nodes: Node[] +const nodeRefs: Map = new Map() +function getNodeRef(node: Node) { + if (nodeRefs.has(node)) return nodeRefs.get(node)! + const ref: NodeRef = { + value: node, + set(node: Node) { + this.value = node + }, } + nodeRefs.set(node, ref) + return ref +} +async function getTransformersByFile(transformer: Transformer[], id: string) { const transformers = ( await Promise.all( - options.transformer.map( - async (t): Promise => { - if (t.filterFile && !(await t.filterFile(id))) return undefined - return { - transformer: t, - nodes: [], - } + transformer.map(async (t): Promise => { + if (t.transformInclude && !(await t.transformInclude(id))) + return undefined + return { + transformer: t, + nodes: [], } - ) + }) ) ).filter((t): t is TransformerParsed => !!t) + return transformers +} + +export const transform = async ( + code: string, + id: string, + options: Pick +): Promise<{ code: string; map: SourceMap } | undefined> => { + const transformers = await getTransformersByFile(options.transformer, id) if (transformers.length === 0) return const nodes = parseCode(code, id, options.parserOptions) @@ -38,11 +51,11 @@ export const transform = async ( if (!node.type) return const p = (async () => { for (const { transformer, nodes } of transformers) { - if (transformer.filterNode) { - const bool = await transformer.filterNode?.(node, parent) + if (transformer.onNode) { + const bool = await transformer.onNode?.(node, parent) if (!bool) continue } - nodes.push(node) + nodes.push(getNodeRef(node)) } })() promises.push(p) @@ -54,7 +67,32 @@ export const transform = async ( const s = new MagicString(code) for (const { transformer, nodes } of transformers) { for (const node of nodes) { - await transformer.transform(node, s, { id }) + const value = node.value + const result = await transformer.transform(value, code, { id }) + if (!result) continue + + let newAST: Node + if (typeof result === 'string') { + s.overwrite(value.start!, value.end!, result) + newAST = ( + parseCode( + `(${result})`, + id, + options.parserOptions + )[0] as ExpressionStatement + ).expression + newAST.start = value.start! + newAST.end = value.end! + } else { + s.overwrite( + value.start!, + value.end!, + s.slice(result.start!, result.end!) + ) + newAST = result + } + + node.set(newAST) } } if (!s.hasChanged()) return undefined diff --git a/src/core/types.ts b/src/core/types.ts new file mode 100644 index 0000000..b2d41f9 --- /dev/null +++ b/src/core/types.ts @@ -0,0 +1,26 @@ +import type { Awaitable } from '@antfu/utils' +import type { Node } from '@babel/types' + +export interface TransformerParsed { + transformer: Transformer + nodes: NodeRef[] +} + +export interface NodeRef { + value: Node + set: (node: Node) => void +} + +export interface Transformer { + transformInclude?: (id: string) => Awaitable + onNode?: + | ((node: Node, parent: Node) => Awaitable) + | ((node: Node, parent: Node) => node is T) + transform: ( + node: T, + code: string, + context: { + id: string + } + ) => Awaitable +} diff --git a/src/index.ts b/src/index.ts index be3e8ab..6b6efaa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,3 +26,4 @@ export default createUnplugin((options = {}) => { export * from './core/options' export * from './core/parse' export * from './core/transform' +export * from './core/types' diff --git a/src/presets.ts b/src/presets.ts new file mode 100644 index 0000000..4743d6b --- /dev/null +++ b/src/presets.ts @@ -0,0 +1 @@ +export * from './core/presets/remove-wrapper-function' diff --git a/tests/basic.test.ts b/tests/basic.test.ts index 5e98d5d..6273d23 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -1,68 +1,78 @@ import { expect, test } from 'vitest' import { transform } from '../src/core/transform' -import type { CallExpression, Identifier, StringLiteral } from '@babel/types' +import type { Identifier, NumericLiteral, StringLiteral } from '@babel/types' import type { OptionsResolved, Transformer } from '../src/core/options' +const changeString: Transformer = { + onNode: (node): node is StringLiteral => node.type === 'StringLiteral', + transform() { + return "'Hello'" + }, +} + +const changeVarName: Transformer = { + onNode: (node): node is Identifier => + node.type === 'Identifier' && node.name === 'foo', + transform() { + return 'newName' + }, +} + +const overwriteVarName: Transformer = { + onNode: (node): node is Identifier => node.type === 'Identifier', + transform(node) { + return `overwrite_${node.name}` + }, +} + +const timesTen: Transformer = { + onNode: (node): node is NumericLiteral => node.type === 'NumericLiteral', + transform(node) { + return `${node.value * 10}` + }, +} + test('basic', async () => { - const source = `const foo = 'string'` + const source = `const foo = 'string'\nlet i = 10` const options: Pick = { transformer: [], parserOptions: {}, } - let code = await transform(source, 'foo.js', options) + let code = (await transform(source, 'foo.js', options))?.code expect(code).toMatchInlineSnapshot('undefined') - const transformer: Transformer = { - filterNode: (node): node is StringLiteral => node.type === 'StringLiteral', - transform(node, code) { - code.overwrite(node.start!, node.end!, '"Hello"') - }, - } - options.transformer.push(transformer) - code = await transform(source, 'foo.js', options) - expect(code).toMatchInlineSnapshot('"const foo = \\"Hello\\""') + options.transformer = [changeString] + code = (await transform(source, 'foo.js', options))?.code + expect(code).toMatchInlineSnapshot(` + "const foo = 'Hello' + let i = 10" + `) - const transformer2: Transformer = { - filterNode: (node): node is Identifier => node.type === 'Identifier', - transform(node, code) { - code.overwrite(node.start!, node.end!, 'newName') - }, - } - options.transformer.push(transformer2) - code = await transform(source, 'foo.js', options) - expect(code).toMatchInlineSnapshot('"const newName = \\"Hello\\""') + options.transformer = [changeVarName] + code = (await transform(source, 'foo.js', options))?.code + expect(code).toMatchInlineSnapshot(` + "const newName = 'string' + let i = 10" + `) - options.transformer.splice(0, 1) - code = await transform(source, 'foo.js', options) - expect(code).toMatchInlineSnapshot('"const newName = \'string\'"') -}) + options.transformer = [changeString, changeVarName] + code = (await transform(source, 'foo.js', options))?.code + expect(code).toMatchInlineSnapshot(` + "const newName = 'Hello' + let i = 10" + `) -test('remove wrapper function', async () => { - const source = `const comp = defineComponent({ - render() { - return [] - } - })` - const transformer: Transformer = { - filterNode: (node) => - node.type === 'CallExpression' && - node.callee.type === 'Identifier' && - node.callee.name === 'defineComponent', - transform(node, code) { - const [arg] = node.arguments - code.overwrite(node.start!, node.end!, code.slice(arg.start!, arg.end!)) - }, - } - const options: Pick = { - transformer: [transformer], - parserOptions: {}, - } - const code = await transform(source, 'foo.js', options) + options.transformer = [changeString, changeVarName, overwriteVarName] + code = (await transform(source, 'foo.js', options))?.code + expect(code).toMatchInlineSnapshot(` + "const overwrite_newName = 'Hello' + let overwrite_i = 10" + `) + + options.transformer = [timesTen, timesTen, timesTen] + code = (await transform(source, 'foo.js', options))?.code expect(code).toMatchInlineSnapshot(` - "const comp = { - render() { - return [] - } - }" + "const foo = 'string' + let i = 10000" `) }) diff --git a/tests/remove-wrapper-function.test.ts b/tests/remove-wrapper-function.test.ts new file mode 100644 index 0000000..a32f964 --- /dev/null +++ b/tests/remove-wrapper-function.test.ts @@ -0,0 +1,28 @@ +import { expect, test } from 'vitest' +import { transform } from '../src/core/transform' +import { RemoveWrapperFunction } from '../src/presets' +import type { OptionsResolved } from '../src/core/options' + +test('remove wrapper function', async () => { + const source = `const comp = defineComponent({ + render() { + return [] + } + }) + console.log(mutable({} as const)) + ` + const options: Pick = { + transformer: [RemoveWrapperFunction(['defineComponent', 'mutable'])], + parserOptions: {}, + } + const code = (await transform(source, 'foo.ts', options))?.code + expect(code).toMatchInlineSnapshot(` + "const comp = { + render() { + return [] + } + } + console.log({} as const) + " + `) +})