AkifumiSato / esbuild-jest-transform

14 stars 9 forks source link

"Cannot use plugins in synchronous API calls" error #7

Open allomov opened 1 year ago

allomov commented 1 year ago

I am looking for a solution that will allow me to use ImportGlobPlugin. When I try to run the following code

const ImportGlobPlugin = require('esbuild-plugin-import-glob');

const config = {
  transform: {
    "^.+\\.tsx?$": ["esbuild-jest-transform", {plugins: [ImportGlobPlugin.default()]}],
    "^.+\\.jsx?$": ["esbuild-jest-transform", {plugins: [ImportGlobPlugin.default()]}]
  }
};

I get an error message error: Cannot use plugins in synchronous API calls, which makes sense taking into account that plugins work in async mode. I tried to change the implementation of the Transformer to an AsyncTransformer but got an error from jest telling me that the Transformer should implement the process function, which should be synchronous.

Here is how I tried to update it:

diff --git a/index.ts b/index.ts
index 3122cfa..e7f45de 100644
--- a/index.ts
+++ b/index.ts
@@ -2,7 +2,7 @@ import * as path from "path";

 import * as esbuild from "esbuild";
 import { builtinModules } from "module";
-import type { SyncTransformer, TransformedSource } from "@jest/transform";
+import type { AsyncTransformer, TransformedSource } from "@jest/transform";

 const pkg = require(path.resolve("package.json"));

@@ -13,24 +13,26 @@ const external = [
   ...Object.keys(pkg.peerDependencies ?? {}),
 ];

-const transformer: SyncTransformer<esbuild.BuildOptions> = {
-  process(_content, filename, { transformerConfig }) {
-    const { outputFiles } = esbuild.buildSync({
+const transformer: AsyncTransformer<esbuild.BuildOptions> = {
+  async processAsync(_sourceText, sourcePath, { transformerConfig }) {
+    const { outputFiles } = await esbuild.build({
       outdir: "./dist",
       minify: false,
       bundle: true,
       write: false,
       sourcemap: true,
       ...transformerConfig,
-      entryPoints: [filename],
+      entryPoints: [sourcePath],
       external,
     });

-    return outputFiles!.reduce((cur, item) => {
+    const result = outputFiles!.reduce((cur, item) => {
       const key = item.path.includes(".map") ? "map" : "code";
       cur[key] = Buffer.from(item.contents).toString();
       return cur;
-    }, {} as Exclude<TransformedSource, string>);
+    }, {code: "", map: ""});
+
+    return result as TransformedSource;
   },
 };

and here is an error message that I receive from jest:

    ● Invalid synchronous transformer module:
      "/Users/allomau/work/glt/GLTEngageReleases/node_modules/esbuild-jest-transform/index.js" specified in the "transform" object of Jest configuration
      must export a `process` function.
      Code Transformation Documentation:
      https://jestjs.io/docs/code-transformation

Do you have thoughts of how it can be fixed?

AkifumiSato commented 1 year ago

Can process and processAsync be specified at the same time?

allomov commented 1 year ago

@AkifumiSato I tried to do it, but all my attempts failed. It looks like the easiest way to achieve it is fixing it on jest side, but I do not hear anything from their side (here is the question)

AkifumiSato commented 1 year ago

I do not yet know if this can be solved.

robwierzbowski commented 1 year ago

I'm working on this as well. The AsyncTransformer type allows you to specify both process and processAsync, but currently I'm not having any luck triggering processAsync by enabling the async mode through the methods listed here: https://jestjs.io/docs/ecmascript-modules. Running node --experimental-vm-modules node_modules/jest/bin/jest.js is still triggering the process method in my transformer.

I'll keep hacking on it for a while, but the documentation on writing transformers is very light.

acarroll-trend commented 10 months ago

@AkifumiSato I was able to get this working asynchronously with the following code changes. I tried to keep it as backwards compatible as possible. I did move external before the options spread, because I needed to clear it on my project. Otherwise, I'd get this error.

Dynamic require of "some-package-name-in-the-file-being-tested" is not supported

I don't know if that is a configuration issue on my end or not, but it was the only way I could get it to work.

// index.ts
import * as esbuild from "esbuild";
import { builtinModules } from "module";
import * as path from "path";
import type { AsyncTransformer, TransformedSource, Transformer } from "@jest/transform";

const pkg = require(path.resolve("package.json"));

const external = [
  ...builtinModules,
  ...Object.keys(pkg.dependencies ?? {}),
  ...Object.keys(pkg.devDependencies ?? {}),
  ...Object.keys(pkg.peerDependencies ?? {}),
];

function createOptions(options: esbuild.BuildOptions, sourcePath: string): esbuild.BuildOptions {
  return {
    bundle: true,
    external,
    minify: false,
    outdir: "./dist",
    sourcemap: true,
    target: "esnext",
    write: false,
    ...options,
    entryPoints: [sourcePath],
  }
}

function transformSource(outputFiles: esbuild.OutputFile[] | undefined): TransformedSource {
  return outputFiles!.reduce((result, outputFile) => {
    const key = outputFile.path.includes(".map") ? "map" : "code";

    result[key] = Buffer.from(outputFile.contents).toString();

    return result;
  }, {} as Exclude<TransformedSource, string>);
}

const transformer: Transformer<esbuild.BuildOptions> = {
  process(_sourceText, sourcePath, { transformerConfig }) {
    const { outputFiles } = esbuild.buildSync(createOptions(transformerConfig, sourcePath));

    return transformSource(outputFiles);
  },
};

function createAsyncTransformer(plugins?: esbuild.Plugin[]): AsyncTransformer<esbuild.BuildOptions> {
  return {
    ...transformer,
    async processAsync(_sourceText, sourcePath, { transformerConfig }) {
      const options = { plugins, ...createOptions(transformerConfig, sourcePath) };
      const { outputFiles } = await esbuild.build(options);

      return transformSource(outputFiles);
    },
  }
}

module.exports = {
  createAsyncTransformer,
  default: transformer,
}

You can't specify functions in the jest config transformer config, because it strips them out. I had to create an external file like this. Otherwise, the plugin setup function would get striped, and esbuild would throw an error.

// esbuild-transform.js
import { createAsyncTransformer } from 'esbuild-jest-transform';

const transformer = createAsyncTransformer([
    {
        name: 'ignore-sass',
        setup(build) {
            build.onLoad({ filter: /\.scss$/ }, () => ({ contents: '' }));
        },
    },
]);

export default transformer;

and reference it from my jest config

/** @type { import('@jest/types').Config.InitialOptions } */
module.exports = {
    // ...
    transform: {
        '^.+\\.(js|tsx|ts)$': [
            '<rootDir>/esbuild-transform.mjs',
            {
                external: [],
                tsconfig: 'tsconfig.test.json',
            },
        ],
    },
    // ...
};