diff --git a/package.json b/package.json index b024a05..48c43ef 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,16 @@ "require": "./dist/index.cjs" }, "./transform": { - "types": "./dist/transform.d.ts", - "import": "./dist/transform.mjs" + "types": "./dist/transform/index.d.ts", + "import": "./dist/transform/index.mjs" + }, + "./transform/acorn": { + "types": "./dist/transform/acorn.d.ts", + "import": "./dist/transform/acorn.mjs" + }, + "./transform/oxc": { + "types": "./dist/transform/oxc.d.ts", + "import": "./dist/transform/oxc.mjs" }, "./plugin": { "types": "./dist/plugin.d.ts", @@ -50,6 +58,10 @@ "magic-string": "^0.30.21", "unplugin": "^2.3.11" }, + "peerDependencies": { + "oxc-parser": "^0.82.1", + "oxc-walker": "^0.4.0" + }, "devDependencies": { "@types/estree": "^1.0.8", "@types/node": "^25.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8b4b02..70c8649 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,12 @@ importers: magic-string: specifier: ^0.30.21 version: 0.30.21 + oxc-parser: + specifier: ^0.82.1 + version: 0.82.1 + oxc-walker: + specifier: ^0.4.0 + version: 0.4.0(oxc-parser@0.82.1) unplugin: specifier: ^2.3.11 version: 2.3.11 @@ -82,6 +88,15 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@emnapi/core@1.4.5': + resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} + + '@emnapi/runtime@1.4.5': + resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} + + '@emnapi/wasi-threads@1.0.4': + resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -472,6 +487,101 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@napi-rs/wasm-runtime@1.0.3': + resolution: {integrity: sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==} + + '@oxc-parser/binding-android-arm64@0.82.1': + resolution: {integrity: sha512-Vph9abEKcjDm1qypjgvvHzrMcjIC5Nhi5kVO/GQ9WTRIbVEq5yS7vWp3VYh6TQ405DxAX2z8g2o67Ovdh3r1hA==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.82.1': + resolution: {integrity: sha512-0biUTb+VBpbNG3begg5e2FW9DlOW/7wLLr/sF9JLXLKiCQnYMTTQ/FTkHMqxkDJBON+FTiHpnvp4aS2eUv3lkA==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.82.1': + resolution: {integrity: sha512-VGIJSzPsWAK501FOOW44TspbZ8eWIOhY1FfBNIsn0JTN3Ve9uk/waSVN/lysQzMJOX3S3mIhtrVGdKQ3fum8GQ==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.82.1': + resolution: {integrity: sha512-5oLRbNxkNQz8bRuvr07xOTJIzsmaRg6pLHxH2HqlRvhhpo9sXJN4yROSGKgoQSFCSWZyDylg18Aw5cxvSxfJgw==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.82.1': + resolution: {integrity: sha512-lhbZaoDoxbxNp83GOvcXw02R+qJk3ckUPmHmbxkTCjSD/BttrjTLsZ30HJdH2rDB9kmBtfsbg3/Mnz/bU2D6ow==} + engines: {node: '>=20.0.0'} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.82.1': + resolution: {integrity: sha512-0dLHzsC22O+9/diTCjDrTDSEaTUsnvbvq2jTGpHNaVg7903Jieb3Eftqut3MpBea9764a/IZkGoKP3btCdQnYQ==} + engines: {node: '>=20.0.0'} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.82.1': + resolution: {integrity: sha512-ftux8M4nPbYj/6lEO9PbfZ8knacHx/o4JfwueDbcrWOf53otJ9jHNITbZooIgl7zwGCW+JSZgJis2Ts4u9feQw==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-arm64-musl@0.82.1': + resolution: {integrity: sha512-dvtBGwTsW2bUHB0c6lVj0KIm7NT00xcsATWTCXiwGoDIGl/FPJctudpj+nMwXyjdPk3rlKRDjJ3pHcd7pYR1DQ==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-gnu@0.82.1': + resolution: {integrity: sha512-y2A2lXUyppruU5AE6MforoqAvptwRGSRV8rO32Xv+vdflJH3rKxqKhWxW2bfRc14YhKRbQUcwPaW4nEqPPvwag==} + engines: {node: '>=20.0.0'} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-s390x-gnu@0.82.1': + resolution: {integrity: sha512-2oajEj8l0TGyWawVl+cuFjn7mcVBCR2fTO2EFsCf9WH7KEG/gyU86G5XDLN6tnl1E3Gqe88A09s0J8UUj+qUKA==} + engines: {node: '>=20.0.0'} + cpu: [s390x] + os: [linux] + + '@oxc-parser/binding-linux-x64-gnu@0.82.1': + resolution: {integrity: sha512-r0XCtdH36uXlwH504O2zRXqjhD8NjBQaPgInnwGMFskgPKPQBJIAYqWqrBzTEOJEPQEuhksfjsGNn7TAOQKdNQ==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-linux-x64-musl@0.82.1': + resolution: {integrity: sha512-CW6Rw6RME+cp1AmS2GFT7wdg/7nCxL/pHGbwhq7RumO6ITBKzicd6YH2rkQrMYy0x8ZTzSdS4YjbPBHT78E4jA==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-wasm32-wasi@0.82.1': + resolution: {integrity: sha512-0Vy/d8iBwFxrXldnc5GfXFmF8Pxgrqv14d/htz5u2kb02bhCCWIk+GjI2gKEcQfOgl2Bn4oOK3tL5rUrFNPRPg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.82.1': + resolution: {integrity: sha512-qfWIjAPt7ljozHkIH8sa155yH4rLrG8w36GRRhSZ1ltQT6HFgNZO56HSyZShSw3++3zBb5AkHVVnBWvBTo5zjQ==} + engines: {node: '>=20.0.0'} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.82.1': + resolution: {integrity: sha512-xiMVlP38bsq/7FHR6e+pZQ8XJetPhNToPy5mNh227pIybSWWjcdPTHo0LAJmIrsqrx5+/msIkZ+Wm/E+SXBkww==} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.82.1': + resolution: {integrity: sha512-MCPtxtmHRmCqMI+DZyADBtW7QrFQ6OtQvHVAu576LWu6Y5zshLNabDc6RDJE/+uKVdypd9ZU1r05J/547VooPQ==} + '@rollup/plugin-alias@5.1.1': resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} engines: {node: '>=14.0.0'} @@ -639,6 +749,9 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tybys/wasm-util@0.10.0': + resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1354,6 +1467,12 @@ packages: lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + magic-regexp@0.10.0: + resolution: {integrity: sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1456,6 +1575,15 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + oxc-parser@0.82.1: + resolution: {integrity: sha512-2bBrazc/0wpA/+XECTwLA6dvp2Swp9vm/psgvwDz4CcxwfvYyQN0/ghw9km0LFpPIDSrxhltJSsfajhb2NZq0A==} + engines: {node: '>=20.0.0'} + + oxc-walker@0.4.0: + resolution: {integrity: sha512-x5TJAZQD3kRnRBGZ+8uryMZUwkTYddwzBftkqyJIcmpBOXmoK/fwriRKATjZroR2d+aS7+2w1B0oz189bBTwfw==} + peerDependencies: + oxc-parser: '>=0.72.0' + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1821,10 +1949,16 @@ packages: peerDependencies: typescript: '>=4.8.4' + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-level-regexp@0.1.17: + resolution: {integrity: sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==} + typescript-eslint@8.50.0: resolution: {integrity: sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1998,6 +2132,22 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@emnapi/core@1.4.5': + dependencies: + '@emnapi/wasi-threads': 1.0.4 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.5': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.4': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.25.12': optional: true @@ -2239,6 +2389,62 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@napi-rs/wasm-runtime@1.0.3': + dependencies: + '@emnapi/core': 1.4.5 + '@emnapi/runtime': 1.4.5 + '@tybys/wasm-util': 0.10.0 + optional: true + + '@oxc-parser/binding-android-arm64@0.82.1': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.82.1': + optional: true + + '@oxc-parser/binding-darwin-x64@0.82.1': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.82.1': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.82.1': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.82.1': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.82.1': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.82.1': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.82.1': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.82.1': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.82.1': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.82.1': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.82.1': + dependencies: + '@napi-rs/wasm-runtime': 1.0.3 + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.82.1': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.82.1': + optional: true + + '@oxc-project/types@0.82.1': {} + '@rollup/plugin-alias@5.1.1(rollup@4.53.5)': optionalDependencies: rollup: 4.53.5 @@ -2354,6 +2560,11 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tybys/wasm-util@0.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -3188,6 +3399,20 @@ snapshots: lodash.uniq@4.5.0: {} + magic-regexp@0.10.0: + dependencies: + estree-walker: 3.0.3 + magic-string: 0.30.17 + mlly: 1.8.0 + regexp-tree: 0.1.27 + type-level-regexp: 0.1.17 + ufo: 1.6.1 + unplugin: 2.3.11 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3308,6 +3533,32 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + oxc-parser@0.82.1: + dependencies: + '@oxc-project/types': 0.82.1 + optionalDependencies: + '@oxc-parser/binding-android-arm64': 0.82.1 + '@oxc-parser/binding-darwin-arm64': 0.82.1 + '@oxc-parser/binding-darwin-x64': 0.82.1 + '@oxc-parser/binding-freebsd-x64': 0.82.1 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.82.1 + '@oxc-parser/binding-linux-arm-musleabihf': 0.82.1 + '@oxc-parser/binding-linux-arm64-gnu': 0.82.1 + '@oxc-parser/binding-linux-arm64-musl': 0.82.1 + '@oxc-parser/binding-linux-riscv64-gnu': 0.82.1 + '@oxc-parser/binding-linux-s390x-gnu': 0.82.1 + '@oxc-parser/binding-linux-x64-gnu': 0.82.1 + '@oxc-parser/binding-linux-x64-musl': 0.82.1 + '@oxc-parser/binding-wasm32-wasi': 0.82.1 + '@oxc-parser/binding-win32-arm64-msvc': 0.82.1 + '@oxc-parser/binding-win32-x64-msvc': 0.82.1 + + oxc-walker@0.4.0(oxc-parser@0.82.1): + dependencies: + estree-walker: 3.0.3 + magic-regexp: 0.10.0 + oxc-parser: 0.82.1 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -3652,10 +3903,15 @@ snapshots: dependencies: typescript: 5.9.3 + tslib@2.8.1: + optional: true + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 + type-level-regexp@0.1.17: {} + typescript-eslint@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) diff --git a/src/plugin.ts b/src/plugin.ts index af07aca..aff4618 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,7 +1,11 @@ import { createUnplugin, type HookFilter } from "unplugin"; -import { createTransformer, type TransformerOptions } from "./transform"; +import { type TransformerOptions } from "./transform/_shared.js"; export interface UnctxPluginOptions extends TransformerOptions { + /** The parser to use. + * @default 'acorn' + */ + parser?: "acorn" | "oxc"; /** Plugin Hook Filter for the transform hook * @see https://unplugin.unjs.io/guide/#filters */ @@ -10,17 +14,32 @@ export interface UnctxPluginOptions extends TransformerOptions { transformInclude?: (id: string) => boolean; } +let transformer: + | ReturnType + | ReturnType + | undefined; +async function loadCreateTransformerFn(options: UnctxPluginOptions) { + if (transformer) { + return; + } + const { createTransformer } = + !options.parser || options.parser === "acorn" + ? await import("./transform/acorn") + : await import("./transform/oxc"); + transformer = createTransformer(options); +} + export const unctxPlugin = createUnplugin( (options: UnctxPluginOptions = {}) => { - const transformer = createTransformer(options); return { name: "unctx:transform", enforce: "post", transformInclude: options.transformInclude, transform: { - filter: options.transformFilter ?? transformer.filter, - handler(code, id) { - const result = transformer.transform(code); + filter: options.transformFilter, + async handler(code, id) { + await loadCreateTransformerFn(options); + const result = transformer!.transform(code); if (result) { return { code: result.code, diff --git a/src/transform/_shared.ts b/src/transform/_shared.ts new file mode 100644 index 0000000..e937bbd --- /dev/null +++ b/src/transform/_shared.ts @@ -0,0 +1,37 @@ +export interface TransformerOptions { + /** + * The function names to be transformed. + * + * @default ['withAsyncContext'] + */ + asyncFunctions?: string[]; + /** + * @default 'unctx' + */ + helperModule?: string; + /** + * @default 'executeAsync' + */ + helperName?: string; + /** + * Whether to transform properties of an object defined with a helper function. For example, + * to transform key `middleware` within the object defined with function `defineMeta`, you would pass: + * `{ defineMeta: ['middleware'] }`. + * @default {} + */ + objectDefinitions?: Record; +} + +export const kInjected = "__unctx_injected__"; + +export type MaybeHandledNode = Node & { + [kInjected]?: boolean; +}; + +export const defaultTransformerOptions = () => + ({ + asyncFunctions: ["withAsyncContext"], + helperModule: "unctx", + helperName: "executeAsync", + objectDefinitions: {}, + }) satisfies TransformerOptions; diff --git a/src/transform.ts b/src/transform/acorn.ts similarity index 87% rename from src/transform.ts rename to src/transform/acorn.ts index 298c81a..25bf7d2 100644 --- a/src/transform.ts +++ b/src/transform/acorn.ts @@ -8,32 +8,11 @@ import type { AwaitExpression, Position, } from "estree"; - -export interface TransformerOptions { - /** - * The function names to be transformed. - * - * @default ['withAsyncContext'] - */ - asyncFunctions?: string[]; - /** - * @default 'unctx' - */ - helperModule?: string; - /** - * @default 'executeAsync' - */ - helperName?: string; - /** - * Whether to transform properties of an object defined with a helper function. For example, - * to transform key `middleware` within the object defined with function `defineMeta`, you would pass: - * `{ defineMeta: ['middleware'] }`. - * @default {} - */ - objectDefinitions?: Record; -} - -const kInjected = "__unctx_injected__"; +import { + type TransformerOptions, + defaultTransformerOptions, + kInjected, +} from "./_shared.js"; type MaybeHandledNode = Node & { [kInjected]?: boolean; @@ -41,10 +20,7 @@ type MaybeHandledNode = Node & { export function createTransformer(options: TransformerOptions = {}) { options = { - asyncFunctions: ["withAsyncContext"], - helperModule: "unctx", - helperName: "executeAsync", - objectDefinitions: {}, + ...defaultTransformerOptions(), ...options, }; diff --git a/src/transform/oxc.ts b/src/transform/oxc.ts new file mode 100644 index 0000000..36aaf97 --- /dev/null +++ b/src/transform/oxc.ts @@ -0,0 +1,206 @@ +/** + * Based on transform.ts but uses oxc-parser and oxc-walker + */ +import { parseSync } from "oxc-parser"; +import MagicString from "magic-string"; +import { walk } from "oxc-walker"; +import type { + Node, + CallExpression, + BlockStatement, + AwaitExpression, +} from "oxc-parser"; +import { + defaultTransformerOptions, + kInjected, + type TransformerOptions, +} from "./_shared"; + +type MaybeHandledNode = Node & { + [kInjected]?: boolean; +}; + +export function createTransformer(options: TransformerOptions = {}) { + options = { + ...defaultTransformerOptions(), + ...options, + }; + + const objectDefinitionFunctions = Object.keys(options.objectDefinitions!); + + const matchRE = new RegExp( + `\\b(${[...options.asyncFunctions!, ...objectDefinitionFunctions].join( + "|", + )})\\(`, + ); + + function shouldTransform(code: string) { + return typeof code === "string" && matchRE.test(code); + } + + function transform(code: string, options_: { force?: false } = {}) { + if (!options_.force && !shouldTransform(code)) { + return; + } + const ast = parseSync("", code, { + sourceType: "module", + }); + + const s = new MagicString(code); + + let detected = false; + + walk(ast.program, { + enter(node: Node) { + if (node.type === "CallExpression") { + const functionName = _getFunctionName(node.callee); + if (options.asyncFunctions!.includes(functionName)) { + transformFunctionArguments(node); + if (functionName !== "callAsync") { + const lastArgument = node.arguments[node.arguments.length - 1]; + if (lastArgument && lastArgument.end) { + s.appendRight(lastArgument.end, ",1"); + } + } + } + if (objectDefinitionFunctions.includes(functionName)) { + for (const argument of node.arguments) { + if (argument.type !== "ObjectExpression") { + continue; + } + + for (const property of argument.properties) { + if ( + property.type !== "Property" || + property.key.type !== "Identifier" + ) { + continue; + } + + if ( + options.objectDefinitions![functionName]?.includes( + property.key?.name, + ) + ) { + transformFunctionBody(property.value); + } + } + } + } + } + }, + }); + + if (!detected) { + return; + } + + s.appendLeft( + 0, + `import { ${options.helperName} as __executeAsync } from "${options.helperModule}";`, + ); + + return { + code: s.toString(), + magicString: s, + }; + + function transformFunctionBody(function_: Node) { + if ( + function_.type !== "ArrowFunctionExpression" && + function_.type !== "FunctionExpression" + ) { + return; + } + + // No need to transform non-async function + if (!function_.async) { + return; + } + + const body = function_.body as BlockStatement; + + let injectVariable = false; + walk(body, { + enter( + node: MaybeHandledNode, + parent: MaybeHandledNode | undefined | null, + ) { + if (node.type === "AwaitExpression" && !node[kInjected]) { + detected = true; + injectVariable = true; + injectForNode(node, parent); + } else if ( + node.type === "IfStatement" && + node.consequent.type === "ExpressionStatement" && + node.consequent.expression.type === "AwaitExpression" + ) { + detected = true; + injectVariable = true; + (node.consequent.expression as MaybeHandledNode)[kInjected] = true; + injectForNode(node.consequent.expression, node); + } + // Skip transform for nested functions + if ( + node.type === "ArrowFunctionExpression" || + node.type === "FunctionExpression" || + node.type === "FunctionDeclaration" + ) { + return this.skip(); + } + }, + }); + + if (injectVariable && body.start) { + s.appendLeft(body.start + 1, "let __temp, __restore;"); + } + } + + function transformFunctionArguments(node: CallExpression) { + for (const function_ of node.arguments) { + transformFunctionBody(function_); + } + } + + function injectForNode( + node: AwaitExpression, + parent: Node | undefined | null, + ) { + const isStatement = parent?.type === "ExpressionStatement"; + + if (!node.start || !node.argument.start) { + return; + } + + s.remove(node.start, node.argument.start); + s.remove(node.end, node.argument.end); + + s.appendLeft( + node.argument.start, + isStatement + ? `;(([__temp,__restore]=__executeAsync(()=>` + : `(([__temp,__restore]=__executeAsync(()=>`, + ); + s.appendRight( + node.argument.end, + isStatement + ? `)),await __temp,__restore());` + : `)),__temp=await __temp,__restore(),__temp)`, + ); + } + } + + return { + transform, + shouldTransform, + }; +} + +function _getFunctionName(node: Node): string { + if (node.type === "Identifier") { + return node.name; + } else if (node.type === "MemberExpression") { + return _getFunctionName(node.property); + } + return ""; +} diff --git a/test/transform.test.ts b/test/transform.test.ts index 466b989..898586a 100644 --- a/test/transform.test.ts +++ b/test/transform.test.ts @@ -1,34 +1,42 @@ import { expect, it, describe } from "vitest"; -import { createTransformer } from "../src/transform"; +import { createTransformer as acornTransformer } from "../src/transform/acorn"; +import { createTransformer as oxcTransformer } from "../src/transform/oxc"; + +const transformers = [ + { name: "acorn", create: acornTransformer }, + { name: "oxc", create: oxcTransformer }, +] as const; describe("transforms", () => { - const transformer = createTransformer({ - asyncFunctions: ["withAsyncContext", "callAsync"], - objectDefinitions: { - defineSomething: ["someKey"], - }, - }); + for (const { name, create } of transformers) { + describe(name, () => { + const transformer = create({ + asyncFunctions: ["withAsyncContext", "callAsync"], + objectDefinitions: { + defineSomething: ["someKey"], + }, + }); - function transform(input: string) { - return transformer.transform( - // Slice 6 spaces indention for snapshot alignment - input - .split("\n") - .map((index) => index.slice(6)) - .join("\n"), - )?.code; - } + function transform(input: string) { + return transformer.transform( + // Slice 6 spaces indention for snapshot alignment + input + .split("\n") + .map((index) => index.slice(6)) + .join("\n"), + )?.code; + } - it("transforms", () => { - expect( - transform(` + it("transforms", () => { + expect( + transform(` export default withAsyncContext(async () => { const ctx1 = useSomething() await something() const ctx2 = useSomething() }) `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { executeAsync as __executeAsync } from "unctx"; export default withAsyncContext(async () => {let __temp, __restore; const ctx1 = useSomething() @@ -37,18 +45,18 @@ describe("transforms", () => { },1) " `); - }); + }); - it("transforms await as variable", () => { - expect( - transform(` + it("transforms await as variable", () => { + expect( + transform(` export default withAsyncContext(async () => { const foo = await something() const bar = hello(await something()) const ctx = useSomething() }) `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { executeAsync as __executeAsync } from "unctx"; export default withAsyncContext(async () => {let __temp, __restore; const foo = (([__temp,__restore]=__executeAsync(()=>something())),__temp=await __temp,__restore(),__temp) @@ -57,11 +65,11 @@ describe("transforms", () => { },1) " `); - }); + }); - it("transforms await in nested scopes", () => { - expect( - transform(` + it("transforms await in nested scopes", () => { + expect( + transform(` export default withAsyncContext(async () => { for (const i of foo) { if (i) { @@ -71,7 +79,7 @@ describe("transforms", () => { const ctx = useSomething() }) `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { executeAsync as __executeAsync } from "unctx"; export default withAsyncContext(async () => {let __temp, __restore; for (const i of foo) { @@ -83,11 +91,11 @@ describe("transforms", () => { },1) " `); - }); + }); - it("transforms await in try-catch", () => { - expect( - transform(` + it("transforms await in try-catch", () => { + expect( + transform(` export default withAsyncContext(async () => { let user; @@ -101,7 +109,7 @@ describe("transforms", () => { return navigateTo('/'); }) `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { executeAsync as __executeAsync } from "unctx"; export default withAsyncContext(async () => {let __temp, __restore; let user; @@ -117,18 +125,18 @@ describe("transforms", () => { },1) " `); - }); + }); - it("transforms dot usage", () => { - expect( - transform(` + it("transforms dot usage", () => { + expect( + transform(` export default ctx.callAsync(async () => { const ctx1 = useSomething() await something() const ctx2 = useSomething() }) `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { executeAsync as __executeAsync } from "unctx"; export default ctx.callAsync(async () => {let __temp, __restore; const ctx1 = useSomething() @@ -138,15 +146,15 @@ describe("transforms", () => { " `); - expect( - transform(` + expect( + transform(` export default x.ctx.callAsync(async () => { const ctx1 = useSomething() await something() const ctx2 = useSomething() }) `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { executeAsync as __executeAsync } from "unctx"; export default x.ctx.callAsync(async () => {let __temp, __restore; const ctx1 = useSomething() @@ -155,21 +163,21 @@ describe("transforms", () => { }) " `); - }); + }); - it("does not transform non async usage", () => { - expect( - transform(` + it("does not transform non async usage", () => { + expect( + transform(` export default withAsyncContext(async () => { const ctx = useSomething() }) `), - ).toBeUndefined(); - }); + ).toBeUndefined(); + }); - it("does not transform unrelated nested functions", () => { - expect( - transform(` + it("does not transform unrelated nested functions", () => { + expect( + transform(` export default withAsyncContext(async () => { async function foo() { await something() @@ -180,12 +188,12 @@ describe("transforms", () => { const ctx = useSomething() }) `), - ).toBeUndefined(); - }); + ).toBeUndefined(); + }); - it("transforms validly nested functions", () => { - expect( - transform(` + it("transforms validly nested functions", () => { + expect( + transform(` export default withAsyncContext(async () => { await something() @@ -196,7 +204,7 @@ describe("transforms", () => { const ctx = useSomething() }) `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { executeAsync as __executeAsync } from "unctx"; export default withAsyncContext(async () => {let __temp, __restore; ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); @@ -209,39 +217,39 @@ describe("transforms", () => { },1) " `); - }); + }); - it("transforms multiple awaits in same chunk", () => { - expect( - transform(` + it("transforms multiple awaits in same chunk", () => { + expect( + transform(` export default withAsyncContext(async () => { await writeConfig(await readConfig()) }) `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { executeAsync as __executeAsync } from "unctx"; export default withAsyncContext(async () => {let __temp, __restore; ;(([__temp,__restore]=__executeAsync(()=>writeConfig((([__temp,__restore]=__executeAsync(()=>readConfig())),__temp=await __temp,__restore(),__temp)))),await __temp,__restore()); },1) " `); - }); + }); - it("does not transform non target function", () => { - expect( - transform(` + it("does not transform non target function", () => { + expect( + transform(` export default someFunction(async () => { const ctx1 = useSomething() await something() const ctx2 = useSomething() }) `), - ).toBeUndefined(); - }); + ).toBeUndefined(); + }); - it("transforms certain keys of an object", () => { - expect( - transform(` + it("transforms certain keys of an object", () => { + expect( + transform(` export default defineSomething({ someKey: async () => { const ctx1 = useSomething() @@ -266,7 +274,7 @@ describe("transforms", () => { } }) `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { executeAsync as __executeAsync } from "unctx"; export default defineSomething({ someKey: async () => {let __temp, __restore; @@ -293,28 +301,30 @@ describe("transforms", () => { }) " `); - }); + }); - it("doesn't transform non-objects", () => { - expect( - transform(` + it("doesn't transform non-objects", () => { + expect( + transform(` export default defineSomething('test') `), - ).toBeUndefined(); - }); - it("Should not add a statement terminator if expression comes after if statement", () => { - expect( - transform(` + ).toBeUndefined(); + }); + it("Should not add a statement terminator if expression comes after if statement", () => { + expect( + transform(` export default withAsyncContext(async () => { if(false) await something() }) `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { executeAsync as __executeAsync } from "unctx"; export default withAsyncContext(async () => {let __temp, __restore; if(false) (([__temp,__restore]=__executeAsync(()=>something())),__temp=await __temp,__restore(),__temp) },1) " `); - }); + }); + }); + } });