Next.js + TypeScript + AWS AmplifyのプロジェクトでJest実行時に「SyntaxError: Unexpected token 'export'」が発生する

Next.js と AWS Amplify を使ったプロジェクトで、TypeScriptのファイルに対してJestのテストを書いたら、 SyntaxError: Unexpected token 'export' エラーが発生。

このエラー自体はよくあるやつだが、設定を大きくいじらず解決する方法を調べるのに結構時間がかかったので、対処法をメモ。

環境

名前 バージョン
Node.js 16.15.0
Next.js 12.1.5
TypeScript 4.6.3
Jest 28.1.0
aws-amplify*1 4.3.24
Amplify CLI 8.0.2
Yarn 1.22.15

環境についての備考

Amplifyは、記事作成時点ではNext.js v12をサポートしていない。

Support Next.js 12 · Issue #2343 · aws-amplify/amplify-hosting · GitHub

middlewareが動かないなどの問題がある模様。

NextJs 12 / Middleware feature is not working · Issue #2353 · aws-amplify/amplify-hosting · GitHub

事前設定

Next.jsのドキュメントに沿って、JestおよびReact Testing Libraryを導入。

依存パッケージを yarn add -D jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom で追加し、 jest.config.js をプロジェクトルート直下に作成する。

ほぼドキュメント通りだが、TypeScriptのbaseUrlを src/ に変更しているため、 moduleDirectories だけ変更している。

const nextJest = require('next/jest')

const createJestConfig = nextJest({ dir: './' })

const customJestConfig = {
  moduleDirectories: ['node_modules', '<rootDir>/src/'],
  testEnvironment: 'jest-environment-jsdom',
}

module.exports = createJestConfig(customJestConfig)

なお、 jest-environment-jsdom は、Jest v28より同梱されなくなった模様。明示的に追加しないと、テスト実行時に以下のエラーが発生する。

● Validation Error:

  Test environment jest-environment-jsdom cannot be found. Make sure the testEnv
ironment configuration option points to an existing node module.

  Configuration Documentation:
  https://jestjs.io/docs/configuration


As of Jest 28 "jest-environment-jsdom" is no longer shipped by default, make sur
e to install it separately.

問題

Amplify関連のモジュールをimportしたファイルを、テストファイルから読み込むと、 SyntaxError: Unexpected token 'export' エラーが発生する。内部で利用しているAWS SDKが原因の模様。

● Test suite failed to run

    Jest encountered an unexpected token

    Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

    Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.

    By default "node_modules" folder is ignored by transformers.

    Here's what you can do:
     • If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.
     • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript
     • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
     • If you need a custom transformation specify a "transform" option in your config.
     • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.

    You'll find more details and examples of these config options in the docs:
    https://jestjs.io/docs/configuration
    For information about custom transformations, see:
    https://jestjs.io/docs/code-transformation

    Details:

    <rootDir>\node_modules\@aws-sdk\client-location\node_modules\uuid\dist\esm-browser\index.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){export { default as v1 } from './v1.js';
                                                                                      ^^^^^^

    SyntaxError: Unexpected token 'export'

      at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1773:14)
      at Object.<anonymous> (node_modules/@aws-sdk/client-location/node_modules/@aws-sdk/middleware-retry/dist-cjs/StandardRetryStrategy.js:6:16)

対応

これ自体はよくあるやつで、Nodeモジュール内にCommonJSとESM(ECMAScript modules)が混在している模様。

エラーメッセージに含まれる通り To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config. してやればよい。

nextJest では transformIgnorePatterns に何が設定されているかと思い、ソースを確認。トランスパイルされたJavaScriptは、 node_modules/next/dist/build/jest/jest.js に出力されている。

Next.js v12.1.5では、 transformIgnorePatterns には固定値で /node_modules/ が指定されており、引数で渡したJest設定は追記されるようになっている。

        transformIgnorePatterns: [
          // To match Next.js behavior node_modules is not transformed
          '/node_modules/',
          // CSS modules are mocked so they don't need to be transformed
          '^.+\\.module\\.(css|sass|scss)$',

          // Custom config can append to transformIgnorePatterns but not modify it
          // This is to ensure `node_modules` and .module.css/sass/scss are always excluded
          ...(resolvedJestConfig.transformIgnorePatterns || []),
        ],

今回の場合、エラーが出ているパッケージはトランスパイルの対象にしたいので、 /node_modules/ を上書きしたい。

どうにかできないかとリポジトリを検索してみると、同じ内容のissueがあった。

github.com

変更可能とするプルリクエストもあったが、コメントを見ると現状のままにしておくためCloseされている。ただ、issue#35634のコメントや、PR#35635からリンクされたdiscussion#31152のコメントに対応方法が記載されていた。

単純に、 createJestConfig の結果をスプレッド構文で別オブジェクトに展開し、そこで transformIgnorePatterns を上書きしている。

以下のように jext.config.js を変更。 @aws-sdk/client-location 以外にも同様のエラーが発生したため、適宜追加し、シンタックスエラーを解消できた。

const nextJest = require('next/jest')

const createJestConfig = nextJest({ dir: './' })

const customJestConfig = {
  moduleDirectories: ['node_modules', '<rootDir>/src/'],
  testEnvironment: 'jest-environment-jsdom',
}

const jestConfig = async () => {
  // createJestConfig の戻り値は関数なので、さらに実行している
  const nextJestConfig = await createJestConfig(customJestConfig)()
  return {
    ...nextJestConfig,
    transformIgnorePatterns: [
      '/node_modules/@aws-sdk/(?!(client-(location|sts|sso))/)',
      '^.+\\.module\\.(css|sass|scss)$',
    ],
  }
}

module.exports = jestConfig

その他の問題

@aws-amplify/ui-react をimportしている部分で、 TypeError: window.URL.createObjectURL is not a function が発生。

これもよくある問題で、jsdomで実装されていない模様。

以下のモック設定ファイルを作成し、 jext.config.jssetupFiles で読み込ませて解消。

Object.defineProperty(URL, 'createObjectURL', {
  writable: true,
  value: jest.fn().mockImplementation(() => ''),
})

振り返り

nextJestでほとんど設定なしにJestが使えるようになったのに、やっぱりBabelでいろいろやらないとダメかと思ったが、大した手間もなく対応できてよかった。

ただ、 transformIgnorePatterns を上書きしてしまっているので、Next.jsのバージョンを変更した場合、追加・削除されたパスがないか都度確認してやる必要がありそう。

transformIgnorePatterns の配列を取得し、そこから /node_modules/ を削除、という形も考えたが、 /node_modules/ からスラッシュが消されたりする可能性も0ではないと思うので、どっちみちソース確認の必要性は変わらないか。

*1:Node Package