Skip to content

Commit

Permalink
Add next-swc jest transform (#30993)
Browse files Browse the repository at this point in the history
Co-authored-by: JJ Kasper <jj@jjsweb.site>
Co-authored-by: Tim Neutkens <tim@timneutkens.nl>
  • Loading branch information
3 people committed Nov 8, 2021
1 parent 83cd452 commit bc88831
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 124 deletions.
7 changes: 7 additions & 0 deletions 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.
4 changes: 4 additions & 0 deletions errors/manifest.json
Expand Up @@ -506,6 +506,10 @@
{
"title": "middleware-new-signature",
"path": "/errors/middleware-new-signature.md"
},
{
"title": "experimental-jest-transformer",
"path": "/errors/experimental-jest-transformer.md"
}
]
}
Expand Down
33 changes: 3 additions & 30 deletions jest.config.js
@@ -1,3 +1,5 @@
const path = require('path')

module.exports = {
testMatch: ['**/*.test.js', '**/*.test.ts', '**/*.test.tsx'],
setupFilesAfterEnv: ['<rootDir>/jest-setup-after-env.ts'],
Expand All @@ -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'),
],
},
}
1 change: 0 additions & 1 deletion package.json
Expand Up @@ -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",
Expand Down
89 changes: 89 additions & 0 deletions 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)
)
)
}
126 changes: 126 additions & 0 deletions 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,
}
}
}
90 changes: 2 additions & 88 deletions packages/next/build/webpack/loaders/next-swc-loader.js
Expand Up @@ -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
Expand All @@ -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,
})

Expand Down
1 change: 1 addition & 0 deletions packages/next/jest.js
@@ -0,0 +1 @@
module.exports = require('./dist/build/swc/jest')

1 comment on commit bc88831

@ijjk
Copy link
Member

@ijjk ijjk commented on bc88831 Nov 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stats from current release

Default Build (Increase detected ⚠️)
General Overall increase ⚠️
vercel/next.js canary v12.0.3 vercel/next.js refs/heads/canary Change
buildDuration 18.1s 18.2s ⚠️ +186ms
buildDurationCached 3.7s 3.7s -21ms
nodeModulesSize 332 MB 332 MB ⚠️ +24.5 kB
Page Load Tests Overall decrease ⚠️
vercel/next.js canary v12.0.3 vercel/next.js refs/heads/canary Change
/ failed reqs 0 0
/ total time (seconds) 2.973 2.924 -0.05
/ avg req/sec 841.03 854.96 +13.93
/error-in-render failed reqs 0 0
/error-in-render total time (seconds) 1.364 1.407 ⚠️ +0.04
/error-in-render avg req/sec 1833.04 1776.47 ⚠️ -56.57
Client Bundles (main, webpack, commons) Overall increase ⚠️
vercel/next.js canary v12.0.3 vercel/next.js refs/heads/canary Change
450.HASH.js gzip 179 B 179 B
framework-HASH.js gzip 42.2 kB 42.2 kB
main-HASH.js gzip 28 kB 28 kB ⚠️ +11 B
webpack-HASH.js gzip 1.45 kB 1.45 kB
Overall change 71.9 kB 71.9 kB ⚠️ +11 B
Legacy Client Bundles (polyfills)
vercel/next.js canary v12.0.3 vercel/next.js refs/heads/canary Change
polyfills-HASH.js gzip 31 kB 31 kB
Overall change 31 kB 31 kB
Client Pages
vercel/next.js canary v12.0.3 vercel/next.js refs/heads/canary Change
_app-HASH.js gzip 1.23 kB 1.23 kB
_error-HASH.js gzip 194 B 194 B
amp-HASH.js gzip 312 B 312 B
css-HASH.js gzip 327 B 327 B
dynamic-HASH.js gzip 2.38 kB 2.38 kB
head-HASH.js gzip 350 B 350 B
hooks-HASH.js gzip 635 B 635 B
image-HASH.js gzip 4.44 kB 4.44 kB
index-HASH.js gzip 263 B 263 B
link-HASH.js gzip 1.87 kB 1.87 kB
routerDirect..HASH.js gzip 321 B 321 B
script-HASH.js gzip 383 B 383 B
withRouter-HASH.js gzip 318 B 318 B
334f979574ae..6f4.css gzip 106 B 106 B
Overall change 13.1 kB 13.1 kB
Client Build Manifests
vercel/next.js canary v12.0.3 vercel/next.js refs/heads/canary Change
_buildManifest.js gzip 459 B 459 B
Overall change 459 B 459 B
Rendered Page Sizes Overall decrease ✓
vercel/next.js canary v12.0.3 vercel/next.js refs/heads/canary Change
index.html gzip 522 B 521 B -1 B
link.html gzip 535 B 534 B -1 B
withRouter.html gzip 515 B 515 B
Overall change 1.57 kB 1.57 kB -2 B

Diffs

Diff for main-HASH.js
@@ -527,7 +527,7 @@
         document.getElementById("__NEXT_DATA__").textContent
       );
       window.__NEXT_DATA__ = data;
