diff --git a/errors/experimental-jest-transformer.md b/errors/experimental-jest-transformer.md new file mode 100644 index 0000000000000..c790e177d4583 --- /dev/null +++ b/errors/experimental-jest-transformer.md @@ -0,0 +1,7 @@ +# "next/jest" Experimental + +#### Why This Message Occurred + +You are using `next/jest` which is currently an experimental feature of Next.js. In a future version of Next.js `next/jest` will be marked as stable. + +If you have any feedback about the transformer you can share it on this discussion: https://github.com/vercel/next.js/discussions/31152. diff --git a/errors/manifest.json b/errors/manifest.json index 4d19b214c5144..4dc90b1c186db 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -506,6 +506,10 @@ { "title": "middleware-new-signature", "path": "/errors/middleware-new-signature.md" + }, + { + "title": "experimental-jest-transformer", + "path": "/errors/experimental-jest-transformer.md" } ] } diff --git a/jest.config.js b/jest.config.js index 96682b8402b6f..10152d4f93072 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,5 @@ +const path = require('path') + module.exports = { testMatch: ['**/*.test.js', '**/*.test.ts', '**/*.test.tsx'], setupFilesAfterEnv: ['/jest-setup-after-env.ts'], @@ -8,36 +10,7 @@ module.exports = { transform: { '.+\\.(t|j)sx?$': [ // this matches our SWC options used in https://github.com/vercel/next.js/blob/canary/packages/next/taskfile-swc.js - '@swc/jest', - { - sourceMaps: 'inline', - module: { - type: 'commonjs', - }, - env: { - targets: { - node: '12.0.0', - }, - }, - jsc: { - loose: true, - - parser: { - syntax: 'typescript', - dynamicImport: true, - tsx: true, - }, - transform: { - react: { - pragma: 'React.createElement', - pragmaFrag: 'React.Fragment', - throwIfNamespace: true, - development: false, - useBuiltins: true, - }, - }, - }, - }, + path.join(__dirname, './packages/next/jest.js'), ], }, } diff --git a/package.json b/package.json index 7e5cebe0b4117..2f8f5ccfaadae 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,6 @@ "@svgr/webpack": "5.5.0", "@swc/cli": "0.1.49", "@swc/core": "1.2.97", - "@swc/jest": "0.2.3", "@testing-library/react": "11.2.5", "@types/cheerio": "0.22.16", "@types/fs-extra": "8.1.0", diff --git a/packages/next/build/swc/jest.js b/packages/next/build/swc/jest.js new file mode 100644 index 0000000000000..63c6b6fe04545 --- /dev/null +++ b/packages/next/build/swc/jest.js @@ -0,0 +1,89 @@ +/* +Copyright (c) 2021 The swc Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +*/ + +import vm from 'vm' +import { transformSync } from './index' +import { getJestSWCOptions } from './options' + +console.warn( + '"next/jest" is currently experimental. https://nextjs.org/docs/messages/experimental-jest-transformer' +) + +/** + * Loads closest package.json in the directory hierarchy + */ +function loadClosestPackageJson(attempts = 1) { + if (attempts > 5) { + throw new Error("Can't resolve main package.json file") + } + var mainPath = attempts === 1 ? './' : Array(attempts).join('../') + try { + return require(mainPath + 'package.json') + } catch (e) { + return loadClosestPackageJson(attempts + 1) + } +} + +const packageConfig = loadClosestPackageJson() +const isEsmProject = packageConfig.type === 'module' + +// Jest use the `vm` [Module API](https://nodejs.org/api/vm.html#vm_class_vm_module) for ESM. +// see https://github.com/facebook/jest/issues/9430 +const isSupportEsm = 'Module' in vm + +module.exports = { + process(src, filename, jestOptions) { + if (!/\.[jt]sx?$/.test(filename)) { + return src + } + + let swcTransformOpts = getJestSWCOptions({ + filename, + esm: isSupportEsm && isEsm(filename, jestOptions), + }) + + return transformSync(src, { ...swcTransformOpts, filename }) + }, +} + +function getJestConfig(jestConfig) { + return 'config' in jestConfig + ? // jest 27 + jestConfig.config + : // jest 26 + jestConfig +} + +function isEsm(filename, jestOptions) { + return ( + (/\.jsx?$/.test(filename) && isEsmProject) || + getJestConfig(jestOptions).extensionsToTreatAsEsm?.find((ext) => + filename.endsWith(ext) + ) + ) +} diff --git a/packages/next/build/swc/options.js b/packages/next/build/swc/options.js new file mode 100644 index 0000000000000..ccc0e5aa00a1a --- /dev/null +++ b/packages/next/build/swc/options.js @@ -0,0 +1,126 @@ +const nextDistPath = + /(next[\\/]dist[\\/]shared[\\/]lib)|(next[\\/]dist[\\/]client)|(next[\\/]dist[\\/]pages)/ + +function getBaseSWCOptions({ + filename, + development, + hasReactRefresh, + globalWindow, +}) { + const isTSFile = filename.endsWith('.ts') + const isTypeScript = isTSFile || filename.endsWith('.tsx') + + return { + jsc: { + parser: { + syntax: isTypeScript ? 'typescript' : 'ecmascript', + dynamicImport: true, + // Exclude regular TypeScript files from React transformation to prevent e.g. generic parameters and angle-bracket type assertion from being interpreted as JSX tags. + [isTypeScript ? 'tsx' : 'jsx']: isTSFile ? false : true, + }, + + transform: { + react: { + runtime: 'automatic', + pragma: 'React.createElement', + pragmaFrag: 'React.Fragment', + throwIfNamespace: true, + development: development, + useBuiltins: true, + refresh: hasReactRefresh, + }, + optimizer: { + simplify: false, + globals: { + typeofs: { + window: globalWindow ? 'object' : 'undefined', + }, + }, + }, + regenerator: { + importPath: require.resolve('regenerator-runtime'), + }, + }, + }, + } +} + +export function getJestSWCOptions({ filename, esm }) { + let baseOptions = getBaseSWCOptions({ + filename, + development: false, + hasReactRefresh: false, + globalWindow: false, + }) + + const isNextDist = nextDistPath.test(filename) + + return { + ...baseOptions, + env: { + targets: { + // Targets the current version of Node.js + node: process.versions.node, + }, + }, + module: { + type: esm && !isNextDist ? 'es6' : 'commonjs', + }, + disableNextSsg: true, + disablePageConfig: true, + } +} + +export function getLoaderSWCOptions({ + filename, + development, + isServer, + pagesDir, + isPageFile, + hasReactRefresh, +}) { + let baseOptions = getBaseSWCOptions({ + filename, + development, + globalWindow: !isServer, + hasReactRefresh, + }) + + const isNextDist = nextDistPath.test(filename) + + if (isServer) { + return { + ...baseOptions, + // Disables getStaticProps/getServerSideProps tree shaking on the server compilation for pages + disableNextSsg: true, + disablePageConfig: true, + isDevelopment: development, + pagesDir, + isPageFile, + env: { + targets: { + // Targets the current version of Node.js + node: process.versions.node, + }, + }, + } + } else { + // Matches default @babel/preset-env behavior + baseOptions.jsc.target = 'es5' + return { + ...baseOptions, + // Ensure Next.js internals are output as commonjs modules + ...(isNextDist + ? { + module: { + type: 'commonjs', + }, + } + : {}), + disableNextSsg: !isPageFile, + isDevelopment: development, + pagesDir, + isPageFile, + } + } +} diff --git a/packages/next/build/webpack/loaders/next-swc-loader.js b/packages/next/build/webpack/loaders/next-swc-loader.js index a5e5c51fcd426..4215bb257921e 100644 --- a/packages/next/build/webpack/loaders/next-swc-loader.js +++ b/packages/next/build/webpack/loaders/next-swc-loader.js @@ -27,90 +27,7 @@ DEALINGS IN THE SOFTWARE. */ import { transform } from '../../swc' - -const nextDistPath = - /(next[\\/]dist[\\/]shared[\\/]lib)|(next[\\/]dist[\\/]client)|(next[\\/]dist[\\/]pages)/ - -function getSWCOptions({ - filename, - isServer, - development, - isPageFile, - pagesDir, - isNextDist, - hasReactRefresh, -}) { - const isTSFile = filename.endsWith('.ts') - const isTypeScript = isTSFile || filename.endsWith('.tsx') - - const jsc = { - parser: { - syntax: isTypeScript ? 'typescript' : 'ecmascript', - dynamicImport: true, - // Exclude regular TypeScript files from React transformation to prevent e.g. generic parameters and angle-bracket type assertion from being interpreted as JSX tags. - [isTypeScript ? 'tsx' : 'jsx']: isTSFile ? false : true, - }, - - transform: { - react: { - runtime: 'automatic', - pragma: 'React.createElement', - pragmaFrag: 'React.Fragment', - throwIfNamespace: true, - development: development, - useBuiltins: true, - refresh: hasReactRefresh, - }, - optimizer: { - simplify: false, - globals: { - typeofs: { - window: isServer ? 'undefined' : 'object', - }, - }, - }, - regenerator: { - importPath: require.resolve('regenerator-runtime'), - }, - }, - } - - if (isServer) { - return { - jsc, - // Disables getStaticProps/getServerSideProps tree shaking on the server compilation for pages - disableNextSsg: true, - disablePageConfig: true, - isDevelopment: development, - pagesDir, - isPageFile, - env: { - targets: { - // Targets the current version of Node.js - node: process.versions.node, - }, - }, - } - } else { - // Matches default @babel/preset-env behavior - jsc.target = 'es5' - return { - // Ensure Next.js internals are output as commonjs modules - ...(isNextDist - ? { - module: { - type: 'commonjs', - }, - } - : {}), - disableNextSsg: !isPageFile, - isDevelopment: development, - pagesDir, - isPageFile, - jsc, - } - } -} +import { getLoaderSWCOptions } from '../../swc/options' async function loaderTransform(parentTrace, source, inputSourceMap) { // Make the loader async @@ -121,15 +38,12 @@ async function loaderTransform(parentTrace, source, inputSourceMap) { const { isServer, pagesDir, hasReactRefresh } = loaderOptions const isPageFile = filename.startsWith(pagesDir) - const isNextDist = nextDistPath.test(filename) - - const swcOptions = getSWCOptions({ + const swcOptions = getLoaderSWCOptions({ pagesDir, filename, isServer: isServer, isPageFile, development: this.mode === 'development', - isNextDist, hasReactRefresh, }) diff --git a/packages/next/jest.js b/packages/next/jest.js new file mode 100644 index 0000000000000..338e34d677b32 --- /dev/null +++ b/packages/next/jest.js @@ -0,0 +1 @@ +module.exports = require('./dist/build/swc/jest') diff --git a/yarn.lock b/yarn.lock index 9235b48274f7b..ae311890732e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4111,11 +4111,6 @@ "@swc/core-win32-ia32-msvc" "1.2.97" "@swc/core-win32-x64-msvc" "1.2.97" -"@swc/jest@0.2.3": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@swc/jest/-/jest-0.2.3.tgz#5c32aaa6298267a955d35eb67094edabd5db598f" - integrity sha512-ARZIY5OkXdFRQLHc/1i+yKrl0H3B1sa7Bu9XE8yTvYZZ4G5Ewu6oyyJBM52TiROP6EpMcF7ZeQQsKMZvzuKkNw== - "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"