From 361a70d2963864ee006b4454b4cca5e327dc2549 Mon Sep 17 00:00:00 2001 From: Alexander Lichter Date: Thu, 14 Aug 2025 11:07:50 +0200 Subject: [PATCH 1/7] feat: add experimental oxc-based transform and plugin --- package.json | 12 ++ pnpm-lock.yaml | 249 +++++++++++++++++++++++++++++ src/plugin-oxc.ts | 29 ++++ src/transform-oxc.ts | 227 ++++++++++++++++++++++++++ test/transform-oxc.test.ts | 320 +++++++++++++++++++++++++++++++++++++ 5 files changed, 837 insertions(+) create mode 100644 src/plugin-oxc.ts create mode 100644 src/transform-oxc.ts create mode 100644 test/transform-oxc.test.ts diff --git a/package.json b/package.json index f18c926..9141f74 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,17 @@ "types": "./dist/transform.d.ts", "import": "./dist/transform.mjs" }, + "./transform-oxc": { + "types": "./dist/transform-oxc.d.ts", + "import": "./dist/transform-oxc.mjs" + }, "./plugin": { "types": "./dist/plugin.d.ts", "import": "./dist/plugin.mjs" + }, + "./plugin-oxc": { + "types": "./dist/plugin-oxc.d.ts", + "import": "./dist/plugin-oxc.mjs" } }, "main": "./dist/index.cjs", @@ -50,6 +58,10 @@ "magic-string": "^0.30.17", "unplugin": "^2.3.5" }, + "peerDependencies": { + "oxc-parser": "^0.82.1", + "oxc-walker": "^0.4.0" + }, "devDependencies": { "@types/estree": "^1.0.8", "@types/node": "^24.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e371905..f42f61f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,12 @@ importers: magic-string: specifier: ^0.30.17 version: 0.30.17 + 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.5 version: 2.3.5 @@ -86,6 +92,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.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -467,6 +482,9 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@napi-rs/wasm-runtime@1.0.3': + resolution: {integrity: sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -479,6 +497,98 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@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==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -637,6 +747,9 @@ packages: cpu: [x64] os: [win32] + '@tybys/wasm-util@0.10.0': + resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -1426,6 +1539,9 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + magic-regexp@0.10.0: + resolution: {integrity: sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -1545,6 +1661,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'} @@ -1976,10 +2101,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.39.1: resolution: {integrity: sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2156,6 +2287,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.21.5': optional: true @@ -2394,6 +2541,13 @@ 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 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2406,6 +2560,55 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@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': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -2516,6 +2719,11 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.46.2': optional: true + '@tybys/wasm-util@0.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 @@ -3430,6 +3638,16 @@ snapshots: lru-cache@10.4.3: {} + magic-regexp@0.10.0: + dependencies: + estree-walker: 3.0.3 + magic-string: 0.30.17 + mlly: 1.7.4 + regexp-tree: 0.1.27 + type-level-regexp: 0.1.17 + ufo: 1.6.1 + unplugin: 2.3.5 + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3561,6 +3779,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 @@ -3966,10 +4210,15 @@ snapshots: dependencies: typescript: 5.9.2 + 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.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2): dependencies: '@typescript-eslint/eslint-plugin': 8.39.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) diff --git a/src/plugin-oxc.ts b/src/plugin-oxc.ts new file mode 100644 index 0000000..af87d4f --- /dev/null +++ b/src/plugin-oxc.ts @@ -0,0 +1,29 @@ +import { createUnplugin } from "unplugin"; +import { createTransformer, type TransformerOptions } from "./transform-oxc"; + +export interface UnctxPluginOptions extends TransformerOptions { + transformInclude?: (id: string) => boolean; +} + +export const unctxOxcPlugin = createUnplugin( + (options: UnctxPluginOptions = {}) => { + const transformer = createTransformer(options); + return { + name: "unctx:transform", + enforce: "post", + transformInclude: options.transformInclude, + transform(code, id) { + const result = transformer.transform(code); + if (result) { + return { + code: result.code, + map: result.magicString.generateMap({ + source: id, + includeContent: true, + }), + }; + } + }, + }; + }, +); diff --git a/src/transform-oxc.ts b/src/transform-oxc.ts new file mode 100644 index 0000000..172e113 --- /dev/null +++ b/src/transform-oxc.ts @@ -0,0 +1,227 @@ +import { parseSync } from "oxc-parser"; +import MagicString from "magic-string"; +import { walk } from "oxc-walker"; +import type { + Node, + CallExpression, + BlockStatement, + AwaitExpression, +} from "oxc-parser"; + +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__"; + +type MaybeHandledNode = Node & { + [kInjected]?: boolean; +}; + +export function createTransformer(options: TransformerOptions = {}) { + options = { + asyncFunctions: ["withAsyncContext"], + helperModule: "unctx", + helperName: "executeAsync", + objectDefinitions: {}, + ...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-oxc.test.ts b/test/transform-oxc.test.ts new file mode 100644 index 0000000..7b3200f --- /dev/null +++ b/test/transform-oxc.test.ts @@ -0,0 +1,320 @@ +import { expect, it, describe } from "vitest"; +import { createTransformer } from "../src/transform-oxc"; + +describe("transforms with oxc", () => { + const transformer = createTransformer({ + 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; + } + + it("transforms", () => { + expect( + transform(` + export default withAsyncContext(async () => { + const ctx1 = useSomething() + await something() + const ctx2 = useSomething() + }) + `), + ).toMatchInlineSnapshot(` + "import { executeAsync as __executeAsync } from "unctx"; + export default withAsyncContext(async () => {let __temp, __restore; + const ctx1 = useSomething() + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + const ctx2 = useSomething() + },1) + " + `); + }); + + it("transforms await as variable", () => { + expect( + transform(` + export default withAsyncContext(async () => { + const foo = await something() + const bar = hello(await something()) + const ctx = useSomething() + }) + `), + ).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) + const bar = hello((([__temp,__restore]=__executeAsync(()=>something())),__temp=await __temp,__restore(),__temp)) + const ctx = useSomething() + },1) + " + `); + }); + + it("transforms await in nested scopes", () => { + expect( + transform(` + export default withAsyncContext(async () => { + for (const i of foo) { + if (i) { + await i() + } + } + const ctx = useSomething() + }) + `), + ).toMatchInlineSnapshot(` + "import { executeAsync as __executeAsync } from "unctx"; + export default withAsyncContext(async () => {let __temp, __restore; + for (const i of foo) { + if (i) { + ;(([__temp,__restore]=__executeAsync(()=>i())),await __temp,__restore()); + } + } + const ctx = useSomething() + },1) + " + `); + }); + + it("transforms await in try-catch", () => { + expect( + transform(` + export default withAsyncContext(async () => { + let user; + + try { + user = await fetchUser(); + } catch (e) { + user = null; + } + + if (!user) + return navigateTo('/'); + }) + `), + ).toMatchInlineSnapshot(` + "import { executeAsync as __executeAsync } from "unctx"; + export default withAsyncContext(async () => {let __temp, __restore; + let user; + + try { + user = (([__temp,__restore]=__executeAsync(()=>fetchUser())),__temp=await __temp,__restore(),__temp); + } catch (e) { + user = null; + } + + if (!user) + return navigateTo('/'); + },1) + " + `); + }); + + it("transforms dot usage", () => { + expect( + transform(` + export default ctx.callAsync(async () => { + const ctx1 = useSomething() + await something() + const ctx2 = useSomething() + }) + `), + ).toMatchInlineSnapshot(` + "import { executeAsync as __executeAsync } from "unctx"; + export default ctx.callAsync(async () => {let __temp, __restore; + const ctx1 = useSomething() + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + const ctx2 = useSomething() + }) + " + `); + + expect( + transform(` + export default x.ctx.callAsync(async () => { + const ctx1 = useSomething() + await something() + const ctx2 = useSomething() + }) + `), + ).toMatchInlineSnapshot(` + "import { executeAsync as __executeAsync } from "unctx"; + export default x.ctx.callAsync(async () => {let __temp, __restore; + const ctx1 = useSomething() + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + const ctx2 = useSomething() + }) + " + `); + }); + + it("does not transform non async usage", () => { + expect( + transform(` + export default withAsyncContext(async () => { + const ctx = useSomething() + }) + `), + ).toBeUndefined(); + }); + + it("does not transform unrelated nested functions", () => { + expect( + transform(` + export default withAsyncContext(async () => { + async function foo() { + await something() + } + const bar = async () => { + await something() + } + const ctx = useSomething() + }) + `), + ).toBeUndefined(); + }); + + it("transforms validly nested functions", () => { + expect( + transform(` + export default withAsyncContext(async () => { + await something() + + withAsyncContext(async () => { + await something() + }) + + const ctx = useSomething() + }) + `), + ).toMatchInlineSnapshot(` + "import { executeAsync as __executeAsync } from "unctx"; + export default withAsyncContext(async () => {let __temp, __restore; + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + + withAsyncContext(async () => {let __temp, __restore; + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + },1) + + const ctx = useSomething() + },1) + " + `); + }); + + it("transforms multiple awaits in same chunk", () => { + expect( + transform(` + export default withAsyncContext(async () => { + await writeConfig(await readConfig()) + }) + `), + ).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(` + export default someFunction(async () => { + const ctx1 = useSomething() + await something() + const ctx2 = useSomething() + }) + `), + ).toBeUndefined(); + }); + + it("transforms certain keys of an object", () => { + expect( + transform(` + export default defineSomething({ + someKey: async () => { + const ctx1 = useSomething() + await something() + const ctx2 = useSomething() + }, + async someKey () { + const ctx1 = useSomething() + await something() + const ctx2 = useSomething() + }, + ...someKey, + someKey: 421, + someKey () { + const ctx1 = useSomething() + const ctx2 = useSomething() + }, + async someOtherKey () { + const ctx1 = useSomething() + await something() + const ctx2 = useSomething() + } + }) + `), + ).toMatchInlineSnapshot(` + "import { executeAsync as __executeAsync } from "unctx"; + export default defineSomething({ + someKey: async () => {let __temp, __restore; + const ctx1 = useSomething() + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + const ctx2 = useSomething() + }, + async someKey () {let __temp, __restore; + const ctx1 = useSomething() + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + const ctx2 = useSomething() + }, + ...someKey, + someKey: 421, + someKey () { + const ctx1 = useSomething() + const ctx2 = useSomething() + }, + async someOtherKey () { + const ctx1 = useSomething() + await something() + const ctx2 = useSomething() + } + }) + " + `); + }); + + 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(` + export default withAsyncContext(async () => { + if(false) await something() + }) + `), + ).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) + " + `); + }); +}); From cc10e8e6f9eb65733aa494e0de07c6de3f5deadb Mon Sep 17 00:00:00 2001 From: Alexander Lichter Date: Thu, 14 Aug 2025 11:09:25 +0200 Subject: [PATCH 2/7] chore: document --- src/plugin-oxc.ts | 35 ++++++++++++++++++++++------------- src/transform-oxc.ts | 3 +++ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/plugin-oxc.ts b/src/plugin-oxc.ts index af87d4f..e3dddf6 100644 --- a/src/plugin-oxc.ts +++ b/src/plugin-oxc.ts @@ -1,28 +1,37 @@ -import { createUnplugin } from "unplugin"; +// Copy of plugin.ts but uses oxc transform +import { createUnplugin, type HookFilter } from "unplugin"; import { createTransformer, type TransformerOptions } from "./transform-oxc"; export interface UnctxPluginOptions extends TransformerOptions { + /** Plugin Hook Filter for the transform hook + * @see https://unplugin.unjs.io/guide/#filters + */ + transformFilter?: HookFilter; + /** Function to determine whether a file should be transformed. If possible, use `transformFilter` instead for better performance. */ transformInclude?: (id: string) => boolean; } -export const unctxOxcPlugin = createUnplugin( +export const unctxPlugin = createUnplugin( (options: UnctxPluginOptions = {}) => { const transformer = createTransformer(options); return { name: "unctx:transform", enforce: "post", transformInclude: options.transformInclude, - transform(code, id) { - const result = transformer.transform(code); - if (result) { - return { - code: result.code, - map: result.magicString.generateMap({ - source: id, - includeContent: true, - }), - }; - } + transform: { + filter: options.transformFilter, + handler(code, id) { + const result = transformer.transform(code); + if (result) { + return { + code: result.code, + map: result.magicString.generateMap({ + source: id, + includeContent: true, + }), + }; + } + }, }, }; }, diff --git a/src/transform-oxc.ts b/src/transform-oxc.ts index 172e113..57e6ea6 100644 --- a/src/transform-oxc.ts +++ b/src/transform-oxc.ts @@ -1,3 +1,6 @@ +/** + * 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"; From 1a3f666d2f7c84a2eb2d02774c998826f06b6314 Mon Sep 17 00:00:00 2001 From: Alexander Lichter Date: Thu, 14 Aug 2025 13:42:56 +0200 Subject: [PATCH 3/7] chore: refactor and use normal snapshots --- src/plugin-oxc.ts | 38 --- src/plugin.ts | 27 +- src/{transform.ts => transform/acorn.ts} | 36 +-- src/transform/index.ts | 36 +++ src/{transform-oxc.ts => transform/oxc.ts} | 36 +-- test/__snapshots__/transform.test.ts.snap | 129 +++++++++ test/transform-oxc.test.ts | 320 --------------------- test/transform.test.ts | 130 +-------- 8 files changed, 211 insertions(+), 541 deletions(-) delete mode 100644 src/plugin-oxc.ts rename src/{transform.ts => transform/acorn.ts} (87%) create mode 100644 src/transform/index.ts rename src/{transform-oxc.ts => transform/oxc.ts} (87%) create mode 100644 test/__snapshots__/transform.test.ts.snap delete mode 100644 test/transform-oxc.test.ts diff --git a/src/plugin-oxc.ts b/src/plugin-oxc.ts deleted file mode 100644 index e3dddf6..0000000 --- a/src/plugin-oxc.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copy of plugin.ts but uses oxc transform -import { createUnplugin, type HookFilter } from "unplugin"; -import { createTransformer, type TransformerOptions } from "./transform-oxc"; - -export interface UnctxPluginOptions extends TransformerOptions { - /** Plugin Hook Filter for the transform hook - * @see https://unplugin.unjs.io/guide/#filters - */ - transformFilter?: HookFilter; - /** Function to determine whether a file should be transformed. If possible, use `transformFilter` instead for better performance. */ - transformInclude?: (id: string) => boolean; -} - -export const unctxPlugin = createUnplugin( - (options: UnctxPluginOptions = {}) => { - const transformer = createTransformer(options); - return { - name: "unctx:transform", - enforce: "post", - transformInclude: options.transformInclude, - transform: { - filter: options.transformFilter, - handler(code, id) { - const result = transformer.transform(code); - if (result) { - return { - code: result.code, - map: result.magicString.generateMap({ - source: id, - includeContent: true, - }), - }; - } - }, - }, - }; - }, -); diff --git a/src/plugin.ts b/src/plugin.ts index 4a1d0af..6dfafc9 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/index.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, - handler(code, id) { - const result = transformer.transform(code); + async handler(code, id) { + await loadCreateTransformerFn(options); + const result = transformer!.transform(code); if (result) { return { code: result.code, 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 8555a8a..9c66e3c 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 "./index.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/index.ts b/src/transform/index.ts new file mode 100644 index 0000000..91c8847 --- /dev/null +++ b/src/transform/index.ts @@ -0,0 +1,36 @@ +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: TransformerOptions = { + asyncFunctions: ["withAsyncContext"], + helperModule: "unctx", + helperName: "executeAsync", + objectDefinitions: {}, +}; diff --git a/src/transform-oxc.ts b/src/transform/oxc.ts similarity index 87% rename from src/transform-oxc.ts rename to src/transform/oxc.ts index 57e6ea6..f4f720a 100644 --- a/src/transform-oxc.ts +++ b/src/transform/oxc.ts @@ -10,32 +10,11 @@ import type { BlockStatement, AwaitExpression, } from "oxc-parser"; - -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 { + defaultTransformerOptions, + kInjected, + type TransformerOptions, +} from "./index.js"; type MaybeHandledNode = Node & { [kInjected]?: boolean; @@ -43,10 +22,7 @@ type MaybeHandledNode = Node & { export function createTransformer(options: TransformerOptions = {}) { options = { - asyncFunctions: ["withAsyncContext"], - helperModule: "unctx", - helperName: "executeAsync", - objectDefinitions: {}, + ...defaultTransformerOptions, ...options, }; diff --git a/test/__snapshots__/transform.test.ts.snap b/test/__snapshots__/transform.test.ts.snap new file mode 100644 index 0000000..f019e67 --- /dev/null +++ b/test/__snapshots__/transform.test.ts.snap @@ -0,0 +1,129 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`transforms > Should not add a statement terminator if expression comes after if statement 1`] = ` +"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) +" +`; + +exports[`transforms > transforms 1`] = ` +"import { executeAsync as __executeAsync } from "unctx"; +export default withAsyncContext(async () => {let __temp, __restore; + const ctx1 = useSomething() + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + const ctx2 = useSomething() +},1) +" +`; + +exports[`transforms > transforms await as variable 1`] = ` +"import { executeAsync as __executeAsync } from "unctx"; +export default withAsyncContext(async () => {let __temp, __restore; + const foo = (([__temp,__restore]=__executeAsync(()=>something())),__temp=await __temp,__restore(),__temp) + const bar = hello((([__temp,__restore]=__executeAsync(()=>something())),__temp=await __temp,__restore(),__temp)) + const ctx = useSomething() +},1) +" +`; + +exports[`transforms > transforms await in nested scopes 1`] = ` +"import { executeAsync as __executeAsync } from "unctx"; +export default withAsyncContext(async () => {let __temp, __restore; + for (const i of foo) { + if (i) { + ;(([__temp,__restore]=__executeAsync(()=>i())),await __temp,__restore()); + } + } + const ctx = useSomething() +},1) +" +`; + +exports[`transforms > transforms await in try-catch 1`] = ` +"import { executeAsync as __executeAsync } from "unctx"; +export default withAsyncContext(async () => {let __temp, __restore; + let user; + + try { + user = (([__temp,__restore]=__executeAsync(()=>fetchUser())),__temp=await __temp,__restore(),__temp); + } catch (e) { + user = null; + } + + if (!user) + return navigateTo('/'); +},1) +" +`; + +exports[`transforms > transforms certain keys of an object 1`] = ` +"import { executeAsync as __executeAsync } from "unctx"; +export default defineSomething({ + someKey: async () => {let __temp, __restore; + const ctx1 = useSomething() + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + const ctx2 = useSomething() + }, + async someKey () {let __temp, __restore; + const ctx1 = useSomething() + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + const ctx2 = useSomething() + }, + ...someKey, + someKey: 421, + someKey () { + const ctx1 = useSomething() + const ctx2 = useSomething() + }, + async someOtherKey () { + const ctx1 = useSomething() + await something() + const ctx2 = useSomething() + } +}) +" +`; + +exports[`transforms > transforms dot usage 1`] = ` +"import { executeAsync as __executeAsync } from "unctx"; +export default ctx.callAsync(async () => {let __temp, __restore; + const ctx1 = useSomething() + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + const ctx2 = useSomething() +}) +" +`; + +exports[`transforms > transforms dot usage 2`] = ` +"import { executeAsync as __executeAsync } from "unctx"; +export default x.ctx.callAsync(async () => {let __temp, __restore; + const ctx1 = useSomething() + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + const ctx2 = useSomething() +}) +" +`; + +exports[`transforms > transforms multiple awaits in same chunk 1`] = ` +"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) +" +`; + +exports[`transforms > transforms validly nested functions 1`] = ` +"import { executeAsync as __executeAsync } from "unctx"; +export default withAsyncContext(async () => {let __temp, __restore; + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + + withAsyncContext(async () => {let __temp, __restore; + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + },1) + + const ctx = useSomething() +},1) +" +`; diff --git a/test/transform-oxc.test.ts b/test/transform-oxc.test.ts deleted file mode 100644 index 7b3200f..0000000 --- a/test/transform-oxc.test.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { expect, it, describe } from "vitest"; -import { createTransformer } from "../src/transform-oxc"; - -describe("transforms with oxc", () => { - const transformer = createTransformer({ - 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; - } - - it("transforms", () => { - expect( - transform(` - export default withAsyncContext(async () => { - const ctx1 = useSomething() - await something() - const ctx2 = useSomething() - }) - `), - ).toMatchInlineSnapshot(` - "import { executeAsync as __executeAsync } from "unctx"; - export default withAsyncContext(async () => {let __temp, __restore; - const ctx1 = useSomething() - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - const ctx2 = useSomething() - },1) - " - `); - }); - - it("transforms await as variable", () => { - expect( - transform(` - export default withAsyncContext(async () => { - const foo = await something() - const bar = hello(await something()) - const ctx = useSomething() - }) - `), - ).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) - const bar = hello((([__temp,__restore]=__executeAsync(()=>something())),__temp=await __temp,__restore(),__temp)) - const ctx = useSomething() - },1) - " - `); - }); - - it("transforms await in nested scopes", () => { - expect( - transform(` - export default withAsyncContext(async () => { - for (const i of foo) { - if (i) { - await i() - } - } - const ctx = useSomething() - }) - `), - ).toMatchInlineSnapshot(` - "import { executeAsync as __executeAsync } from "unctx"; - export default withAsyncContext(async () => {let __temp, __restore; - for (const i of foo) { - if (i) { - ;(([__temp,__restore]=__executeAsync(()=>i())),await __temp,__restore()); - } - } - const ctx = useSomething() - },1) - " - `); - }); - - it("transforms await in try-catch", () => { - expect( - transform(` - export default withAsyncContext(async () => { - let user; - - try { - user = await fetchUser(); - } catch (e) { - user = null; - } - - if (!user) - return navigateTo('/'); - }) - `), - ).toMatchInlineSnapshot(` - "import { executeAsync as __executeAsync } from "unctx"; - export default withAsyncContext(async () => {let __temp, __restore; - let user; - - try { - user = (([__temp,__restore]=__executeAsync(()=>fetchUser())),__temp=await __temp,__restore(),__temp); - } catch (e) { - user = null; - } - - if (!user) - return navigateTo('/'); - },1) - " - `); - }); - - it("transforms dot usage", () => { - expect( - transform(` - export default ctx.callAsync(async () => { - const ctx1 = useSomething() - await something() - const ctx2 = useSomething() - }) - `), - ).toMatchInlineSnapshot(` - "import { executeAsync as __executeAsync } from "unctx"; - export default ctx.callAsync(async () => {let __temp, __restore; - const ctx1 = useSomething() - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - const ctx2 = useSomething() - }) - " - `); - - expect( - transform(` - export default x.ctx.callAsync(async () => { - const ctx1 = useSomething() - await something() - const ctx2 = useSomething() - }) - `), - ).toMatchInlineSnapshot(` - "import { executeAsync as __executeAsync } from "unctx"; - export default x.ctx.callAsync(async () => {let __temp, __restore; - const ctx1 = useSomething() - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - const ctx2 = useSomething() - }) - " - `); - }); - - it("does not transform non async usage", () => { - expect( - transform(` - export default withAsyncContext(async () => { - const ctx = useSomething() - }) - `), - ).toBeUndefined(); - }); - - it("does not transform unrelated nested functions", () => { - expect( - transform(` - export default withAsyncContext(async () => { - async function foo() { - await something() - } - const bar = async () => { - await something() - } - const ctx = useSomething() - }) - `), - ).toBeUndefined(); - }); - - it("transforms validly nested functions", () => { - expect( - transform(` - export default withAsyncContext(async () => { - await something() - - withAsyncContext(async () => { - await something() - }) - - const ctx = useSomething() - }) - `), - ).toMatchInlineSnapshot(` - "import { executeAsync as __executeAsync } from "unctx"; - export default withAsyncContext(async () => {let __temp, __restore; - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - - withAsyncContext(async () => {let __temp, __restore; - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - },1) - - const ctx = useSomething() - },1) - " - `); - }); - - it("transforms multiple awaits in same chunk", () => { - expect( - transform(` - export default withAsyncContext(async () => { - await writeConfig(await readConfig()) - }) - `), - ).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(` - export default someFunction(async () => { - const ctx1 = useSomething() - await something() - const ctx2 = useSomething() - }) - `), - ).toBeUndefined(); - }); - - it("transforms certain keys of an object", () => { - expect( - transform(` - export default defineSomething({ - someKey: async () => { - const ctx1 = useSomething() - await something() - const ctx2 = useSomething() - }, - async someKey () { - const ctx1 = useSomething() - await something() - const ctx2 = useSomething() - }, - ...someKey, - someKey: 421, - someKey () { - const ctx1 = useSomething() - const ctx2 = useSomething() - }, - async someOtherKey () { - const ctx1 = useSomething() - await something() - const ctx2 = useSomething() - } - }) - `), - ).toMatchInlineSnapshot(` - "import { executeAsync as __executeAsync } from "unctx"; - export default defineSomething({ - someKey: async () => {let __temp, __restore; - const ctx1 = useSomething() - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - const ctx2 = useSomething() - }, - async someKey () {let __temp, __restore; - const ctx1 = useSomething() - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - const ctx2 = useSomething() - }, - ...someKey, - someKey: 421, - someKey () { - const ctx1 = useSomething() - const ctx2 = useSomething() - }, - async someOtherKey () { - const ctx1 = useSomething() - await something() - const ctx2 = useSomething() - } - }) - " - `); - }); - - 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(` - export default withAsyncContext(async () => { - if(false) await something() - }) - `), - ).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) - " - `); - }); -}); diff --git a/test/transform.test.ts b/test/transform.test.ts index 466b989..d25a633 100644 --- a/test/transform.test.ts +++ b/test/transform.test.ts @@ -1,5 +1,5 @@ import { expect, it, describe } from "vitest"; -import { createTransformer } from "../src/transform"; +import { createTransformer } from "../src/transform/acorn"; describe("transforms", () => { const transformer = createTransformer({ @@ -28,15 +28,7 @@ describe("transforms", () => { const ctx2 = useSomething() }) `), - ).toMatchInlineSnapshot(` - "import { executeAsync as __executeAsync } from "unctx"; - export default withAsyncContext(async () => {let __temp, __restore; - const ctx1 = useSomething() - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - const ctx2 = useSomething() - },1) - " - `); + ).toMatchSnapshot(); }); it("transforms await as variable", () => { @@ -48,15 +40,7 @@ describe("transforms", () => { const ctx = useSomething() }) `), - ).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) - const bar = hello((([__temp,__restore]=__executeAsync(()=>something())),__temp=await __temp,__restore(),__temp)) - const ctx = useSomething() - },1) - " - `); + ).toMatchSnapshot(); }); it("transforms await in nested scopes", () => { @@ -71,18 +55,7 @@ describe("transforms", () => { const ctx = useSomething() }) `), - ).toMatchInlineSnapshot(` - "import { executeAsync as __executeAsync } from "unctx"; - export default withAsyncContext(async () => {let __temp, __restore; - for (const i of foo) { - if (i) { - ;(([__temp,__restore]=__executeAsync(()=>i())),await __temp,__restore()); - } - } - const ctx = useSomething() - },1) - " - `); + ).toMatchSnapshot(); }); it("transforms await in try-catch", () => { @@ -101,22 +74,7 @@ describe("transforms", () => { return navigateTo('/'); }) `), - ).toMatchInlineSnapshot(` - "import { executeAsync as __executeAsync } from "unctx"; - export default withAsyncContext(async () => {let __temp, __restore; - let user; - - try { - user = (([__temp,__restore]=__executeAsync(()=>fetchUser())),__temp=await __temp,__restore(),__temp); - } catch (e) { - user = null; - } - - if (!user) - return navigateTo('/'); - },1) - " - `); + ).toMatchSnapshot(); }); it("transforms dot usage", () => { @@ -128,15 +86,7 @@ describe("transforms", () => { const ctx2 = useSomething() }) `), - ).toMatchInlineSnapshot(` - "import { executeAsync as __executeAsync } from "unctx"; - export default ctx.callAsync(async () => {let __temp, __restore; - const ctx1 = useSomething() - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - const ctx2 = useSomething() - }) - " - `); + ).toMatchSnapshot(); expect( transform(` @@ -146,15 +96,7 @@ describe("transforms", () => { const ctx2 = useSomething() }) `), - ).toMatchInlineSnapshot(` - "import { executeAsync as __executeAsync } from "unctx"; - export default x.ctx.callAsync(async () => {let __temp, __restore; - const ctx1 = useSomething() - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - const ctx2 = useSomething() - }) - " - `); + ).toMatchSnapshot(); }); it("does not transform non async usage", () => { @@ -196,19 +138,7 @@ describe("transforms", () => { const ctx = useSomething() }) `), - ).toMatchInlineSnapshot(` - "import { executeAsync as __executeAsync } from "unctx"; - export default withAsyncContext(async () => {let __temp, __restore; - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - - withAsyncContext(async () => {let __temp, __restore; - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - },1) - - const ctx = useSomething() - },1) - " - `); + ).toMatchSnapshot(); }); it("transforms multiple awaits in same chunk", () => { @@ -218,13 +148,7 @@ describe("transforms", () => { await writeConfig(await readConfig()) }) `), - ).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) - " - `); + ).toMatchSnapshot(); }); it("does not transform non target function", () => { @@ -266,33 +190,7 @@ describe("transforms", () => { } }) `), - ).toMatchInlineSnapshot(` - "import { executeAsync as __executeAsync } from "unctx"; - export default defineSomething({ - someKey: async () => {let __temp, __restore; - const ctx1 = useSomething() - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - const ctx2 = useSomething() - }, - async someKey () {let __temp, __restore; - const ctx1 = useSomething() - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - const ctx2 = useSomething() - }, - ...someKey, - someKey: 421, - someKey () { - const ctx1 = useSomething() - const ctx2 = useSomething() - }, - async someOtherKey () { - const ctx1 = useSomething() - await something() - const ctx2 = useSomething() - } - }) - " - `); + ).toMatchSnapshot(); }); it("doesn't transform non-objects", () => { @@ -309,12 +207,6 @@ describe("transforms", () => { if(false) await something() }) `), - ).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) - " - `); + ).toMatchSnapshot(); }); }); From 14580ce9680de8743eff712cbfcf062b420c7b63 Mon Sep 17 00:00:00 2001 From: Alexander Lichter Date: Thu, 14 Aug 2025 17:13:56 +0200 Subject: [PATCH 4/7] chore: fix build --- package.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 9141f74..e65dea5 100644 --- a/package.json +++ b/package.json @@ -12,20 +12,20 @@ "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-oxc": { - "types": "./dist/transform-oxc.d.ts", - "import": "./dist/transform-oxc.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", "import": "./dist/plugin.mjs" - }, - "./plugin-oxc": { - "types": "./dist/plugin-oxc.d.ts", - "import": "./dist/plugin-oxc.mjs" } }, "main": "./dist/index.cjs", From b283971da5fb6de8b1d58a840f5c9dd1ca83504f Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 16 Dec 2025 21:24:00 +0100 Subject: [PATCH 5/7] clone defaults --- src/plugin.ts | 2 +- src/transform/{index.ts => _shared.ts} | 13 +++++++------ src/transform/acorn.ts | 4 ++-- src/transform/oxc.ts | 4 ++-- 4 files changed, 12 insertions(+), 11 deletions(-) rename src/transform/{index.ts => _shared.ts} (77%) diff --git a/src/plugin.ts b/src/plugin.ts index 6dfafc9..aff4618 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,5 +1,5 @@ import { createUnplugin, type HookFilter } from "unplugin"; -import { type TransformerOptions } from "./transform/index.js"; +import { type TransformerOptions } from "./transform/_shared.js"; export interface UnctxPluginOptions extends TransformerOptions { /** The parser to use. diff --git a/src/transform/index.ts b/src/transform/_shared.ts similarity index 77% rename from src/transform/index.ts rename to src/transform/_shared.ts index 91c8847..e937bbd 100644 --- a/src/transform/index.ts +++ b/src/transform/_shared.ts @@ -28,9 +28,10 @@ export type MaybeHandledNode = Node & { [kInjected]?: boolean; }; -export const defaultTransformerOptions: TransformerOptions = { - asyncFunctions: ["withAsyncContext"], - helperModule: "unctx", - helperName: "executeAsync", - objectDefinitions: {}, -}; +export const defaultTransformerOptions = () => + ({ + asyncFunctions: ["withAsyncContext"], + helperModule: "unctx", + helperName: "executeAsync", + objectDefinitions: {}, + }) satisfies TransformerOptions; diff --git a/src/transform/acorn.ts b/src/transform/acorn.ts index d267dcc..25bf7d2 100644 --- a/src/transform/acorn.ts +++ b/src/transform/acorn.ts @@ -12,7 +12,7 @@ import { type TransformerOptions, defaultTransformerOptions, kInjected, -} from "./index.js"; +} from "./_shared.js"; type MaybeHandledNode = Node & { [kInjected]?: boolean; @@ -20,7 +20,7 @@ type MaybeHandledNode = Node & { export function createTransformer(options: TransformerOptions = {}) { options = { - ...defaultTransformerOptions, + ...defaultTransformerOptions(), ...options, }; diff --git a/src/transform/oxc.ts b/src/transform/oxc.ts index f4f720a..36aaf97 100644 --- a/src/transform/oxc.ts +++ b/src/transform/oxc.ts @@ -14,7 +14,7 @@ import { defaultTransformerOptions, kInjected, type TransformerOptions, -} from "./index.js"; +} from "./_shared"; type MaybeHandledNode = Node & { [kInjected]?: boolean; @@ -22,7 +22,7 @@ type MaybeHandledNode = Node & { export function createTransformer(options: TransformerOptions = {}) { options = { - ...defaultTransformerOptions, + ...defaultTransformerOptions(), ...options, }; From c6581e3e8f9d70d8c8e3b001633c63f9eaa2093c Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 16 Dec 2025 21:24:45 +0100 Subject: [PATCH 6/7] keep inline snapshot to reduce diff --- test/__snapshots__/transform.test.ts.snap | 129 ---------------------- test/transform.test.ts | 128 +++++++++++++++++++-- 2 files changed, 118 insertions(+), 139 deletions(-) delete mode 100644 test/__snapshots__/transform.test.ts.snap diff --git a/test/__snapshots__/transform.test.ts.snap b/test/__snapshots__/transform.test.ts.snap deleted file mode 100644 index f019e67..0000000 --- a/test/__snapshots__/transform.test.ts.snap +++ /dev/null @@ -1,129 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`transforms > Should not add a statement terminator if expression comes after if statement 1`] = ` -"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) -" -`; - -exports[`transforms > transforms 1`] = ` -"import { executeAsync as __executeAsync } from "unctx"; -export default withAsyncContext(async () => {let __temp, __restore; - const ctx1 = useSomething() - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - const ctx2 = useSomething() -},1) -" -`; - -exports[`transforms > transforms await as variable 1`] = ` -"import { executeAsync as __executeAsync } from "unctx"; -export default withAsyncContext(async () => {let __temp, __restore; - const foo = (([__temp,__restore]=__executeAsync(()=>something())),__temp=await __temp,__restore(),__temp) - const bar = hello((([__temp,__restore]=__executeAsync(()=>something())),__temp=await __temp,__restore(),__temp)) - const ctx = useSomething() -},1) -" -`; - -exports[`transforms > transforms await in nested scopes 1`] = ` -"import { executeAsync as __executeAsync } from "unctx"; -export default withAsyncContext(async () => {let __temp, __restore; - for (const i of foo) { - if (i) { - ;(([__temp,__restore]=__executeAsync(()=>i())),await __temp,__restore()); - } - } - const ctx = useSomething() -},1) -" -`; - -exports[`transforms > transforms await in try-catch 1`] = ` -"import { executeAsync as __executeAsync } from "unctx"; -export default withAsyncContext(async () => {let __temp, __restore; - let user; - - try { - user = (([__temp,__restore]=__executeAsync(()=>fetchUser())),__temp=await __temp,__restore(),__temp); - } catch (e) { - user = null; - } - - if (!user) - return navigateTo('/'); -},1) -" -`; - -exports[`transforms > transforms certain keys of an object 1`] = ` -"import { executeAsync as __executeAsync } from "unctx"; -export default defineSomething({ - someKey: async () => {let __temp, __restore; - const ctx1 = useSomething() - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - const ctx2 = useSomething() - }, - async someKey () {let __temp, __restore; - const ctx1 = useSomething() - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - const ctx2 = useSomething() - }, - ...someKey, - someKey: 421, - someKey () { - const ctx1 = useSomething() - const ctx2 = useSomething() - }, - async someOtherKey () { - const ctx1 = useSomething() - await something() - const ctx2 = useSomething() - } -}) -" -`; - -exports[`transforms > transforms dot usage 1`] = ` -"import { executeAsync as __executeAsync } from "unctx"; -export default ctx.callAsync(async () => {let __temp, __restore; - const ctx1 = useSomething() - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - const ctx2 = useSomething() -}) -" -`; - -exports[`transforms > transforms dot usage 2`] = ` -"import { executeAsync as __executeAsync } from "unctx"; -export default x.ctx.callAsync(async () => {let __temp, __restore; - const ctx1 = useSomething() - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - const ctx2 = useSomething() -}) -" -`; - -exports[`transforms > transforms multiple awaits in same chunk 1`] = ` -"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) -" -`; - -exports[`transforms > transforms validly nested functions 1`] = ` -"import { executeAsync as __executeAsync } from "unctx"; -export default withAsyncContext(async () => {let __temp, __restore; - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - - withAsyncContext(async () => {let __temp, __restore; - ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); - },1) - - const ctx = useSomething() -},1) -" -`; diff --git a/test/transform.test.ts b/test/transform.test.ts index d25a633..edb7cb2 100644 --- a/test/transform.test.ts +++ b/test/transform.test.ts @@ -28,7 +28,15 @@ describe("transforms", () => { const ctx2 = useSomething() }) `), - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(` + "import { executeAsync as __executeAsync } from "unctx"; + export default withAsyncContext(async () => {let __temp, __restore; + const ctx1 = useSomething() + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + const ctx2 = useSomething() + },1) + " + `); }); it("transforms await as variable", () => { @@ -40,7 +48,15 @@ describe("transforms", () => { const ctx = useSomething() }) `), - ).toMatchSnapshot(); + ).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) + const bar = hello((([__temp,__restore]=__executeAsync(()=>something())),__temp=await __temp,__restore(),__temp)) + const ctx = useSomething() + },1) + " + `); }); it("transforms await in nested scopes", () => { @@ -55,7 +71,18 @@ describe("transforms", () => { const ctx = useSomething() }) `), - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(` + "import { executeAsync as __executeAsync } from "unctx"; + export default withAsyncContext(async () => {let __temp, __restore; + for (const i of foo) { + if (i) { + ;(([__temp,__restore]=__executeAsync(()=>i())),await __temp,__restore()); + } + } + const ctx = useSomething() + },1) + " + `); }); it("transforms await in try-catch", () => { @@ -74,7 +101,22 @@ describe("transforms", () => { return navigateTo('/'); }) `), - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(` + "import { executeAsync as __executeAsync } from "unctx"; + export default withAsyncContext(async () => {let __temp, __restore; + let user; + + try { + user = (([__temp,__restore]=__executeAsync(()=>fetchUser())),__temp=await __temp,__restore(),__temp); + } catch (e) { + user = null; + } + + if (!user) + return navigateTo('/'); + },1) + " + `); }); it("transforms dot usage", () => { @@ -86,7 +128,15 @@ describe("transforms", () => { const ctx2 = useSomething() }) `), - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(` + "import { executeAsync as __executeAsync } from "unctx"; + export default ctx.callAsync(async () => {let __temp, __restore; + const ctx1 = useSomething() + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + const ctx2 = useSomething() + }) + " + `); expect( transform(` @@ -96,7 +146,15 @@ describe("transforms", () => { const ctx2 = useSomething() }) `), - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(` + "import { executeAsync as __executeAsync } from "unctx"; + export default x.ctx.callAsync(async () => {let __temp, __restore; + const ctx1 = useSomething() + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + const ctx2 = useSomething() + }) + " + `); }); it("does not transform non async usage", () => { @@ -138,7 +196,19 @@ describe("transforms", () => { const ctx = useSomething() }) `), - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(` + "import { executeAsync as __executeAsync } from "unctx"; + export default withAsyncContext(async () => {let __temp, __restore; + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + + withAsyncContext(async () => {let __temp, __restore; + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + },1) + + const ctx = useSomething() + },1) + " + `); }); it("transforms multiple awaits in same chunk", () => { @@ -148,7 +218,13 @@ describe("transforms", () => { await writeConfig(await readConfig()) }) `), - ).toMatchSnapshot(); + ).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", () => { @@ -190,7 +266,33 @@ describe("transforms", () => { } }) `), - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(` + "import { executeAsync as __executeAsync } from "unctx"; + export default defineSomething({ + someKey: async () => {let __temp, __restore; + const ctx1 = useSomething() + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + const ctx2 = useSomething() + }, + async someKey () {let __temp, __restore; + const ctx1 = useSomething() + ;(([__temp,__restore]=__executeAsync(()=>something())),await __temp,__restore()); + const ctx2 = useSomething() + }, + ...someKey, + someKey: 421, + someKey () { + const ctx1 = useSomething() + const ctx2 = useSomething() + }, + async someOtherKey () { + const ctx1 = useSomething() + await something() + const ctx2 = useSomething() + } + }) + " + `); }); it("doesn't transform non-objects", () => { @@ -207,6 +309,12 @@ describe("transforms", () => { if(false) await something() }) `), - ).toMatchSnapshot(); + ).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) + " + `); }); }); From 03dbba0edbc0437f437ad09ceb2dfbfc5f201b2b Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 16 Dec 2025 21:30:17 +0100 Subject: [PATCH 7/7] run tests against both --- test/transform.test.ts | 178 ++++++++++++++++++++++------------------- 1 file changed, 94 insertions(+), 84 deletions(-) diff --git a/test/transform.test.ts b/test/transform.test.ts index edb7cb2..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/acorn"; +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) " `); - }); + }); + }); + } });