-      var version = "12.0.3";
+      var version = "12.0.4-canary.0";
       exports.version = version;
       var looseToArray = function(input) {
         return [].slice.call(input);
@@ -1842,14 +1842,17 @@
       }
       function RouteAnnouncer() {
         var asPath = (0, _router).useRouter().asPath;
-        var ref = _slicedToArray(_react.default.useState(""), 2),
-          routeAnnouncement = ref[0],
-          setRouteAnnouncement = ref[1];
-        // Only announce the path change, but not for the first load because screen reader will do that automatically.
+        var ref1 = _slicedToArray(_react.default.useState(""), 2),
+          routeAnnouncement = ref1[0],
+          setRouteAnnouncement = ref1[1];
+        // Only announce the path change, but not for the first load because screen
+        // reader will do that automatically.
         var initialPathLoaded = _react.default.useRef(false);
-        // Every time the path changes, announce the route change. The announcement will be prioritized by h1, then title
-        // (from metadata), and finally if those don't exist, then the pathName that is in the URL. This methodology is
-        // inspired by Marcy Sutton's accessible client routing user testing. More information can be found here:
+        // Every time the path changes, announce the new page’s title following this
+        // priority: first the document title (from head), otherwise the first h1, or
+        // if none of these exist, then the pathname from the URL. This methodology is
+        // inspired by Marcy Sutton’s accessible client routing user testing. More
+        // information can be found here:
         // https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing/
         _react.default.useEffect(
           function() {
@@ -1857,20 +1860,22 @@
               initialPathLoaded.current = true;
               return;
             }
-            var newRouteAnnouncement;
-            var pageHeader = document.querySelector("h1");
-            if (pageHeader) {
-              newRouteAnnouncement =
-                pageHeader.innerText || pageHeader.textContent;
-            }
-            if (!newRouteAnnouncement) {
-              if (document.title) {
-                newRouteAnnouncement = document.title;
-              } else {
-                newRouteAnnouncement = asPath;
-              }
+            if (document.title) {
+              setRouteAnnouncement(document.title);
+            } else {
+              var pageHeader = document.querySelector("h1");
+              var ref;
+              var content =
+                (ref =
+                  pageHeader === null || pageHeader === void 0
+                    ? void 0
+                    : pageHeader.innerText) !== null && ref !== void 0
+                  ? ref
+                  : pageHeader === null || pageHeader === void 0
+                  ? void 0
+                  : pageHeader.textContent;
+              setRouteAnnouncement(content || asPath);
             }
-            setRouteAnnouncement(newRouteAnnouncement);
           },
           [asPath]
         );
Diff for index.html
@@ -19,7 +19,7 @@
       defer=""
     ></script>
     <script
-      src="/_next/static/chunks/main-8b1326231534a220.js"
+      src="/_next/static/chunks/main-a15e759cfdc093b7.js"
       defer=""
     ></script>
     <script
Diff for link.html
@@ -19,7 +19,7 @@
       defer=""
     ></script>
     <script
-      src="/_next/static/chunks/main-8b1326231534a220.js"
+      src="/_next/static/chunks/main-a15e759cfdc093b7.js"
       defer=""
     ></script>
     <script
Diff for withRouter.html
@@ -19,7 +19,7 @@
       defer=""
     ></script>
     <script
-      src="/_next/static/chunks/main-8b1326231534a220.js"
+      src="/_next/static/chunks/main-a15e759cfdc093b7.js"
       defer=""
     ></script>
     <script

Default Build with SWC (Increase detected ⚠️)
General Overall increase ⚠️
vercel/next.js canary v12.0.3 vercel/next.js refs/heads/canary Change
buildDuration 19.7s 19.8s ⚠️ +31ms
buildDurationCached 3.7s 3.7s ⚠️ +23ms
nodeModulesSize 332 MB 332 MB ⚠️ +24.5 kB
Page Load Tests Overall increase ✓
vercel/next.js canary v12.0.3 vercel/next.js refs/heads/canary Change
/ failed reqs 0 0
/ total time (seconds) 2.952 2.935 -0.02
/ avg req/sec 846.88 851.76 +4.88
/error-in-render failed reqs 0 0
/error-in-render total time (seconds) 1.382 1.354 -0.03
/error-in-render avg req/sec 1809.24 1846.36 +37.12
Client Bundles (main, webpack, commons) Overall increase ⚠️
vercel/next.js canary v12.0.3 vercel/next.js refs/heads/canary Change
450.HASH.js gzip 179 B 179 B
framework-HASH.js gzip 42.3 kB 42.3 kB
main-HASH.js gzip 28.2 kB 28.2 kB ⚠️ +19 B
webpack-HASH.js gzip 1.43 kB 1.43 kB
Overall change 72.1 kB 72.1 kB ⚠️ +19 B
Legacy Client Bundles (polyfills)
vercel/next.js canary v12.0.3 vercel/next.js refs/heads/canary Change
polyfills-HASH.js gzip 31 kB 31 kB
Overall change 31 kB 31 kB
Client Pages
vercel/next.js canary v12.0.3 vercel/next.js refs/heads/canary Change
_app-HASH.js gzip 1.22 kB 1.22 kB
_error-HASH.js gzip 180 B 180 B
amp-HASH.js gzip 305 B 305 B
css-HASH.js gzip 321 B 321 B
dynamic-HASH.js gzip 2.38 kB 2.38 kB
head-HASH.js gzip 342 B 342 B
hooks-HASH.js gzip 622 B 622 B
image-HASH.js gzip 4.46 kB 4.46 kB
index-HASH.js gzip 256 B 256 B
link-HASH.js gzip 1.91 kB 1.91 kB
routerDirect..HASH.js gzip 314 B 314 B
script-HASH.js gzip 375 B 375 B
withRouter-HASH.js gzip 309 B 309 B
334f979574ae..6f4.css gzip 106 B 106 B
Overall change 13.1 kB 13.1 kB
Client Build Manifests
vercel/next.js canary v12.0.3 vercel/next.js refs/heads/canary Change
_buildManifest.js gzip 460 B 460 B
Overall change 460 B 460 B
Rendered Page Sizes
vercel/next.js canary v12.0.3 vercel/next.js refs/heads/canary Change
index.html gzip 521 B 521 B
link.html gzip 534 B 534 B
withRouter.html gzip 515 B 515 B
Overall change 1.57 kB 1.57 kB

Diffs

Diff for main-HASH.js
@@ -527,7 +527,7 @@
         document.getElementById("__NEXT_DATA__").textContent
       );
       window.__NEXT_DATA__ = data;
