From 9c0c93eb162cec0a5dacb355e5ff70794f3f9f46 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 2 Mar 2022 00:28:34 +0100 Subject: [PATCH 1/6] Optimize component type filters (#34941) Since we are applying this loader to more files, and these two simple RegExp filters will be executed ~70 times per entry, it's an easy optimization to move the RegExp creation outside and merge related testers into the RegExp. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `yarn lint` --- .../loaders/next-flight-server-loader.ts | 47 +++++++------------ 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-flight-server-loader.ts b/packages/next/build/webpack/loaders/next-flight-server-loader.ts index 1beb921a9d07..fdaf07b21277 100644 --- a/packages/next/build/webpack/loaders/next-flight-server-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-server-loader.ts @@ -5,38 +5,27 @@ import { parse } from '../../swc' import { getBaseSWCOptions } from '../../swc/options' import { getRawPageExtensions } from '../../utils' -const createClientComponentFilter = - (pageExtensions: string[]) => (importSource: string) => { - const hasClientExtension = new RegExp( - `\\.client(\\.(${pageExtensions.join('|')}))?` - ).test(importSource) - // Special cases for Next.js APIs that are considered as client components: - return ( - hasClientExtension || - isNextComponent(importSource) || - isImageImport(importSource) - ) - } - -const createServerComponentFilter = - (pageExtensions: string[]) => (importSource: string) => { - return new RegExp(`\\.server(\\.(${pageExtensions.join('|')}))?`).test( - importSource - ) - } - -function isNextComponent(importSource: string) { - return ( - importSource.includes('next/link') || importSource.includes('next/image') +const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'avif'] + +const createClientComponentFilter = (pageExtensions: string[]) => { + // Special cases for Next.js APIs that are considered as client components: + // - .client.[ext] + // - next/link, next/image + // - .[imageExt] + const regex = new RegExp( + '(' + + `\\.client(\\.(${pageExtensions.join('|')}))?|` + + `next/link|next/image|` + + `\\.(${imageExtensions.join('|')})` + + ')$' ) + + return (importSource: string) => regex.test(importSource) } -function isImageImport(importSource: string) { - // TODO: share extension with next/image - // TODO: add other static assets, jpeg -> jpg - return ['jpg', 'jpeg', 'png', 'webp', 'avif'].some((imageExt) => - importSource.endsWith('.' + imageExt) - ) +const createServerComponentFilter = (pageExtensions: string[]) => { + const regex = new RegExp(`\\.server(\\.(${pageExtensions.join('|')}))?$`) + return (importSource: string) => regex.test(importSource) } async function parseImportsInfo({ From 21c07ebf9e46d65cf442630ead677d2481b481af Mon Sep 17 00:00:00 2001 From: Cody Olsen Date: Wed, 2 Mar 2022 00:42:00 +0100 Subject: [PATCH 2/6] docs: update cms-sanity example (#34898) Upgrades `next-sanity` to the latest version and applies breaking changes. Improves the docs for the Sanity example. ## Documentation / Examples - [x] Make sure the linting passes by running `yarn lint` Co-authored-by: even westvang <172952+evenwestvang@users.noreply.github.com> --- examples/cms-sanity/README.md | 37 +++++++++++++++++-- examples/cms-sanity/components/cover-image.js | 16 +++++--- examples/cms-sanity/components/post-body.js | 6 +-- examples/cms-sanity/lib/sanity.js | 6 +-- examples/cms-sanity/package.json | 9 +++-- 5 files changed, 54 insertions(+), 20 deletions(-) diff --git a/examples/cms-sanity/README.md b/examples/cms-sanity/README.md index 7dfcb0a530ea..e7b5d268d7f9 100644 --- a/examples/cms-sanity/README.md +++ b/examples/cms-sanity/README.md @@ -2,6 +2,11 @@ This example showcases Next.js's [Static Generation](https://nextjs.org/docs/basic-features/pages) feature using [Sanity](https://www.sanity.io/) as the data source. +You'll get: + +- Sanity Studio running on localhost +- Sub-second as-you-type previews in Next.js + ## Demo ### [https://next-blog-sanity.vercel.app/](https://next-blog-sanity.vercel.app/) @@ -85,11 +90,30 @@ SANITY_API_TOKEN=... SANITY_PREVIEW_SECRET=... ``` -### Step 5. Prepare project for previewing +### Step 5. Prepare the project for previewing + +5.1. Install the `@sanity/production-preview` plugin with `sanity install @sanity/production-preview`. + +5.2. Create a file called `resolveProductionUrl.js` (we'll get back to that file in a bit). + +5.3. Open your studio's sanity.json, and add the following entry to the parts-array: -Go to https://www.sanity.io/docs/preview-content-on-site and follow the three steps on that page. It should be done inside the studio project generated in Step 2. +```diff +{ + "plugins": [ + "@sanity/production-preview" + ], + "parts": [ + //... ++ { ++ "implements": "part:@sanity/production-preview/resolve-production-url", ++ "path": "./resolveProductionUrl.js" ++ } + ] +} +``` -When you get to the second step about creating a file called `resolveProductionUrl.js`, copy the following instead: +Now, go back to `resolveProductionUrl.js` and add a function that will receive the full document that was selected for previewing: ```js const previewSecret = 'MY_SECRET' // Copy the string you used for SANITY_PREVIEW_SECRET @@ -100,6 +124,8 @@ export default function resolveProductionUrl(document) { } ``` +For more information on live previewing check the [full guide.](https://www.sanity.io/guides/nextjs-live-preview) + ### Step 6. Copy the schema file After initializing your Sanity studio project there should be a `schemas` folder. @@ -168,3 +194,8 @@ To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [ Alternatively, you can deploy using our template by clicking on the Deploy button below. [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/cms-sanity&project-name=cms-sanity&repository-name=cms-sanity&env=NEXT_PUBLIC_SANITY_PROJECT_ID,SANITY_API_TOKEN,SANITY_PREVIEW_SECRET&envDescription=Required%20to%20connect%20the%20app%20with%20Sanity&envLink=https://vercel.link/cms-sanity-env) + +#### Next steps + +- Invalidate your routes in production [on-demand](https://nextjs.org/blog/next-12-1#on-demand-incremental-static-regeneration-beta) with GROQ powered webhooks +- Mount your preview inside the Sanity Studio for comfortable side-by-side editing diff --git a/examples/cms-sanity/components/cover-image.js b/examples/cms-sanity/components/cover-image.js index 60bf2e8847a3..818e8eef5e47 100644 --- a/examples/cms-sanity/components/cover-image.js +++ b/examples/cms-sanity/components/cover-image.js @@ -5,15 +5,19 @@ import { urlForImage } from '../lib/sanity' export default function CoverImage({ title, slug, image: source }) { const image = source ? ( - {`Cover + > + {`Cover + ) : (
) diff --git a/examples/cms-sanity/components/post-body.js b/examples/cms-sanity/components/post-body.js index 98a958318f08..2e0cb68a48dc 100644 --- a/examples/cms-sanity/components/post-body.js +++ b/examples/cms-sanity/components/post-body.js @@ -1,10 +1,10 @@ import markdownStyles from './markdown-styles.module.css' -import BlockContent from '@sanity/block-content-to-react' +import { PortableText } from '@portabletext/react' export default function PostBody({ content }) { return ( -
- +
+
) } diff --git a/examples/cms-sanity/lib/sanity.js b/examples/cms-sanity/lib/sanity.js index 76cedf4cb57d..1936305c132c 100644 --- a/examples/cms-sanity/lib/sanity.js +++ b/examples/cms-sanity/lib/sanity.js @@ -1,7 +1,5 @@ -import { - createImageUrlBuilder, - createPreviewSubscriptionHook, -} from 'next-sanity' +import createImageUrlBuilder from '@sanity/image-url' +import { createPreviewSubscriptionHook } from 'next-sanity' import { sanityConfig } from './config' export const imageBuilder = createImageUrlBuilder(sanityConfig) diff --git a/examples/cms-sanity/package.json b/examples/cms-sanity/package.json index 494c94ac75ff..8b576f61ae8c 100644 --- a/examples/cms-sanity/package.json +++ b/examples/cms-sanity/package.json @@ -6,17 +6,18 @@ "start": "next start" }, "dependencies": { - "@sanity/block-content-to-react": "2.0.7", + "@portabletext/react": "^1.0.3", + "@sanity/image-url": "^1.0.1", "classnames": "2.3.1", "date-fns": "2.28.0", "next": "latest", - "next-sanity": "0.3.0", + "next-sanity": "0.5.0", "react": "^17.0.2", "react-dom": "^17.0.2" }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", - "tailwindcss": "^3.0.15" + "postcss": "8.4.7", + "tailwindcss": "^3.0.23" } } From abe49367012496d8a1a25e35ea1b74b7ed4d0b38 Mon Sep 17 00:00:00 2001 From: Lydia Hallie Date: Tue, 1 Mar 2022 18:48:05 -0700 Subject: [PATCH 3/6] Update cms-wordpress example (#34944) Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com> --- .../cms-wordpress/components/more-stories.js | 4 +-- examples/cms-wordpress/lib/api.js | 28 ++++++------------- examples/cms-wordpress/next.config.js | 8 ++++++ examples/cms-wordpress/pages/index.js | 5 ++-- examples/cms-wordpress/pages/posts/[slug].js | 6 ++-- 5 files changed, 25 insertions(+), 26 deletions(-) create mode 100644 examples/cms-wordpress/next.config.js diff --git a/examples/cms-wordpress/components/more-stories.js b/examples/cms-wordpress/components/more-stories.js index e35af77cbf8d..951fe76a1500 100644 --- a/examples/cms-wordpress/components/more-stories.js +++ b/examples/cms-wordpress/components/more-stories.js @@ -11,9 +11,9 @@ export default function MoreStories({ posts }) { diff --git a/examples/cms-wordpress/lib/api.js b/examples/cms-wordpress/lib/api.js index cabad8f8113f..9ec551c9d7a6 100644 --- a/examples/cms-wordpress/lib/api.js +++ b/examples/cms-wordpress/lib/api.js @@ -70,18 +70,14 @@ export async function getAllPostsForHome(preview) { slug date featuredImage { - node { - sourceUrl - } + sourceUrl } author { - node { - name - firstName - lastName - avatar { - url - } + name + firstName + lastName + avatar { + url } } } @@ -125,14 +121,10 @@ export async function getPostAndMorePosts(slug, preview, previewData) { slug date featuredImage { - node { - sourceUrl - } + sourceUrl } author { - node { - ...AuthorFields - } + ...AuthorFields } categories { edges { @@ -164,9 +156,7 @@ export async function getPostAndMorePosts(slug, preview, previewData) { excerpt content author { - node { - ...AuthorFields - } + ...AuthorFields } } } diff --git a/examples/cms-wordpress/next.config.js b/examples/cms-wordpress/next.config.js new file mode 100644 index 000000000000..de6202d75136 --- /dev/null +++ b/examples/cms-wordpress/next.config.js @@ -0,0 +1,8 @@ +module.exports = { + images: { + domains: [ + // "[yourapp].wpengine.com" (Update this to be your Wordpress application name in order to load images connected to your posts) + 'secure.gravatar.com', + ], + }, +} diff --git a/examples/cms-wordpress/pages/index.js b/examples/cms-wordpress/pages/index.js index e6c987fb8870..2d01e152485e 100644 --- a/examples/cms-wordpress/pages/index.js +++ b/examples/cms-wordpress/pages/index.js @@ -22,9 +22,9 @@ export default function Index({ allPosts: { edges }, preview }) { {heroPost && ( @@ -38,6 +38,7 @@ export default function Index({ allPosts: { edges }, preview }) { export async function getStaticProps({ preview = false }) { const allPosts = await getAllPostsForHome(preview) + return { props: { allPosts, preview }, } diff --git a/examples/cms-wordpress/pages/posts/[slug].js b/examples/cms-wordpress/pages/posts/[slug].js index b57d69536ff9..dc06089f5831 100644 --- a/examples/cms-wordpress/pages/posts/[slug].js +++ b/examples/cms-wordpress/pages/posts/[slug].js @@ -36,14 +36,14 @@ export default function Post({ post, posts, preview }) { From b579a35e83531519f7edfbbdb1b7b3adc493afb4 Mon Sep 17 00:00:00 2001 From: Sacha Date: Wed, 2 Mar 2022 03:43:34 +0100 Subject: [PATCH 4/6] [examples] Update styled-components to use SWC (#34770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * rename example with-styled-components to with-styled-components-babel * update with-styled-components references * update docs on react hydratation erros for styled components * clone with-styled-components-babel without babel config * add styledComponents to compiler options in next.config * replace babel dependency with Next.js SWC compiler * upgrade example typescript version example with-typescript-styled-components had a typescript dependency lower than the recommended version. Upgraded from 4.2.4 to 4.5.5. * Apply suggestions from code review * update readme for with-styled-components-babel remove Notes section since it describes a workaround which is no longer required to work with `next/link`. Co-authored-by: Lee Robinson Co-authored-by: Balázs Orbán --- errors/react-hydration-error.md | 5 +-- .../.babelrc | 0 .../with-styled-components-babel/.gitignore | 34 +++++++++++++++++++ .../with-styled-components-babel/README.md | 29 ++++++++++++++++ .../with-styled-components-babel/package.json | 18 ++++++++++ .../pages/_app.js | 26 ++++++++++++++ .../pages/_document.js | 0 .../pages/index.js | 10 ++++++ examples/with-styled-components/README.md | 4 ++- .../with-styled-components/next.config.js | 10 ++++++ examples/with-styled-components/package.json | 3 -- .../.babelrc | 4 --- .../next.config.js | 10 ++++++ .../package.json | 3 +- .../pages/_document.tsx | 30 ---------------- ...s => with-styled-components-babel.test.ts} | 2 +- 16 files changed, 145 insertions(+), 43 deletions(-) rename examples/{with-styled-components => with-styled-components-babel}/.babelrc (100%) create mode 100644 examples/with-styled-components-babel/.gitignore create mode 100644 examples/with-styled-components-babel/README.md create mode 100644 examples/with-styled-components-babel/package.json create mode 100644 examples/with-styled-components-babel/pages/_app.js rename examples/{with-styled-components => with-styled-components-babel}/pages/_document.js (100%) create mode 100644 examples/with-styled-components-babel/pages/index.js create mode 100644 examples/with-styled-components/next.config.js delete mode 100644 examples/with-typescript-styled-components/.babelrc create mode 100644 examples/with-typescript-styled-components/next.config.js delete mode 100644 examples/with-typescript-styled-components/pages/_document.tsx rename test/e2e/yarn-pnp/test/{with-styled-components.test.ts => with-styled-components-babel.test.ts} (61%) diff --git a/errors/react-hydration-error.md b/errors/react-hydration-error.md index 38ea8e05540b..5b0ae7b6a500 100644 --- a/errors/react-hydration-error.md +++ b/errors/react-hydration-error.md @@ -41,8 +41,9 @@ Common causes with css-in-js libraries: - When using Styled Components / Emotion - When css-in-js libraries are not set up for pre-rendering (SSR/SSG) it will often lead to a hydration mismatch. In general this means the application has to follow the Next.js example for the library. For example if `pages/_document` is missing and the Babel plugin is not added. - - Possible fix for Styled Components: https://github.com/vercel/next.js/tree/canary/examples/with-styled-components - - If you want to leverage Styled Components with the new Next.js Compiler in Next.js 12 there is an [experimental flag available](https://github.com/vercel/next.js/discussions/30174#discussion-3643870) + - Possible fix for Styled Components: + - If you want to leverage Styled Components with SWC in Next.js 12.1+ you need to [add it to your Next.js config under compiler options](https://nextjs.org/docs/advanced-features/compiler#styled-components): https://github.com/vercel/next.js/tree/canary/examples/with-styled-components + - If you want to use Styled Components with Babel, you need `pages/_document` and the Babel plugin: https://github.com/vercel/next.js/tree/canary/examples/with-styled-components-babel - Possible fix for Emotion: https://github.com/vercel/next.js/tree/canary/examples/with-emotion - When using other css-in-js libraries - Similar to Styled Components / Emotion css-in-js libraries generally need configuration specified in their examples in the [examples directory](https://github.com/vercel/next.js/tree/canary/examples) diff --git a/examples/with-styled-components/.babelrc b/examples/with-styled-components-babel/.babelrc similarity index 100% rename from examples/with-styled-components/.babelrc rename to examples/with-styled-components-babel/.babelrc diff --git a/examples/with-styled-components-babel/.gitignore b/examples/with-styled-components-babel/.gitignore new file mode 100644 index 000000000000..1437c53f70bc --- /dev/null +++ b/examples/with-styled-components-babel/.gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel diff --git a/examples/with-styled-components-babel/README.md b/examples/with-styled-components-babel/README.md new file mode 100644 index 000000000000..f9f489ed9459 --- /dev/null +++ b/examples/with-styled-components-babel/README.md @@ -0,0 +1,29 @@ +# Example app with styled-components using babel + +This example features how you use a different styling solution than [styled-jsx](https://github.com/vercel/styled-jsx) that also supports universal styles. That means we can serve the required styles for the first render within the HTML and then load the rest in the client. In this case we are using [styled-components](https://github.com/styled-components/styled-components). + +For this purpose we are extending the `` and injecting the server side rendered styles into the ``, and also adding the `babel-plugin-styled-components` (which is required for server side rendering). Additionally we set up a global [theme](https://www.styled-components.com/docs/advanced#theming) for styled-components using NextJS custom [``](https://nextjs.org/docs/advanced-features/custom-app) component. + +## Preview + +Preview the example live on [StackBlitz](http://stackblitz.com/): + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/with-styled-components-babel) + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-styled-components-babel&project-name=with-styled-components-babel&repository-name=with-styled-components-babel) + +## How to use + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: + +```bash +npx create-next-app --example with-styled-components-babel with-styled-components-babel-app +# or +yarn create next-app --example with-styled-components-babel with-styled-components-babel-app +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). diff --git a/examples/with-styled-components-babel/package.json b/examples/with-styled-components-babel/package.json new file mode 100644 index 000000000000..83bbeb7b6a61 --- /dev/null +++ b/examples/with-styled-components-babel/package.json @@ -0,0 +1,18 @@ +{ + "private": true, + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "latest", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-is": "^17.0.2", + "styled-components": "^5.2.3" + }, + "devDependencies": { + "babel-plugin-styled-components": "^1.12.0" + } +} diff --git a/examples/with-styled-components-babel/pages/_app.js b/examples/with-styled-components-babel/pages/_app.js new file mode 100644 index 000000000000..019f78b2afc6 --- /dev/null +++ b/examples/with-styled-components-babel/pages/_app.js @@ -0,0 +1,26 @@ +import { createGlobalStyle, ThemeProvider } from 'styled-components' + +const GlobalStyle = createGlobalStyle` + body { + margin: 0; + padding: 0; + box-sizing: border-box; + } +` + +const theme = { + colors: { + primary: '#0070f3', + }, +} + +export default function App({ Component, pageProps }) { + return ( + <> + + + + + + ) +} diff --git a/examples/with-styled-components/pages/_document.js b/examples/with-styled-components-babel/pages/_document.js similarity index 100% rename from examples/with-styled-components/pages/_document.js rename to examples/with-styled-components-babel/pages/_document.js diff --git a/examples/with-styled-components-babel/pages/index.js b/examples/with-styled-components-babel/pages/index.js new file mode 100644 index 000000000000..8d1e94ba0110 --- /dev/null +++ b/examples/with-styled-components-babel/pages/index.js @@ -0,0 +1,10 @@ +import styled from 'styled-components' + +const Title = styled.h1` + font-size: 50px; + color: ${({ theme }) => theme.colors.primary}; +` + +export default function Home() { + return My page +} diff --git a/examples/with-styled-components/README.md b/examples/with-styled-components/README.md index 243e34b97d70..5a0542590fca 100644 --- a/examples/with-styled-components/README.md +++ b/examples/with-styled-components/README.md @@ -2,7 +2,9 @@ This example features how you use a different styling solution than [styled-jsx](https://github.com/vercel/styled-jsx) that also supports universal styles. That means we can serve the required styles for the first render within the HTML and then load the rest in the client. In this case we are using [styled-components](https://github.com/styled-components/styled-components). -For this purpose we are extending the `` and injecting the server side rendered styles into the ``, and also adding the `babel-plugin-styled-components` (which is required for server side rendering). Additionally we set up a global [theme](https://www.styled-components.com/docs/advanced#theming) for styled-components using NextJS custom [``](https://nextjs.org/docs/advanced-features/custom-app) component. +This example uses the Rust-based [SWC](https://nextjs.org/docs/advanced-features/compiler#styled-components) in Next.js for better performance than Babel. + +Currently, only the `ssr` and `displayName` transforms have been implemented. These two transforms are the main requirement for using `styled-components` in Next.js. ## Preview diff --git a/examples/with-styled-components/next.config.js b/examples/with-styled-components/next.config.js new file mode 100644 index 000000000000..52503c2612a3 --- /dev/null +++ b/examples/with-styled-components/next.config.js @@ -0,0 +1,10 @@ +/** @type {import('next').NextConfig} */ + +const nextConfig = { + reactStrictMode: true, + compiler: { + styledComponents: true, + }, +} + +module.exports = nextConfig diff --git a/examples/with-styled-components/package.json b/examples/with-styled-components/package.json index 83bbeb7b6a61..c5b30dc1b2c7 100644 --- a/examples/with-styled-components/package.json +++ b/examples/with-styled-components/package.json @@ -11,8 +11,5 @@ "react-dom": "^17.0.2", "react-is": "^17.0.2", "styled-components": "^5.2.3" - }, - "devDependencies": { - "babel-plugin-styled-components": "^1.12.0" } } diff --git a/examples/with-typescript-styled-components/.babelrc b/examples/with-typescript-styled-components/.babelrc deleted file mode 100644 index 854cb73a8103..000000000000 --- a/examples/with-typescript-styled-components/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["next/babel"], - "plugins": [["styled-components", { "ssr": true }]] -} diff --git a/examples/with-typescript-styled-components/next.config.js b/examples/with-typescript-styled-components/next.config.js new file mode 100644 index 000000000000..52503c2612a3 --- /dev/null +++ b/examples/with-typescript-styled-components/next.config.js @@ -0,0 +1,10 @@ +/** @type {import('next').NextConfig} */ + +const nextConfig = { + reactStrictMode: true, + compiler: { + styledComponents: true, + }, +} + +module.exports = nextConfig diff --git a/examples/with-typescript-styled-components/package.json b/examples/with-typescript-styled-components/package.json index 18ad75ca09f7..76f91cde30c1 100644 --- a/examples/with-typescript-styled-components/package.json +++ b/examples/with-typescript-styled-components/package.json @@ -17,7 +17,6 @@ "@types/react": "17.0.4", "@types/react-dom": "17.0.3", "@types/styled-components": "5.1.9", - "babel-plugin-styled-components": "^1.12.0", - "typescript": "4.2.4" + "typescript": "^4.5.5" } } diff --git a/examples/with-typescript-styled-components/pages/_document.tsx b/examples/with-typescript-styled-components/pages/_document.tsx deleted file mode 100644 index eb67e4c6d739..000000000000 --- a/examples/with-typescript-styled-components/pages/_document.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import Document, { DocumentContext } from 'next/document' -import { ServerStyleSheet } from 'styled-components' - -export default class MyDocument extends Document { - static async getInitialProps(ctx: DocumentContext) { - const sheet = new ServerStyleSheet() - const originalRenderPage = ctx.renderPage - - try { - ctx.renderPage = () => - originalRenderPage({ - enhanceApp: (App) => (props) => - sheet.collectStyles(), - }) - - const initialProps = await Document.getInitialProps(ctx) - return { - ...initialProps, - styles: ( - <> - {initialProps.styles} - {sheet.getStyleElement()} - - ), - } - } finally { - sheet.seal() - } - } -} diff --git a/test/e2e/yarn-pnp/test/with-styled-components.test.ts b/test/e2e/yarn-pnp/test/with-styled-components-babel.test.ts similarity index 61% rename from test/e2e/yarn-pnp/test/with-styled-components.test.ts rename to test/e2e/yarn-pnp/test/with-styled-components-babel.test.ts index f8ee921afe98..35c778df540b 100644 --- a/test/e2e/yarn-pnp/test/with-styled-components.test.ts +++ b/test/e2e/yarn-pnp/test/with-styled-components-babel.test.ts @@ -1,5 +1,5 @@ import { runTests } from './utils' describe('yarn PnP', () => { - runTests('with-styled-components') + runTests('with-styled-components-babel') }) From 7b2fb70a67ac114414941ea93a1351871bfe46ea Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Wed, 2 Mar 2022 17:09:36 +0200 Subject: [PATCH 5/6] Expose WASM bindings in Middleware (#34437) This PR introduces a way to use WASM in middlewares. Next.js will find all `.wasm` imports in middlewares and load them as `WebAssembly.Module` objects, which then can be later instantiated. The metadata will be stored in `middleware-manifest.json` ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `yarn lint` --- packages/next/build/webpack-config.ts | 9 ++ .../loaders/next-middleware-wasm-loader.ts | 21 ++++ .../plugins/functions-manifest-plugin.ts | 6 +- .../webpack/plugins/middleware-plugin.ts | 33 +++++- packages/next/server/next-server.ts | 1 + packages/next/server/require.ts | 12 +- packages/next/server/web/sandbox/context.ts | 32 +++++- packages/next/server/web/sandbox/sandbox.ts | 5 +- .../middleware-can-use-wasm-files/add.wasm | Bin 0 -> 126 bytes .../index.test.ts | 103 ++++++++++++++++++ 10 files changed, 207 insertions(+), 15 deletions(-) create mode 100644 packages/next/build/webpack/loaders/next-middleware-wasm-loader.ts create mode 100755 test/e2e/middleware-can-use-wasm-files/add.wasm create mode 100644 test/e2e/middleware-can-use-wasm-files/index.test.ts diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 3b280aba9737..82d9cb2d9e60 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1146,6 +1146,7 @@ export default async function getBaseWebpackConfig( 'noop-loader', 'next-middleware-loader', 'next-middleware-ssr-loader', + 'next-middleware-wasm-loader', ].reduce((alias, loader) => { // using multiple aliases to replace `resolveLoader.modules` alias[loader] = path.join(__dirname, 'webpack', 'loaders', loader) @@ -1529,6 +1530,14 @@ export default async function getBaseWebpackConfig( const webpack5Config = webpackConfig as webpack5.Configuration + webpack5Config.module?.rules?.unshift({ + test: /\.wasm$/, + issuerLayer: 'middleware', + loader: 'next-middleware-wasm-loader', + type: 'javascript/auto', + resourceQuery: /module/i, + }) + webpack5Config.experiments = { layers: true, cacheUnaffected: true, diff --git a/packages/next/build/webpack/loaders/next-middleware-wasm-loader.ts b/packages/next/build/webpack/loaders/next-middleware-wasm-loader.ts new file mode 100644 index 000000000000..58cf76c9b5e4 --- /dev/null +++ b/packages/next/build/webpack/loaders/next-middleware-wasm-loader.ts @@ -0,0 +1,21 @@ +import crypto from 'crypto' + +export type WasmBinding = { + filePath: string + name: string +} + +export default function MiddlewareWasmLoader(this: any, source: Buffer) { + const name = `wasm_${sha1(source)}` + const filePath = `server/middleware-chunks/${name}.wasm` + const binding: WasmBinding = { filePath, name } + this._module.buildInfo.nextWasmMiddlewareBinding = binding + this.emitFile(`/${filePath}`, source, null) + return `module.exports = ${name};` +} + +export const raw = true + +function sha1(source: string | Buffer) { + return crypto.createHash('sha1').update(source).digest('hex') +} diff --git a/packages/next/build/webpack/plugins/functions-manifest-plugin.ts b/packages/next/build/webpack/plugins/functions-manifest-plugin.ts index 4a5cf366e147..c8a04a2f34b9 100644 --- a/packages/next/build/webpack/plugins/functions-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/functions-manifest-plugin.ts @@ -3,7 +3,7 @@ import { sources, webpack5 } from 'next/dist/compiled/webpack/webpack' import { normalizePagePath } from '../../../server/normalize-page-path' import { FUNCTIONS_MANIFEST } from '../../../shared/lib/constants' import { getPageFromPath } from '../../entries' -import { collectAssets, getEntrypointInfo } from './middleware-plugin' +import { collectAssets, getEntrypointInfo, PerRoute } from './middleware-plugin' const PLUGIN_NAME = 'FunctionsManifestPlugin' export interface FunctionsManifest { @@ -52,7 +52,7 @@ export default class FunctionsManifestPlugin { createAssets( compilation: webpack5.Compilation, assets: any, - envPerRoute: Map, + perRoute: PerRoute, isEdgeRuntime: boolean ) { const functionsManifest: FunctionsManifest = { @@ -60,7 +60,7 @@ export default class FunctionsManifestPlugin { pages: {}, } - const infos = getEntrypointInfo(compilation, envPerRoute, isEdgeRuntime) + const infos = getEntrypointInfo(compilation, perRoute, isEdgeRuntime) infos.forEach((info) => { const { page } = info // TODO: use global default runtime instead of 'web' diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index 7ada45ac32d4..02981076035c 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -10,6 +10,7 @@ import { MIDDLEWARE_SSR_RUNTIME_WEBPACK, } from '../../../shared/lib/constants' import { nonNullable } from '../../../lib/non-nullable' +import type { WasmBinding } from '../loaders/next-middleware-wasm-loader' const PLUGIN_NAME = 'MiddlewarePlugin' const MIDDLEWARE_FULL_ROUTE_REGEX = /^pages[/\\]?(.*)\/_middleware$/ @@ -27,6 +28,7 @@ export interface MiddlewareManifest { name: string page: string regexp: string + wasm?: WasmBinding[] } } } @@ -49,9 +51,14 @@ function getPageFromEntrypointName(pagePath: string) { return page } +export type PerRoute = { + envPerRoute: Map + wasmPerRoute: Map +} + export function getEntrypointInfo( compilation: webpack5.Compilation, - envPerRoute: Map, + { envPerRoute, wasmPerRoute }: PerRoute, isEdgeRuntime: boolean ) { const entrypoints = compilation.entrypoints @@ -87,6 +94,7 @@ export function getEntrypointInfo( infos.push({ env: envPerRoute.get(entrypoint.name) || [], + wasm: wasmPerRoute.get(entrypoint.name) || [], files, name: entrypoint.name, page, @@ -114,10 +122,14 @@ export default class MiddlewarePlugin { createAssets( compilation: webpack5.Compilation, assets: any, - envPerRoute: Map, + { envPerRoute, wasmPerRoute }: PerRoute, isEdgeRuntime: boolean ) { - const infos = getEntrypointInfo(compilation, envPerRoute, isEdgeRuntime) + const infos = getEntrypointInfo( + compilation, + { envPerRoute, wasmPerRoute }, + isEdgeRuntime + ) infos.forEach((info) => { middlewareManifest.middleware[info.page] = info }) @@ -152,7 +164,7 @@ export function collectAssets( createAssets: ( compilation: webpack5.Compilation, assets: any, - envPerRoute: Map, + { envPerRoute, wasmPerRoute }: PerRoute, isEdgeRuntime: boolean ) => void, options: { @@ -175,6 +187,7 @@ export function collectAssets( }) const envPerRoute = new Map() + const wasmPerRoute = new Map() compilation.hooks.afterOptimizeModules.tap(PLUGIN_NAME, () => { const { moduleGraph } = compilation as any @@ -187,6 +200,7 @@ export function collectAssets( ) { const middlewareEntries = new Set() const env = new Set() + const wasm = new Set() const addEntriesFromDependency = (dep: any) => { const module = moduleGraph.getModule(dep) @@ -203,6 +217,9 @@ export function collectAssets( const queue = new Set(middlewareEntries) for (const module of queue) { const { buildInfo } = module + if (buildInfo.nextWasmMiddlewareBinding) { + wasm.add(buildInfo.nextWasmMiddlewareBinding) + } if ( !options.dev && buildInfo && @@ -247,6 +264,7 @@ export function collectAssets( } envPerRoute.set(name, Array.from(env)) + wasmPerRoute.set(name, Array.from(wasm)) } } }) @@ -375,7 +393,12 @@ export function collectAssets( stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, }, (assets: any) => { - createAssets(compilation, assets, envPerRoute, options.isEdgeRuntime) + createAssets( + compilation, + assets, + { envPerRoute, wasmPerRoute }, + options.isEdgeRuntime + ) } ) } diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 69d30e7357d4..d31e33bb5ce1 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -1269,6 +1269,7 @@ export default class NextNodeServer extends BaseServer { name: middlewareInfo.name, paths: middlewareInfo.paths, env: middlewareInfo.env, + wasm: middlewareInfo.wasm, request: { headers: params.request.headers, method, diff --git a/packages/next/server/require.ts b/packages/next/server/require.ts index 9c34b8538300..a5cb61665bb0 100644 --- a/packages/next/server/require.ts +++ b/packages/next/server/require.ts @@ -11,6 +11,7 @@ import { normalizePagePath, denormalizePagePath } from './normalize-page-path' import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path' import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' import type { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin' +import type { WasmBinding } from '../build/webpack/loaders/next-middleware-wasm-loader' export function pageNotFoundError(page: string): Error { const err: any = new Error(`Cannot find module for page: ${page}`) @@ -85,7 +86,12 @@ export function getMiddlewareInfo(params: { distDir: string page: string serverless: boolean -}): { name: string; paths: string[]; env: string[] } { +}): { + name: string + paths: string[] + env: string[] + wasm: WasmBinding[] +} { const serverBuildPath = join( params.distDir, params.serverless && !params.dev ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY @@ -113,5 +119,9 @@ export function getMiddlewareInfo(params: { name: pageInfo.name, paths: pageInfo.files.map((file) => join(params.distDir, file)), env: pageInfo.env ?? [], + wasm: (pageInfo.wasm ?? []).map((binding) => ({ + ...binding, + filePath: join(params.distDir, binding.filePath), + })), } } diff --git a/packages/next/server/web/sandbox/context.ts b/packages/next/server/web/sandbox/context.ts index 42f480bb6a31..6685b4f6255e 100644 --- a/packages/next/server/web/sandbox/context.ts +++ b/packages/next/server/web/sandbox/context.ts @@ -1,6 +1,6 @@ import type { Context } from 'vm' import { Blob, File, FormData } from 'next/dist/compiled/formdata-node' -import { readFileSync } from 'fs' +import { readFileSync, promises as fs } from 'fs' import { requireDependencies } from './require' import { TransformStream } from 'next/dist/compiled/web-streams-polyfill' import cookie from 'next/dist/compiled/cookie' @@ -10,6 +10,7 @@ import { AbortSignal, } from 'next/dist/compiled/abort-controller' import vm from 'vm' +import type { WasmBinding } from '../../../build/webpack/loaders/next-middleware-wasm-loader' const WEBPACK_HASH_REGEX = /__webpack_require__\.h = function\(\) \{ return "[0-9a-f]+"; \}/g @@ -52,18 +53,19 @@ const caches = new Map< * run in within the context. It may or may not use a cache depending on * the parameters. */ -export function getModuleContext(options: { +export async function getModuleContext(options: { module: string onWarning: (warn: Error) => void useCache: boolean env: string[] + wasm: WasmBinding[] }) { let moduleCache = options.useCache ? caches.get(options.module) - : createModuleContext(options) + : await createModuleContext(options) if (!moduleCache) { - moduleCache = createModuleContext(options) + moduleCache = await createModuleContext(options) caches.set(options.module, moduleCache) } @@ -95,10 +97,11 @@ export function getModuleContext(options: { * 2. Dependencies that require runtime globals such as Blob. * 3. Dependencies that are scoped for the provided parameters. */ -function createModuleContext(options: { +async function createModuleContext(options: { onWarning: (warn: Error) => void module: string env: string[] + wasm: WasmBinding[] }) { const requireCache = new Map([ [require.resolve('next/dist/compiled/cookie'), { exports: cookie }], @@ -166,6 +169,8 @@ function createModuleContext(options: { return fetch(String(input), init) } + Object.assign(context, await loadWasm(options.wasm)) + return moduleCache } @@ -260,3 +265,20 @@ function buildEnvironmentVariablesFrom( const pairs = keys.map((key) => [key, process.env[key]]) return Object.fromEntries(pairs) } + +async function loadWasm( + wasm: WasmBinding[] +): Promise> { + const modules: Record = {} + + await Promise.all( + wasm.map(async (binding) => { + const module = await WebAssembly.compile( + await fs.readFile(binding.filePath) + ) + modules[binding.name] = module + }) + ) + + return modules +} diff --git a/packages/next/server/web/sandbox/sandbox.ts b/packages/next/server/web/sandbox/sandbox.ts index 576e8be7580c..b24451abcb26 100644 --- a/packages/next/server/web/sandbox/sandbox.ts +++ b/packages/next/server/web/sandbox/sandbox.ts @@ -1,3 +1,4 @@ +import type { WasmBinding } from '../../../build/webpack/loaders/next-middleware-wasm-loader' import type { RequestData, FetchEventResult } from '../types' import { getModuleContext } from './context' @@ -8,12 +9,14 @@ export async function run(params: { paths: string[] request: RequestData useCache: boolean + wasm: WasmBinding[] }): Promise { - const { runInContext, context } = getModuleContext({ + const { runInContext, context } = await getModuleContext({ module: params.name, onWarning: params.onWarning, useCache: params.useCache !== false, env: params.env, + wasm: params.wasm, }) for (const paramPath of params.paths) { diff --git a/test/e2e/middleware-can-use-wasm-files/add.wasm b/test/e2e/middleware-can-use-wasm-files/add.wasm new file mode 100755 index 0000000000000000000000000000000000000000..f22496d0b6c87704a3844134af2a9fad47fa344c GIT binary patch literal 126 zcmY+)F%E)25CzcxcYuwog{_@8@C=+}7_*ZYlLaC+R?E>mnzU4}d9bw*08e2AMpjml z05&ZblC2Pz?kbhTw*8PQj>db_6)*Gq8<13=Zi_x_bz!fX?PKawmJlsxohJwTbJ%CZ I4Fg~44-%gmga7~l literal 0 HcmV?d00001 diff --git a/test/e2e/middleware-can-use-wasm-files/index.test.ts b/test/e2e/middleware-can-use-wasm-files/index.test.ts new file mode 100644 index 000000000000..9e99b896601c --- /dev/null +++ b/test/e2e/middleware-can-use-wasm-files/index.test.ts @@ -0,0 +1,103 @@ +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { fetchViaHTTP } from 'next-test-utils' +import path from 'path' +import fs from 'fs-extra' + +function baseNextConfig(): Parameters[0] { + return { + files: { + 'src/add.wasm': new FileRef(path.join(__dirname, './add.wasm')), + 'src/add.js': ` + import wasm from './add.wasm?module' + const instance$ = WebAssembly.instantiate(wasm); + + export async function increment(a) { + const { exports } = await instance$; + return exports.add_one(a); + } + `, + 'pages/_middleware.js': ` + import { increment } from '../src/add.js' + export default async function middleware(request) { + const input = Number(request.nextUrl.searchParams.get('input')) || 1; + const value = await increment(input); + return new Response(JSON.stringify({ input, value })); + } + `, + }, + } +} + +describe('middleware can use wasm files', () => { + let next: NextInstance + + beforeAll(async () => { + const config = baseNextConfig() + next = await createNext(config) + }) + afterAll(() => next.destroy()) + + it('uses the wasm file', async () => { + const response = await fetchViaHTTP(next.url, '/') + expect(await response.json()).toEqual({ + input: 1, + value: 2, + }) + }) + + it('can be called twice', async () => { + const response = await fetchViaHTTP(next.url, '/', { input: 2 }) + expect(await response.json()).toEqual({ + input: 2, + value: 3, + }) + }) + + it('lists the necessary wasm bindings in the manifest', async () => { + const manifestPath = path.join( + next.testDir, + '.next/server/middleware-manifest.json' + ) + const manifest = await fs.readJSON(manifestPath) + expect(manifest.middleware['/']).toMatchObject({ + wasm: [ + { + filePath: + 'server/middleware-chunks/wasm_58ccff8b2b94b5dac6ef8957082ecd8f6d34186d.wasm', + name: 'wasm_58ccff8b2b94b5dac6ef8957082ecd8f6d34186d', + }, + ], + }) + }) +}) + +describe('middleware can use wasm files with the experimental modes on', () => { + let next: NextInstance + + beforeAll(async () => { + const config = baseNextConfig() + config.files['next.config.js'] = ` + module.exports = { + webpack(config) { + config.output.webassemblyModuleFilename = 'static/wasm/[modulehash].wasm' + + // Since Webpack 5 doesn't enable WebAssembly by default, we should do it manually + config.experiments = { ...config.experiments, asyncWebAssembly: true } + + return config + }, + } + ` + next = await createNext(config) + }) + afterAll(() => next.destroy()) + + it('uses the wasm file', async () => { + const response = await fetchViaHTTP(next.url, '/') + expect(await response.json()).toEqual({ + input: 1, + value: 2, + }) + }) +}) From d9d494adaf2114f9519b8ea816126ed4787deffb Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 2 Mar 2022 18:41:41 +0100 Subject: [PATCH 6/6] Provide page runtime parsing utils (#34922) * parse runtime config * add test * fix typing * fix lint warning * change branch purging condition * dont fallback to global runtime --- packages/next/build/entries.ts | 61 ++++++++++++++++++- packages/next/build/swc/index.d.ts | 7 +++ packages/next/build/swc/index.js | 4 +- packages/next/build/swc/options.d.ts | 7 +++ packages/next/build/swc/options.js | 29 ++++++--- .../loaders/next-flight-client-loader.ts | 11 +--- .../loaders/next-flight-server-loader.ts | 10 +-- test/integration/react-18/app/pages/index.js | 4 ++ test/unit/fixtures/page-runtime/edge.js | 8 +++ test/unit/fixtures/page-runtime/nodejs.js | 8 +++ test/unit/fixtures/page-runtime/static.js | 3 + test/unit/parse-page-runtime.test.ts | 27 ++++++++ 12 files changed, 147 insertions(+), 32 deletions(-) create mode 100644 packages/next/build/swc/index.d.ts create mode 100644 packages/next/build/swc/options.d.ts create mode 100644 test/unit/fixtures/page-runtime/edge.js create mode 100644 test/unit/fixtures/page-runtime/nodejs.js create mode 100644 test/unit/fixtures/page-runtime/static.js create mode 100644 test/unit/parse-page-runtime.test.ts diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 15f3ab1a7c1e..45ce053f3ac6 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -1,3 +1,4 @@ +import fs from 'fs' import chalk from 'next/dist/compiled/chalk' import { posix, join } from 'path' import { stringify } from 'querystring' @@ -12,6 +13,7 @@ import { ClientPagesLoaderOptions } from './webpack/loaders/next-client-pages-lo import { ServerlessLoaderQuery } from './webpack/loaders/next-serverless-loader' import { LoadedEnvFiles } from '@next/env' import { NextConfigComplete } from '../server/config-shared' +import { parse } from '../build/swc' import { isCustomErrorPage, isFlightPage, isReservedPage } from './utils' import { ssrEntries } from './webpack/plugins/middleware-plugin' import type { webpack5 } from 'next/dist/compiled/webpack/webpack' @@ -101,6 +103,60 @@ type Entrypoints = { edgeServer: webpack5.EntryObject } +export async function getPageRuntime(pageFilePath: string) { + let pageRuntime: string | undefined = undefined + const pageContent = await fs.promises.readFile(pageFilePath, { + encoding: 'utf8', + }) + // branch prunes for entry page without runtime option + if (pageContent.includes('runtime')) { + const { body } = await parse(pageContent, { + filename: pageFilePath, + isModule: true, + }) + body.some((node: any) => { + const { type, declaration } = node + const valueNode = declaration?.declarations?.[0] + if (type === 'ExportDeclaration' && valueNode?.id?.value === 'config') { + const props = valueNode.init.properties + const runtimeKeyValue = props.find( + (prop: any) => prop.key.value === 'runtime' + ) + const runtime = runtimeKeyValue?.value?.value + pageRuntime = + runtime === 'edge' || runtime === 'nodejs' ? runtime : pageRuntime + return true + } + return false + }) + } + + return pageRuntime +} + +export async function createPagesRuntimeMapping( + pagesDir: string, + pages: PagesMapping +) { + const pagesRuntime: Record = {} + + const promises = Object.keys(pages).map(async (page) => { + const absolutePagePath = pages[page] + const isReserved = isReservedPage(page) + if (!isReserved) { + const pageFilePath = join( + pagesDir, + absolutePagePath.replace(PAGES_DIR_ALIAS, '') + ) + const runtime = await getPageRuntime(pageFilePath) + if (runtime) { + pagesRuntime[page] = runtime + } + } + }) + return await Promise.all(promises) +} + export function createEntrypoints( pages: PagesMapping, target: 'server' | 'serverless' | 'experimental-serverless-trace', @@ -117,8 +173,6 @@ export function createEntrypoints( Object.keys(config.publicRuntimeConfig).length > 0 || Object.keys(config.serverRuntimeConfig).length > 0 - const edgeRuntime = config.experimental.runtime === 'edge' - const defaultServerlessOptions = { absoluteAppPath: pages['/_app'], absoluteDocumentPath: pages['/_document'], @@ -146,6 +200,9 @@ export function createEntrypoints( reactRoot: config.experimental.reactRoot ? 'true' : '', } + const globalRuntime = config.experimental.runtime + const edgeRuntime = globalRuntime === 'edge' + Object.keys(pages).forEach((page) => { const absolutePagePath = pages[page] const bundleFile = normalizePagePath(page) diff --git a/packages/next/build/swc/index.d.ts b/packages/next/build/swc/index.d.ts new file mode 100644 index 000000000000..779b53084a45 --- /dev/null +++ b/packages/next/build/swc/index.d.ts @@ -0,0 +1,7 @@ +export function isWasm(): Promise +export function transform(src: string, options?: any): Promise +export function transformSync(src: string, options?: any): any +export function minify(src: string, options: any): Promise +export function minifySync(src: string, options: any): string +export function bundle(options: any): Promise +export function parse(src: string, options: any): any diff --git a/packages/next/build/swc/index.js b/packages/next/build/swc/index.js index 8998782147c3..0f7dc5466a1c 100644 --- a/packages/next/build/swc/index.js +++ b/packages/next/build/swc/index.js @@ -1,6 +1,7 @@ import { platform, arch } from 'os' import { platformArchTriples } from 'next/dist/compiled/@napi-rs/triples' import * as Log from '../output/log' +import { getParserOptions } from './options' const ArchName = arch() const PlatformName = platform() @@ -229,5 +230,6 @@ export async function bundle(options) { export async function parse(src, options) { let bindings = loadBindingsSync() - return bindings.parse(src, options).then((astStr) => JSON.parse(astStr)) + let parserOptions = getParserOptions(options) + return bindings.parse(src, parserOptions).then((astStr) => JSON.parse(astStr)) } diff --git a/packages/next/build/swc/options.d.ts b/packages/next/build/swc/options.d.ts new file mode 100644 index 000000000000..35c16b98e565 --- /dev/null +++ b/packages/next/build/swc/options.d.ts @@ -0,0 +1,7 @@ +export function getParserOptions(options: { + filename: string + jsConfig?: any + [key: string]: any +}): any +export function getJestSWCOptions(...args: any[]): any +export function getLoaderSWCOptions(...args: any[]): any diff --git a/packages/next/build/swc/options.js b/packages/next/build/swc/options.js index 2d2cbee22fa8..1d763b27d11f 100644 --- a/packages/next/build/swc/options.js +++ b/packages/next/build/swc/options.js @@ -5,7 +5,23 @@ const regeneratorRuntimePath = require.resolve( 'next/dist/compiled/regenerator-runtime' ) -export function getBaseSWCOptions({ +export function getParserOptions({ filename, jsConfig, ...rest }) { + const isTSFile = filename.endsWith('.ts') + const isTypeScript = isTSFile || filename.endsWith('.tsx') + const enableDecorators = Boolean( + jsConfig?.compilerOptions?.experimentalDecorators + ) + return { + ...rest, + syntax: isTypeScript ? 'typescript' : 'ecmascript', + dynamicImport: true, + decorators: enableDecorators, + // 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, + } +} + +function getBaseSWCOptions({ filename, jest, development, @@ -15,8 +31,7 @@ export function getBaseSWCOptions({ resolvedBaseUrl, jsConfig, }) { - const isTSFile = filename.endsWith('.ts') - const isTypeScript = isTSFile || filename.endsWith('.tsx') + const parserConfig = getParserOptions({ filename, jsConfig }) const paths = jsConfig?.compilerOptions?.paths const enableDecorators = Boolean( jsConfig?.compilerOptions?.experimentalDecorators @@ -32,13 +47,7 @@ export function getBaseSWCOptions({ paths, } : {}), - parser: { - syntax: isTypeScript ? 'typescript' : 'ecmascript', - dynamicImport: true, - decorators: enableDecorators, - // 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, - }, + parser: parserConfig, transform: { // Enables https://github.com/swc-project/swc/blob/0359deb4841be743d73db4536d4a22ac797d7f65/crates/swc_ecma_ext_transforms/src/jest.rs diff --git a/packages/next/build/webpack/loaders/next-flight-client-loader.ts b/packages/next/build/webpack/loaders/next-flight-client-loader.ts index c7cc6c4badea..afe614e85906 100644 --- a/packages/next/build/webpack/loaders/next-flight-client-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-client-loader.ts @@ -5,11 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -// TODO: add ts support for next-swc api -// @ts-ignore import { parse } from '../../swc' -// @ts-ignore -import { getBaseSWCOptions } from '../../swc/options' function addExportNames(names: string[], node: any) { switch (node.type) { @@ -48,13 +44,8 @@ async function parseExportNamesInto( transformedSource: string, names: Array ): Promise { - const opts = getBaseSWCOptions({ - filename: resourcePath, - globalWindow: true, - }) - const { body } = await parse(transformedSource, { - ...opts.jsc.parser, + filename: resourcePath, isModule: true, }) for (let i = 0; i < body.length; i++) { diff --git a/packages/next/build/webpack/loaders/next-flight-server-loader.ts b/packages/next/build/webpack/loaders/next-flight-server-loader.ts index fdaf07b21277..25b23bf5d47c 100644 --- a/packages/next/build/webpack/loaders/next-flight-server-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-server-loader.ts @@ -1,8 +1,4 @@ -// TODO: add ts support for next-swc api -// @ts-ignore import { parse } from '../../swc' -// @ts-ignore -import { getBaseSWCOptions } from '../../swc/options' import { getRawPageExtensions } from '../../utils' const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'avif'] @@ -46,11 +42,7 @@ async function parseImportsInfo({ source: string defaultExportName: string }> { - const opts = getBaseSWCOptions({ - filename: resourcePath, - globalWindow: isClientCompilation, - }) - const ast = await parse(source, { ...opts.jsc.parser, isModule: true }) + const ast = await parse(source, { filename: resourcePath, isModule: true }) const { body } = ast let transformedSource = '' let lastIndex = 0 diff --git a/test/integration/react-18/app/pages/index.js b/test/integration/react-18/app/pages/index.js index beef70aff7f4..9ce5416ab738 100644 --- a/test/integration/react-18/app/pages/index.js +++ b/test/integration/react-18/app/pages/index.js @@ -19,3 +19,7 @@ export default function Index() {
) } + +export const config = { + runtime: 'edge', +} diff --git a/test/unit/fixtures/page-runtime/edge.js b/test/unit/fixtures/page-runtime/edge.js new file mode 100644 index 000000000000..b5d1e0278d2c --- /dev/null +++ b/test/unit/fixtures/page-runtime/edge.js @@ -0,0 +1,8 @@ +export default function Edge() { + return 'edge' +} + +export const config = { + runtime: 'edge', + amp: false, +} diff --git a/test/unit/fixtures/page-runtime/nodejs.js b/test/unit/fixtures/page-runtime/nodejs.js new file mode 100644 index 000000000000..e2e31db3a9fd --- /dev/null +++ b/test/unit/fixtures/page-runtime/nodejs.js @@ -0,0 +1,8 @@ +export default function Nodejs() { + return 'nodejs' +} + +export const config = { + amp: false, + runtime: 'nodejs', +} diff --git a/test/unit/fixtures/page-runtime/static.js b/test/unit/fixtures/page-runtime/static.js new file mode 100644 index 000000000000..8e8c073a82fc --- /dev/null +++ b/test/unit/fixtures/page-runtime/static.js @@ -0,0 +1,3 @@ +export default function Static() { + return 'static' +} diff --git a/test/unit/parse-page-runtime.test.ts b/test/unit/parse-page-runtime.test.ts new file mode 100644 index 000000000000..701363b27a3f --- /dev/null +++ b/test/unit/parse-page-runtime.test.ts @@ -0,0 +1,27 @@ +import { getPageRuntime } from 'next/dist/build/entries' +import { join } from 'path' + +const fixtureDir = join(__dirname, 'fixtures') + +describe('parse page runtime config', () => { + it('should parse nodejs runtime correctly', async () => { + const runtime = await getPageRuntime( + join(fixtureDir, 'page-runtime/nodejs.js') + ) + expect(runtime).toBe('nodejs') + }) + + it('should parse edge runtime correctly', async () => { + const runtime = await getPageRuntime( + join(fixtureDir, 'page-runtime/edge.js') + ) + expect(runtime).toBe('edge') + }) + + it('should return undefined if no runtime is specified', async () => { + const runtime = await getPageRuntime( + join(fixtureDir, 'page-runtime/static.js') + ) + expect(runtime).toBe(undefined) + }) +})