-      var version = "12.0.3";
+      var version = "12.0.4-canary.0";
       exports.version = version;
       var looseToArray = function(input) {
         return [].slice.call(input);
@@ -1842,14 +1842,17 @@
       }
       function RouteAnnouncer() {
         var asPath = (0, _router).useRouter().asPath;
-        var ref = _slicedToArray(_react.default.useState(""), 2),
-          routeAnnouncement = ref[0],
-          setRouteAnnouncement = ref[1];
-        // Only announce the path change, but not for the first load because screen reader will do that automatically.
+        var ref1 = _slicedToArray(_react.default.useState(""), 2),
+          routeAnnouncement = ref1[0],
+          setRouteAnnouncement = ref1[1];
+        // Only announce the path change, but not for the first load because screen
+        // reader will do that automatically.
         var initialPathLoaded = _react.default.useRef(false);
-        // Every time the path changes, announce the route change. The announcement will be prioritized by h1, then title
-        // (from metadata), and finally if those don't exist, then the pathName that is in the URL. This methodology is
-        // inspired by Marcy Sutton's accessible client routing user testing. More information can be found here:
+        // Every time the path changes, announce the new page’s title following this
+        // priority: first the document title (from head), otherwise the first h1, or
+        // if none of these exist, then the pathname from the URL. This methodology is
+        // inspired by Marcy Sutton’s accessible client routing user testing. More
+        // information can be found here:
         // https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing/
         _react.default.useEffect(
           function() {
@@ -1857,20 +1860,22 @@
               initialPathLoaded.current = true;
               return;
             }
-            var newRouteAnnouncement;
-            var pageHeader = document.querySelector("h1");
-            if (pageHeader) {
-              newRouteAnnouncement =
-                pageHeader.innerText || pageHeader.textContent;
-            }
-            if (!newRouteAnnouncement) {
-              if (document.title) {
-                newRouteAnnouncement = document.title;
-              } else {
-                newRouteAnnouncement = asPath;
-              }
+            if (document.title) {
+              setRouteAnnouncement(document.title);
+            } else {
+              var pageHeader = document.querySelector("h1");
+              var ref;
+              var content =
+                (ref =
+                  pageHeader === null || pageHeader === void 0
+                    ? void 0
+                    : pageHeader.innerText) !== null && ref !== void 0
+                  ? ref
+                  : pageHeader === null || pageHeader === void 0
+                  ? void 0
+                  : pageHeader.textContent;
+              setRouteAnnouncement(content || asPath);
             }
-            setRouteAnnouncement(newRouteAnnouncement);
           },
           [asPath]
         );
Diff for index.html
@@ -19,7 +19,7 @@
       defer=""
     ></script>
     <script
-      src="/_next/static/chunks/main-8b1326231534a220.js"
+      src="/_next/static/chunks/main-a15e759cfdc093b7.js"
       defer=""
     ></script>
     <script
Diff for link.html
@@ -19,7 +19,7 @@
       defer=""
     ></script>
     <script
-      src="/_next/static/chunks/main-8b1326231534a220.js"
+      src="/_next/static/chunks/main-a15e759cfdc093b7.js"
       defer=""
     ></script>
     <script
Diff for withRouter.html
@@ -19,7 +19,7 @@
       defer=""
     ></script>
     <script
-      src="/_next/static/chunks/main-8b1326231534a220.js"
+      src="/_next/static/chunks/main-a15e759cfdc093b7.js"
       defer=""
     ></script>
     <script

Please sign in to comment.