From d5f6c11ab0f85fdc0acf78fcdb3459ee945c0ad0 Mon Sep 17 00:00:00 2001 From: Allen Zhou <46854522+allenzhou101@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:51:11 -0700 Subject: [PATCH 1/8] [docs] fix rewrites example wording (#81985) This PR fixes a small grammatical error in the Next.js rewrites documentation. --------- Co-authored-by: JJ Kasper --- .../03-api-reference/05-config/01-next-config-js/rewrites.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/01-app/03-api-reference/05-config/01-next-config-js/rewrites.mdx b/docs/01-app/03-api-reference/05-config/01-next-config-js/rewrites.mdx index 0c288f6da044e..c74b012b1585b 100644 --- a/docs/01-app/03-api-reference/05-config/01-next-config-js/rewrites.mdx +++ b/docs/01-app/03-api-reference/05-config/01-next-config-js/rewrites.mdx @@ -34,7 +34,7 @@ module.exports = { } ``` -Rewrites are applied to client-side routing, a `` will have the rewrite applied in the above example. +Rewrites are applied to client-side routing. In the example above, navigating to `` will serve content from `/` while keeping the URL as `/about`. `rewrites` is an async function that expects to return either an array or an object of arrays (see below) holding objects with `source` and `destination` properties: From 08ee5f678fe57860ad02afc8e349200ecf6be958 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Wed, 23 Jul 2025 14:36:43 -0700 Subject: [PATCH 2/8] Fix dynamicParams false layout case in dev (#81990) This ensures we don't consider a path as ISR in development when it will be dynamic during a build due to no `generateStaticParams` being fully generated for a path. Fixes: https://github.com/vercel/next.js/issues/81976 Closes: NEXT-4650 --- packages/next/src/build/templates/app-page.ts | 2 +- .../next/src/build/templates/app-route.ts | 2 +- .../next/src/server/dev/next-dev-server.ts | 7 +- .../route-modules/pages/pages-handler.ts | 30 ++++--- .../e2e/app-dir/app-static/app-static.test.ts | 89 +++++++++++++++++++ .../[locale]/[slug]/page.js | 10 +++ .../partial-params-false/[locale]/layout.js | 16 ++++ .../[locale]/static/page.js | 9 ++ 8 files changed, 148 insertions(+), 17 deletions(-) create mode 100644 test/e2e/app-dir/app-static/app/partial-params-false/[locale]/[slug]/page.js create mode 100644 test/e2e/app-dir/app-static/app/partial-params-false/[locale]/layout.js create mode 100644 test/e2e/app-dir/app-static/app/partial-params-false/[locale]/static/page.js diff --git a/packages/next/src/build/templates/app-page.ts b/packages/next/src/build/templates/app-page.ts index 70568be634f43..bc14749d10ab3 100644 --- a/packages/next/src/build/templates/app-page.ts +++ b/packages/next/src/build/templates/app-page.ts @@ -1240,7 +1240,7 @@ export async function handler( } } catch (err) { // if we aren't wrapped by base-server handle here - if (!activeSpan) { + if (!activeSpan && !(err instanceof NoFallbackError)) { await routeModule.onRequestError( req, err, diff --git a/packages/next/src/build/templates/app-route.ts b/packages/next/src/build/templates/app-route.ts index a3ccf4b88f0b9..229d897844f5a 100644 --- a/packages/next/src/build/templates/app-route.ts +++ b/packages/next/src/build/templates/app-route.ts @@ -449,7 +449,7 @@ export async function handler( } } catch (err) { // if we aren't wrapped by base-server handle here - if (!activeSpan) { + if (!activeSpan && !(err instanceof NoFallbackError)) { await routeModule.onRequestError(req, err, { routerKind: 'App Router', routePath: normalizedSrcPage, diff --git a/packages/next/src/server/dev/next-dev-server.ts b/packages/next/src/server/dev/next-dev-server.ts index fdba36790f712..e4ad95a12ce64 100644 --- a/packages/next/src/server/dev/next-dev-server.ts +++ b/packages/next/src/server/dev/next-dev-server.ts @@ -894,7 +894,12 @@ export default class DevServer extends Server { fallbackMode: fallback, } - if (res.value?.fallbackMode !== undefined) { + if ( + res.value?.fallbackMode !== undefined && + // This matches the hasGenerateStaticParams logic + // we do during build + (!isAppPath || (staticPaths && staticPaths.length > 0)) + ) { // we write the static paths to partial manifest for // fallback handling inside of entry handler's const rawExistingManifest = await fs.promises.readFile( diff --git a/packages/next/src/server/route-modules/pages/pages-handler.ts b/packages/next/src/server/route-modules/pages/pages-handler.ts index e888bfc9984c7..83af0ec60ef01 100644 --- a/packages/next/src/server/route-modules/pages/pages-handler.ts +++ b/packages/next/src/server/route-modules/pages/pages-handler.ts @@ -745,20 +745,22 @@ export const getHandler = ({ ) } } catch (err) { - await routeModule.onRequestError( - req, - err, - { - routerKind: 'Pages Router', - routePath: srcPage, - routeType: 'render', - revalidateReason: getRevalidateReason({ - isRevalidate: hasStaticProps, - isOnDemandRevalidate, - }), - }, - routerServerContext - ) + if (!(err instanceof NoFallbackError)) { + await routeModule.onRequestError( + req, + err, + { + routerKind: 'Pages Router', + routePath: srcPage, + routeType: 'render', + revalidateReason: getRevalidateReason({ + isRevalidate: hasStaticProps, + isOnDemandRevalidate, + }), + }, + routerServerContext + ) + } // rethrow so that we can handle serving error page throw err diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index 8e81f823064cf..3f595697eb07a 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -40,6 +40,18 @@ describe('app-dir static/dynamic handling', () => { } }) + if (!process.env.__NEXT_EXPERIMENTAL_PPR) { + it('should respond correctly for dynamic route with dynamicParams false in layout', async () => { + const res = await next.fetch('/partial-params-false/en/another') + expect(res.status).toBe(200) + }) + + it('should respond correctly for partially dynamic route with dynamicParams false in layout', async () => { + const res = await next.fetch('/partial-params-false/en/static') + expect(res.status).toBe(200) + }) + } + it('should use auto no cache when no fetch config', async () => { const res = await next.fetch('/no-config-fetch') expect(res.status).toBe(200) @@ -877,6 +889,10 @@ describe('app-dir static/dynamic handling', () => { "partial-gen-params-no-additional-slug/fr/first.rsc", "partial-gen-params-no-additional-slug/fr/second.html", "partial-gen-params-no-additional-slug/fr/second.rsc", + "partial-params-false/en/static.html", + "partial-params-false/en/static.rsc", + "partial-params-false/fr/static.html", + "partial-params-false/fr/static.rsc", "prerendered-not-found/first.html", "prerendered-not-found/first.rsc", "prerendered-not-found/second.html", @@ -1822,6 +1838,54 @@ describe('app-dir static/dynamic handling', () => { "initialRevalidateSeconds": false, "srcRoute": "/partial-gen-params-no-additional-slug/[lang]/[slug]", }, + "/partial-params-false/en/static": { + "allowHeader": [ + "host", + "x-matched-path", + "x-prerender-revalidate", + "x-prerender-revalidate-if-generated", + "x-next-revalidated-tags", + "x-next-revalidate-tag-token", + ], + "dataRoute": "/partial-params-false/en/static.rsc", + "experimentalBypassFor": [ + { + "key": "Next-Action", + "type": "header", + }, + { + "key": "content-type", + "type": "header", + "value": "multipart/form-data;.*", + }, + ], + "initialRevalidateSeconds": false, + "srcRoute": "/partial-params-false/[locale]/static", + }, + "/partial-params-false/fr/static": { + "allowHeader": [ + "host", + "x-matched-path", + "x-prerender-revalidate", + "x-prerender-revalidate-if-generated", + "x-next-revalidated-tags", + "x-next-revalidate-tag-token", + ], + "dataRoute": "/partial-params-false/fr/static.rsc", + "experimentalBypassFor": [ + { + "key": "Next-Action", + "type": "header", + }, + { + "key": "content-type", + "type": "header", + "value": "multipart/form-data;.*", + }, + ], + "initialRevalidateSeconds": false, + "srcRoute": "/partial-params-false/[locale]/static", + }, "/prerendered-not-found/first": { "allowHeader": [ "host", @@ -2580,6 +2644,31 @@ describe('app-dir static/dynamic handling', () => { "fallback": false, "routeRegex": "^\\/partial\\-gen\\-params\\-no\\-additional\\-slug\\/([^\\/]+?)\\/([^\\/]+?)(?:\\/)?$", }, + "/partial-params-false/[locale]/static": { + "allowHeader": [ + "host", + "x-matched-path", + "x-prerender-revalidate", + "x-prerender-revalidate-if-generated", + "x-next-revalidated-tags", + "x-next-revalidate-tag-token", + ], + "dataRoute": "/partial-params-false/[locale]/static.rsc", + "dataRouteRegex": "^\\/partial\\-params\\-false\\/([^\\/]+?)\\/static\\.rsc$", + "experimentalBypassFor": [ + { + "key": "Next-Action", + "type": "header", + }, + { + "key": "content-type", + "type": "header", + "value": "multipart/form-data;.*", + }, + ], + "fallback": false, + "routeRegex": "^\\/partial\\-params\\-false\\/([^\\/]+?)\\/static(?:\\/)?$", + }, "/prerendered-not-found/[slug]": { "allowHeader": [ "host", diff --git a/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/[slug]/page.js b/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/[slug]/page.js new file mode 100644 index 0000000000000..7ab100431b35e --- /dev/null +++ b/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/[slug]/page.js @@ -0,0 +1,10 @@ +export default async function Page({ params }) { + const { locale, slug } = await params + + return ( + <> +

/[locale]/[slug]/page

+

params: {JSON.stringify({ locale, slug })}

+ + ) +} diff --git a/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/layout.js b/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/layout.js new file mode 100644 index 0000000000000..e9b0920c6fa56 --- /dev/null +++ b/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/layout.js @@ -0,0 +1,16 @@ +export const dynamicParams = false + +export function generateStaticParams() { + return [ + { + locale: 'en', + }, + { + locale: 'fr', + }, + ] +} + +export default function Layout({ children }) { + return <>{children} +} diff --git a/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/static/page.js b/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/static/page.js new file mode 100644 index 0000000000000..7cd2718329aee --- /dev/null +++ b/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/static/page.js @@ -0,0 +1,9 @@ +export default async function Page({ params }) { + const { locale } = await params + return ( + <> +

/[locale]/static

+

locale: {locale}

+ + ) +} From 94496b8669107d10994c10452572f042f78cdf69 Mon Sep 17 00:00:00 2001 From: nextjs-bot Date: Wed, 23 Jul 2025 23:25:01 +0000 Subject: [PATCH 3/8] v15.4.2-canary.15 --- lerna.json | 2 +- packages/create-next-app/package.json | 2 +- packages/eslint-config-next/package.json | 4 ++-- packages/eslint-plugin-internal/package.json | 2 +- packages/eslint-plugin-next/package.json | 2 +- packages/font/package.json | 2 +- packages/next-bundle-analyzer/package.json | 2 +- packages/next-codemod/package.json | 2 +- packages/next-env/package.json | 2 +- packages/next-mdx/package.json | 2 +- packages/next-plugin-storybook/package.json | 2 +- packages/next-polyfill-module/package.json | 2 +- packages/next-polyfill-nomodule/package.json | 2 +- packages/next-rspack/package.json | 2 +- packages/next-swc/package.json | 2 +- packages/next/package.json | 14 +++++++------- packages/react-refresh-utils/package.json | 2 +- packages/third-parties/package.json | 4 ++-- pnpm-lock.yaml | 16 ++++++++-------- 19 files changed, 34 insertions(+), 34 deletions(-) diff --git a/lerna.json b/lerna.json index 5aeaa6ecaa66b..c80f060354ff7 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "15.4.2-canary.14" + "version": "15.4.2-canary.15" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 8db54a69c1a2f..cfab3190c8b3c 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index af7cc65e78b65..143d1b316136f 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "description": "ESLint configuration used by Next.js.", "main": "index.js", "license": "MIT", @@ -10,7 +10,7 @@ }, "homepage": "https://nextjs.org/docs/app/api-reference/config/eslint", "dependencies": { - "@next/eslint-plugin-next": "15.4.2-canary.14", + "@next/eslint-plugin-next": "15.4.2-canary.15", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index e9103bcd195a8..0bb57fdeb72b0 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,7 +1,7 @@ { "name": "@next/eslint-plugin-internal", "private": true, - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "description": "ESLint plugin for working on Next.js.", "exports": { ".": "./src/eslint-plugin-internal.js" diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 408708f691ece..87cdab5209eab 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/font/package.json b/packages/font/package.json index ef3b97c51598f..fe8c202074a30 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,7 +1,7 @@ { "name": "@next/font", "private": true, - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index e44d19d0d36a9..12088f1ec46a3 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index e4eab880cb02f..d4350875fd5c5 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 4d5801bf58cc2..08b9e0a754dbe 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 5fdf95c80e075..a76f4b5890438 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 4c3c4c52279f3..b41af01f97c78 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 84db44405c6a8..4636d2e4e71ce 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 31ede27d7e88a..3ecf45b1f2f57 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-rspack/package.json b/packages/next-rspack/package.json index 893d77a9ed7c5..e13631ab0ac54 100644 --- a/packages/next-rspack/package.json +++ b/packages/next-rspack/package.json @@ -1,6 +1,6 @@ { "name": "next-rspack", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "repository": { "url": "vercel/next.js", "directory": "packages/next-rspack" diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index b2f4add9e6b38..7b39bfd37a21a 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "private": true, "files": [ "native/" diff --git a/packages/next/package.json b/packages/next/package.json index 002bd78debb25..4bf2eb78eeb35 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -100,7 +100,7 @@ ] }, "dependencies": { - "@next/env": "15.4.2-canary.14", + "@next/env": "15.4.2-canary.15", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -163,11 +163,11 @@ "@jest/types": "29.5.0", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", - "@next/font": "15.4.2-canary.14", - "@next/polyfill-module": "15.4.2-canary.14", - "@next/polyfill-nomodule": "15.4.2-canary.14", - "@next/react-refresh-utils": "15.4.2-canary.14", - "@next/swc": "15.4.2-canary.14", + "@next/font": "15.4.2-canary.15", + "@next/polyfill-module": "15.4.2-canary.15", + "@next/polyfill-nomodule": "15.4.2-canary.15", + "@next/react-refresh-utils": "15.4.2-canary.15", + "@next/swc": "15.4.2-canary.15", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.51.1", "@rspack/core": "1.4.5", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 7a7fea01b1a85..71f42b95eedae 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index d107f7e440b1d..579e252147a8d 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "15.4.2-canary.14", + "next": "15.4.2-canary.15", "outdent": "0.8.0", "prettier": "2.5.1", "typescript": "5.8.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8eaf5dd7e35d5..bba0fd43a895c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -854,7 +854,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 15.4.2-canary.14 + specifier: 15.4.2-canary.15 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.10.3 @@ -924,7 +924,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 15.4.2-canary.14 + specifier: 15.4.2-canary.15 version: link:../next-env '@swc/helpers': specifier: 0.5.15 @@ -1046,19 +1046,19 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/font': - specifier: 15.4.2-canary.14 + specifier: 15.4.2-canary.15 version: link:../font '@next/polyfill-module': - specifier: 15.4.2-canary.14 + specifier: 15.4.2-canary.15 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 15.4.2-canary.14 + specifier: 15.4.2-canary.15 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 15.4.2-canary.14 + specifier: 15.4.2-canary.15 version: link:../react-refresh-utils '@next/swc': - specifier: 15.4.2-canary.14 + specifier: 15.4.2-canary.15 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1758,7 +1758,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 15.4.2-canary.14 + specifier: 15.4.2-canary.15 version: link:../next outdent: specifier: 0.8.0 From b7a97a059877c525ebac41e5d0647e71e5dbb44c Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 24 Jul 2025 01:46:38 +0200 Subject: [PATCH 4/8] Initial MCP implementation (#81770) ### What? Add MCP for next dev exposing entrypoints, module graph and issues. Can be enabled via `NEXT_EXPERIMENTAL_MCP_SECRET=` and makes the MCP available on `/_next/mcp?`. Interesting change is in: packages/next/src/server/lib/router-utils/mcp.ts --- crates/napi/src/next_api/endpoint.rs | 116 +++- crates/napi/src/next_api/mod.rs | 1 + crates/napi/src/next_api/module_graph.rs | 98 +++ crates/napi/src/next_api/project.rs | 105 +++- crates/next-api/src/app.rs | 26 +- crates/next-api/src/empty.rs | 7 +- crates/next-api/src/instrumentation.rs | 42 +- crates/next-api/src/lib.rs | 1 + crates/next-api/src/middleware.rs | 13 +- crates/next-api/src/module_graph_snapshot.rs | 193 ++++++ crates/next-api/src/pages.rs | 9 +- crates/next-api/src/project.rs | 10 +- crates/next-api/src/route.rs | 5 + packages/next/errors.json | 5 +- packages/next/package.json | 5 + .../next/src/build/swc/generated-native.d.ts | 33 + packages/next/src/build/swc/index.ts | 22 + packages/next/src/build/swc/types.ts | 10 + .../next/src/server/lib/router-utils/mcp.ts | 584 ++++++++++++++++++ .../lib/router-utils/setup-dev-bundler.ts | 107 ++++ .../next/src/shared/lib/turbopack/utils.ts | 65 +- pnpm-lock.yaml | 416 +++++++++++++ .../turbopack-core/src/module_graph/mod.rs | 17 + 23 files changed, 1820 insertions(+), 70 deletions(-) create mode 100644 crates/napi/src/next_api/module_graph.rs create mode 100644 crates/next-api/src/module_graph_snapshot.rs create mode 100644 packages/next/src/server/lib/router-utils/mcp.ts diff --git a/crates/napi/src/next_api/endpoint.rs b/crates/napi/src/next_api/endpoint.rs index 01f7cba26e57f..97d02507fb92e 100644 --- a/crates/napi/src/next_api/endpoint.rs +++ b/crates/napi/src/next_api/endpoint.rs @@ -4,21 +4,25 @@ use anyhow::Result; use futures_util::TryFutureExt; use napi::{JsFunction, bindgen_prelude::External}; use next_api::{ + module_graph_snapshot::{ModuleGraphSnapshot, get_module_graph_snapshot}, operation::OptionEndpoint, paths::ServerPath, route::{ - EndpointOutputPaths, endpoint_client_changed_operation, endpoint_server_changed_operation, - endpoint_write_to_disk_operation, + Endpoint, EndpointOutputPaths, endpoint_client_changed_operation, + endpoint_server_changed_operation, endpoint_write_to_disk_operation, }, }; use tracing::Instrument; -use turbo_tasks::{Completion, Effects, OperationVc, ReadRef, Vc}; -use turbopack_core::{diagnostics::PlainDiagnostic, issue::PlainIssue}; +use turbo_tasks::{ + Completion, Effects, OperationVc, ReadRef, TryFlatJoinIterExt, TryJoinIterExt, Vc, +}; +use turbopack_core::{diagnostics::PlainDiagnostic, error::PrettyPrintError, issue::PlainIssue}; use super::utils::{ DetachedVc, NapiDiagnostic, NapiIssue, RootTask, TurbopackResult, strongly_consistent_catch_collectables, subscribe, }; +use crate::next_api::module_graph::NapiModuleGraphSnapshot; #[napi(object)] #[derive(Default)] @@ -81,6 +85,11 @@ impl From> for NapiWrittenEndpoint { } } +#[napi(object)] +pub struct NapiModuleGraphSnapshots { + pub module_graphs: Vec, +} + // NOTE(alexkirsz) We go through an extra layer of indirection here because of // two factors: // 1. rustc currently has a bug where using a dyn trait as a type argument to @@ -155,6 +164,105 @@ pub async fn endpoint_write_to_disk( }) } +#[turbo_tasks::value(serialization = "none")] +struct ModuleGraphsWithIssues { + module_graphs: Option>, + issues: Arc>>, + diagnostics: Arc>>, + effects: Arc, +} + +#[turbo_tasks::function(operation)] +async fn get_module_graphs_with_issues_operation( + endpoint_op: OperationVc, +) -> Result> { + let module_graphs_op = get_module_graphs_operation(endpoint_op); + let (module_graphs, issues, diagnostics, effects) = + strongly_consistent_catch_collectables(module_graphs_op).await?; + Ok(ModuleGraphsWithIssues { + module_graphs, + issues, + diagnostics, + effects, + } + .cell()) +} + +#[turbo_tasks::value(transparent)] +struct ModuleGraphSnapshots(Vec>); + +#[turbo_tasks::function(operation)] +async fn get_module_graphs_operation( + endpoint_op: OperationVc, +) -> Result> { + let Some(endpoint) = *endpoint_op.connect().await? else { + return Ok(Vc::cell(vec![])); + }; + let graphs = endpoint.module_graphs().await?; + let entries = endpoint.entries().await?; + let entry_modules = entries.iter().flat_map(|e| e.entries()).collect::>(); + let snapshots = graphs + .iter() + .map(async |&graph| { + let module_graph = graph.await?; + let entry_modules = entry_modules + .iter() + .map(async |&m| Ok(module_graph.has_entry(m).await?.then_some(m))) + .try_flat_join() + .await?; + Ok((*graph, entry_modules)) + }) + .try_join() + .await? + .into_iter() + .map(|(graph, entry_modules)| (graph, Vc::cell(entry_modules))) + .collect::>() + .into_iter() + .map(async |(graph, entry_modules)| { + get_module_graph_snapshot(graph, Some(entry_modules)).await + }) + .try_join() + .await?; + Ok(Vc::cell(snapshots)) +} + +#[napi] +pub async fn endpoint_module_graphs( + #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External, +) -> napi::Result> { + let endpoint_op: OperationVc = ***endpoint; + let (module_graphs, issues, diagnostics) = endpoint + .turbopack_ctx() + .turbo_tasks() + .run_once(async move { + let module_graphs_op = get_module_graphs_with_issues_operation(endpoint_op); + let ModuleGraphsWithIssues { + module_graphs, + issues, + diagnostics, + effects: _, + } = &*module_graphs_op.connect().await?; + Ok((module_graphs.clone(), issues.clone(), diagnostics.clone())) + }) + .await + .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?; + + Ok(TurbopackResult { + result: NapiModuleGraphSnapshots { + module_graphs: module_graphs + .into_iter() + .flat_map(|m| m.into_iter()) + .map(|m| NapiModuleGraphSnapshot::from(&**m)) + .collect(), + }, + issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(), + diagnostics: diagnostics + .iter() + .map(|d| NapiDiagnostic::from(d)) + .collect(), + }) +} + #[napi(ts_return_type = "{ __napiType: \"RootTask\" }")] pub fn endpoint_server_changed_subscribe( #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External, diff --git a/crates/napi/src/next_api/mod.rs b/crates/napi/src/next_api/mod.rs index 4897b2d3bf9ba..d8a1b0ab8cd26 100644 --- a/crates/napi/src/next_api/mod.rs +++ b/crates/napi/src/next_api/mod.rs @@ -1,4 +1,5 @@ pub mod endpoint; +pub mod module_graph; pub mod project; pub mod turbopack_ctx; pub mod utils; diff --git a/crates/napi/src/next_api/module_graph.rs b/crates/napi/src/next_api/module_graph.rs new file mode 100644 index 0000000000000..606aa8186a10d --- /dev/null +++ b/crates/napi/src/next_api/module_graph.rs @@ -0,0 +1,98 @@ +use next_api::module_graph_snapshot::{ModuleGraphSnapshot, ModuleInfo, ModuleReference}; +use turbo_rcstr::RcStr; +use turbopack_core::chunk::ChunkingType; + +#[napi(object)] +pub struct NapiModuleReference { + /// The index of the referenced/referencing module in the modules list. + pub index: u32, + /// The export used in the module reference. + pub export: String, + /// The type of chunking for the module reference. + pub chunking_type: String, +} + +impl From<&ModuleReference> for NapiModuleReference { + fn from(reference: &ModuleReference) -> Self { + Self { + index: reference.index as u32, + export: reference.export.to_string(), + chunking_type: match &reference.chunking_type { + ChunkingType::Parallel { hoisted: true, .. } => "hoisted".to_string(), + ChunkingType::Parallel { hoisted: false, .. } => "sync".to_string(), + ChunkingType::Async => "async".to_string(), + ChunkingType::Isolated { + merge_tag: None, .. + } => "isolated".to_string(), + ChunkingType::Isolated { + merge_tag: Some(name), + .. + } => format!("isolated {name}"), + ChunkingType::Shared { + merge_tag: None, .. + } => "shared".to_string(), + ChunkingType::Shared { + merge_tag: Some(name), + .. + } => format!("shared {name}"), + ChunkingType::Traced => "traced".to_string(), + }, + } + } +} + +#[napi(object)] +pub struct NapiModuleInfo { + pub ident: RcStr, + pub path: RcStr, + pub depth: u32, + pub size: u32, + pub retained_size: u32, + pub references: Vec, + pub incoming_references: Vec, +} + +impl From<&ModuleInfo> for NapiModuleInfo { + fn from(info: &ModuleInfo) -> Self { + Self { + ident: info.ident.clone(), + path: info.path.clone(), + depth: info.depth, + size: info.size, + retained_size: info.retained_size, + references: info + .references + .iter() + .map(NapiModuleReference::from) + .collect(), + incoming_references: info + .incoming_references + .iter() + .map(NapiModuleReference::from) + .collect(), + } + } +} + +#[napi(object)] +#[derive(Default)] +pub struct NapiModuleGraphSnapshot { + pub modules: Vec, + pub entries: Vec, +} + +impl From<&ModuleGraphSnapshot> for NapiModuleGraphSnapshot { + fn from(snapshot: &ModuleGraphSnapshot) -> Self { + Self { + modules: snapshot.modules.iter().map(NapiModuleInfo::from).collect(), + entries: snapshot + .entries + .iter() + .map(|&i| { + // If you have more that 4294967295 entries, you probably have other problems... + i.try_into().unwrap() + }) + .collect(), + } + } +} diff --git a/crates/napi/src/next_api/project.rs b/crates/napi/src/next_api/project.rs index cd4eecf7a093d..63e4560d60ce0 100644 --- a/crates/napi/src/next_api/project.rs +++ b/crates/napi/src/next_api/project.rs @@ -9,6 +9,7 @@ use napi::{ }; use next_api::{ entrypoints::Entrypoints, + module_graph_snapshot::{ModuleGraphSnapshot, get_module_graph_snapshot}, operation::{ EntrypointsOperation, InstrumentationOperation, MiddlewareOperation, OptionEndpoint, RouteOperation, @@ -63,13 +64,14 @@ use url::Url; use crate::{ next_api::{ endpoint::ExternalEndpoint, + module_graph::NapiModuleGraphSnapshot, turbopack_ctx::{ NapiNextTurbopackCallbacks, NapiNextTurbopackCallbacksJsObject, NextTurboTasks, NextTurbopackContext, create_turbo_tasks, }, utils::{ DetachedVc, NapiDiagnostic, NapiIssue, RootTask, TurbopackResult, get_diagnostics, - get_issues, subscribe, + get_issues, strongly_consistent_catch_collectables, subscribe, }, }, register, @@ -987,6 +989,40 @@ async fn output_assets_operation( Ok(Vc::cell(output_assets.into_iter().collect())) } +#[napi] +pub async fn project_entrypoints( + #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External, +) -> napi::Result> { + let container = project.container; + + let (entrypoints, issues, diags) = project + .turbopack_ctx + .turbo_tasks() + .run_once(async move { + let entrypoints_with_issues_op = get_entrypoints_with_issues_operation(container); + + // Read and compile the files + let EntrypointsWithIssues { + entrypoints, + issues, + diagnostics, + effects: _, + } = &*entrypoints_with_issues_op + .read_strongly_consistent() + .await?; + + Ok((entrypoints.clone(), issues.clone(), diagnostics.clone())) + }) + .await + .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?; + + Ok(TurbopackResult { + result: NapiEntrypoints::from_entrypoints_op(&entrypoints, &project.turbopack_ctx)?, + issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(), + diagnostics: diags.iter().map(|d| NapiDiagnostic::from(d)).collect(), + }) +} + #[napi(ts_return_type = "{ __napiType: \"RootTask\" }")] pub fn project_entrypoints_subscribe( #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External, @@ -1650,3 +1686,70 @@ pub fn project_get_source_map_sync( tokio::runtime::Handle::current().block_on(project_get_source_map(project, file_path)) }) } + +#[napi] +pub async fn project_module_graph( + #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External, +) -> napi::Result> { + let container = project.container; + let (module_graph, issues, diagnostics) = project + .turbopack_ctx + .turbo_tasks() + .run_once(async move { + let module_graph_op = get_module_graph_with_issues_operation(container); + let ModuleGraphWithIssues { + module_graph, + issues, + diagnostics, + effects: _, + } = &*module_graph_op.connect().await?; + Ok((module_graph.clone(), issues.clone(), diagnostics.clone())) + }) + .await + .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?; + + Ok(TurbopackResult { + result: module_graph.map_or_else(NapiModuleGraphSnapshot::default, |m| { + NapiModuleGraphSnapshot::from(&*m) + }), + issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(), + diagnostics: diagnostics + .iter() + .map(|d| NapiDiagnostic::from(d)) + .collect(), + }) +} + +#[turbo_tasks::value(serialization = "none")] +struct ModuleGraphWithIssues { + module_graph: Option>, + issues: Arc>>, + diagnostics: Arc>>, + effects: Arc, +} + +#[turbo_tasks::function(operation)] +async fn get_module_graph_with_issues_operation( + project: ResolvedVc, +) -> Result> { + let module_graph_op = get_module_graph_operation(project); + let (module_graph, issues, diagnostics, effects) = + strongly_consistent_catch_collectables(module_graph_op).await?; + Ok(ModuleGraphWithIssues { + module_graph, + issues, + diagnostics, + effects, + } + .cell()) +} + +#[turbo_tasks::function(operation)] +async fn get_module_graph_operation( + project: ResolvedVc, +) -> Result> { + let project = project.project(); + let graph = project.whole_app_module_graphs().await?.full; + let snapshot = get_module_graph_snapshot(*graph, None).resolve().await?; + Ok(snapshot) +} diff --git a/crates/next-api/src/app.rs b/crates/next-api/src/app.rs index 7f021e40be104..093cb5a83b28e 100644 --- a/crates/next-api/src/app.rs +++ b/crates/next-api/src/app.rs @@ -82,8 +82,10 @@ use crate::{ all_paths_in_root, all_server_paths, get_asset_paths_from_root, get_js_paths_from_root, get_wasm_paths_from_root, paths_to_bindings, wasm_paths_to_bindings, }, - project::{ModuleGraphs, Project}, - route::{AppPageRoute, Endpoint, EndpointOutput, EndpointOutputPaths, Route, Routes}, + project::{BaseAndFullModuleGraph, Project}, + route::{ + AppPageRoute, Endpoint, EndpointOutput, EndpointOutputPaths, ModuleGraphs, Route, Routes, + }, server_actions::{build_server_actions_loader, create_server_actions_manifest}, webpack_stats::generate_webpack_stats, }; @@ -859,7 +861,7 @@ impl AppProject { rsc_entry: ResolvedVc>, client_shared_entries: Vc, has_layout_segments: bool, - ) -> Result> { + ) -> Result> { if *self.project.per_page_module_graph().await? { let should_trace = self.project.next_mode().await?.is_production(); let client_shared_entries = client_shared_entries @@ -956,7 +958,7 @@ impl AppProject { graphs.push(additional_module_graph); let full = ModuleGraph::from_graphs(graphs); - Ok(ModuleGraphs { + Ok(BaseAndFullModuleGraph { base: base.to_resolved().await?, full: full.to_resolved().await?, } @@ -2071,6 +2073,22 @@ impl Endpoint for AppEndpoint { server_actions_loader, ])])) } + + #[turbo_tasks::function] + async fn module_graphs(self: Vc) -> Result> { + let this = self.await?; + let app_entry = self.app_endpoint_entry().await?; + let module_graphs = this + .app_project + .app_module_graphs( + self, + *app_entry.rsc_entry, + this.app_project.client_runtime_entries(), + matches!(this.ty, AppEndpointType::Page { .. }), + ) + .await?; + Ok(Vc::cell(vec![module_graphs.full])) + } } #[turbo_tasks::value] diff --git a/crates/next-api/src/empty.rs b/crates/next-api/src/empty.rs index 7174aa69a1ca1..cc220d178458e 100644 --- a/crates/next-api/src/empty.rs +++ b/crates/next-api/src/empty.rs @@ -2,7 +2,7 @@ use anyhow::{Result, bail}; use turbo_tasks::{Completion, Vc}; use turbopack_core::module_graph::GraphEntries; -use crate::route::{Endpoint, EndpointOutput}; +use crate::route::{Endpoint, EndpointOutput, ModuleGraphs}; #[turbo_tasks::value] pub struct EmptyEndpoint; @@ -36,4 +36,9 @@ impl Endpoint for EmptyEndpoint { fn entries(self: Vc) -> Vc { GraphEntries::empty() } + + #[turbo_tasks::function] + fn module_graphs(self: Vc) -> Vc { + Vc::cell(vec![]) + } } diff --git a/crates/next-api/src/instrumentation.rs b/crates/next-api/src/instrumentation.rs index e5bcb45112614..106c955a76569 100644 --- a/crates/next-api/src/instrumentation.rs +++ b/crates/next-api/src/instrumentation.rs @@ -33,7 +33,7 @@ use crate::{ all_server_paths, get_js_paths_from_root, get_wasm_paths_from_root, wasm_paths_to_bindings, }, project::Project, - route::{Endpoint, EndpointOutput, EndpointOutputPaths}, + route::{Endpoint, EndpointOutput, EndpointOutputPaths, ModuleGraphs}, }; #[turbo_tasks::value] @@ -70,7 +70,7 @@ impl InstrumentationEndpoint { } #[turbo_tasks::function] - async fn core_modules(&self) -> Result> { + async fn entry_module(&self) -> Result>> { let userland_module = self .asset_context .process( @@ -81,6 +81,10 @@ impl InstrumentationEndpoint { .to_resolved() .await?; + if !self.is_edge { + return Ok(Vc::upcast(*userland_module)); + } + let edge_entry_module = wrap_edge_entry( *self.asset_context, self.project.project_path().owned().await?, @@ -90,17 +94,13 @@ impl InstrumentationEndpoint { .to_resolved() .await?; - Ok(InstrumentationCoreModules { - userland_module, - edge_entry_module, - } - .cell()) + Ok(Vc::upcast(*edge_entry_module)) } #[turbo_tasks::function] async fn edge_files(self: Vc) -> Result> { let this = self.await?; - let module = self.core_modules().await?.edge_entry_module; + let module = self.entry_module().to_resolved().await?; let module_graph = this.project.module_graph(*module); @@ -137,7 +137,7 @@ impl InstrumentationEndpoint { let chunking_context = this.project.server_chunking_context(false); - let userland_module = self.core_modules().await?.userland_module; + let userland_module = self.entry_module().to_resolved().await?; let module_graph = this.project.module_graph(*userland_module); let Some(module) = ResolvedVc::try_downcast(userland_module) else { @@ -227,12 +227,6 @@ impl InstrumentationEndpoint { } } -#[turbo_tasks::value] -struct InstrumentationCoreModules { - pub userland_module: ResolvedVc>, - pub edge_entry_module: ResolvedVc>, -} - #[turbo_tasks::value_impl] impl Endpoint for InstrumentationEndpoint { #[turbo_tasks::function] @@ -276,13 +270,15 @@ impl Endpoint for InstrumentationEndpoint { #[turbo_tasks::function] async fn entries(self: Vc) -> Result> { - let core_modules = self.core_modules().await?; - Ok(Vc::cell(vec![ChunkGroupEntry::Entry( - if self.await?.is_edge { - vec![core_modules.edge_entry_module] - } else { - vec![core_modules.userland_module] - }, - )])) + let entry_module = self.entry_module().to_resolved().await?; + Ok(Vc::cell(vec![ChunkGroupEntry::Entry(vec![entry_module])])) + } + + #[turbo_tasks::function] + async fn module_graphs(self: Vc) -> Result> { + let this = self.await?; + let module = self.entry_module(); + let module_graph = this.project.module_graph(module).to_resolved().await?; + Ok(Vc::cell(vec![module_graph])) } } diff --git a/crates/next-api/src/lib.rs b/crates/next-api/src/lib.rs index 17360fdb01d21..4db17a756ba88 100644 --- a/crates/next-api/src/lib.rs +++ b/crates/next-api/src/lib.rs @@ -13,6 +13,7 @@ mod instrumentation; mod loadable_manifest; mod middleware; mod module_graph; +pub mod module_graph_snapshot; mod nft_json; pub mod operation; mod pages; diff --git a/crates/next-api/src/middleware.rs b/crates/next-api/src/middleware.rs index ade51e52f8c17..ab89d6ee047b6 100644 --- a/crates/next-api/src/middleware.rs +++ b/crates/next-api/src/middleware.rs @@ -38,7 +38,7 @@ use crate::{ get_wasm_paths_from_root, paths_to_bindings, wasm_paths_to_bindings, }, project::Project, - route::{Endpoint, EndpointOutput, EndpointOutputPaths}, + route::{Endpoint, EndpointOutput, EndpointOutputPaths, ModuleGraphs}, }; #[turbo_tasks::value] @@ -408,4 +408,15 @@ impl Endpoint for MiddlewareEndpoint { self.entry_module().to_resolved().await?, ])])) } + + #[turbo_tasks::function] + async fn module_graphs(self: Vc) -> Result> { + let this = self.await?; + let module_graph = this + .project + .module_graph(self.entry_module()) + .to_resolved() + .await?; + Ok(Vc::cell(vec![module_graph])) + } } diff --git a/crates/next-api/src/module_graph_snapshot.rs b/crates/next-api/src/module_graph_snapshot.rs new file mode 100644 index 0000000000000..dac1bd29502aa --- /dev/null +++ b/crates/next-api/src/module_graph_snapshot.rs @@ -0,0 +1,193 @@ +use std::{cell::RefCell, cmp::Reverse, collections::hash_map::Entry, mem::take}; + +use anyhow::Result; +use either::Either; +use rustc_hash::{FxHashMap, FxHashSet}; +use serde::{Deserialize, Serialize}; +use turbo_rcstr::RcStr; +use turbo_tasks::{ + NonLocalValue, ResolvedVc, TryJoinIterExt, ValueToString, Vc, trace::TraceRawVcs, +}; +use turbopack_core::{ + asset::Asset, + chunk::ChunkingType, + module::{Module, Modules}, + module_graph::{GraphTraversalAction, ModuleGraph}, + resolve::ExportUsage, +}; + +#[derive(PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, Debug)] +pub struct ModuleReference { + pub index: usize, + pub chunking_type: ChunkingType, + pub export: ExportUsage, +} + +#[derive(PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, Debug)] +pub struct ModuleInfo { + pub ident: RcStr, + pub path: RcStr, + pub depth: u32, + pub size: u32, + // TODO this should be per layer + pub retained_size: u32, + pub references: Vec, + pub incoming_references: Vec, +} + +#[turbo_tasks::value] +pub struct ModuleGraphSnapshot { + pub modules: Vec, + pub entries: Vec, +} + +#[turbo_tasks::function] +pub async fn get_module_graph_snapshot( + module_graph: Vc, + entry_modules: Option>, +) -> Result> { + let module_graph = module_graph.await?; + + struct RawModuleInfo { + module: ResolvedVc>, + depth: u32, + retained_modules: RefCell>, + references: Vec, + incoming_references: Vec, + } + + let mut entries = Vec::new(); + let mut modules = Vec::new(); + let mut module_to_index = FxHashMap::default(); + + fn get_or_create_module( + modules: &mut Vec, + module_to_index: &mut FxHashMap>, usize>, + module: ResolvedVc>, + ) -> usize { + match module_to_index.entry(module) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let index = modules.len(); + modules.push(RawModuleInfo { + module, + depth: u32::MAX, + references: Vec::new(), + incoming_references: Vec::new(), + retained_modules: Default::default(), + }); + entry.insert(index); + index + } + } + } + + let entry_modules = if let Some(entry_modules) = entry_modules { + Either::Left(entry_modules.await?) + } else { + Either::Right(module_graph.entries().await?) + }; + module_graph + .traverse_edges_from_entries_bfs(entry_modules.iter().copied(), |parent_info, node| { + let module = node.module; + let module_index = get_or_create_module(&mut modules, &mut module_to_index, module); + + if let Some((parent_module, ty)) = parent_info { + let parent_index = + get_or_create_module(&mut modules, &mut module_to_index, parent_module.module); + let parent_module = &mut modules[parent_index]; + let parent_depth = parent_module.depth; + debug_assert!(parent_depth < u32::MAX); + parent_module.references.push(ModuleReference { + index: module_index, + chunking_type: ty.chunking_type.clone(), + export: ty.export.clone(), + }); + let module = &mut modules[module_index]; + module.depth = module.depth.min(parent_depth + 1); + module.incoming_references.push(ModuleReference { + index: parent_index, + chunking_type: ty.chunking_type.clone(), + export: ty.export.clone(), + }); + } else { + entries.push(module_index); + let module = &mut modules[module_index]; + module.depth = 0; + } + + Ok(GraphTraversalAction::Continue) + }) + .await?; + + let mut modules_by_depth = FxHashMap::default(); + for (index, info) in modules.iter().enumerate() { + modules_by_depth + .entry(info.depth) + .or_insert_with(Vec::new) + .push(index); + } + let mut modules_by_depth = modules_by_depth.into_iter().collect::>(); + modules_by_depth.sort_by_key(|(depth, _)| Reverse(*depth)); + for (depth, module_indicies) in modules_by_depth { + for module_index in module_indicies { + let module = &modules[module_index]; + for ref_info in &module.incoming_references { + let ref_module = &modules[ref_info.index]; + if ref_module.depth < depth { + let mut retained_modules = ref_module.retained_modules.borrow_mut(); + retained_modules.insert(module_index as u32); + for retained in module.retained_modules.borrow().iter() { + retained_modules.insert(*retained); + } + } + } + } + } + + let mut final_modules = modules + .iter_mut() + .map(async |info| { + Ok(ModuleInfo { + ident: info.module.ident().to_string().owned().await?, + path: info.module.ident().path().to_string().owned().await?, + depth: info.depth, + size: info + .module + .content() + .len() + .owned() + .await + // TODO all modules should report some content and should not crash + .unwrap_or_default() + .unwrap_or_default() + .try_into() + .unwrap_or(u32::MAX), + retained_size: 0, + references: take(&mut info.references), + incoming_references: take(&mut info.incoming_references), + }) + }) + .try_join() + .await?; + + for (index, info) in modules.into_iter().enumerate() { + let retained_size = info + .retained_modules + .into_inner() + .iter() + .map(|&retained_index| { + let retained_info = &final_modules[retained_index as usize]; + retained_info.size + }) + .reduce(|a, b| a.saturating_add(b)) + .unwrap_or_default(); + final_modules[index].retained_size = retained_size + final_modules[index].size; + } + + Ok(ModuleGraphSnapshot { + modules: final_modules, + entries, + } + .cell()) +} diff --git a/crates/next-api/src/pages.rs b/crates/next-api/src/pages.rs index 68d50332f38c7..a74ce698ef70b 100644 --- a/crates/next-api/src/pages.rs +++ b/crates/next-api/src/pages.rs @@ -77,7 +77,7 @@ use crate::{ get_wasm_paths_from_root, paths_to_bindings, wasm_paths_to_bindings, }, project::Project, - route::{Endpoint, EndpointOutput, EndpointOutputPaths, Route, Routes}, + route::{Endpoint, EndpointOutput, EndpointOutputPaths, ModuleGraphs, Route, Routes}, webpack_stats::generate_webpack_stats, }; @@ -1727,6 +1727,13 @@ impl Endpoint for PageEndpoint { Ok(Vc::cell(modules)) } + + #[turbo_tasks::function] + async fn module_graphs(self: Vc) -> Result> { + let client_module_graph = self.client_module_graph().to_resolved().await?; + let ssr_module_graph = self.ssr_module_graph().to_resolved().await?; + Ok(Vc::cell(vec![client_module_graph, ssr_module_graph])) + } } #[turbo_tasks::value] diff --git a/crates/next-api/src/project.rs b/crates/next-api/src/project.rs index 7f1f0394ab40c..0b0a8399e4b96 100644 --- a/crates/next-api/src/project.rs +++ b/crates/next-api/src/project.rs @@ -973,7 +973,9 @@ impl Project { } #[turbo_tasks::function] - pub async fn whole_app_module_graphs(self: ResolvedVc) -> Result> { + pub async fn whole_app_module_graphs( + self: ResolvedVc, + ) -> Result> { async move { let module_graphs_op = whole_app_module_graph_operation(self); let module_graphs_vc = module_graphs_op.resolve_strongly_consistent().await?; @@ -1813,7 +1815,7 @@ impl Project { #[turbo_tasks::function(operation)] async fn whole_app_module_graph_operation( project: ResolvedVc, -) -> Result> { +) -> Result> { mark_root(); let should_trace = project.next_mode().await?.is_production(); @@ -1831,7 +1833,7 @@ async fn whole_app_module_graph_operation( ); let full = ModuleGraph::from_graphs(vec![base_single_module_graph, additional_module_graph]); - Ok(ModuleGraphs { + Ok(BaseAndFullModuleGraph { base: base.to_resolved().await?, full: full.to_resolved().await?, } @@ -1839,7 +1841,7 @@ async fn whole_app_module_graph_operation( } #[turbo_tasks::value(shared)] -pub struct ModuleGraphs { +pub struct BaseAndFullModuleGraph { pub base: ResolvedVc, pub full: ResolvedVc, } diff --git a/crates/next-api/src/route.rs b/crates/next-api/src/route.rs index f5466441ca679..5fda6da82881d 100644 --- a/crates/next-api/src/route.rs +++ b/crates/next-api/src/route.rs @@ -47,6 +47,9 @@ pub enum Route { Conflict, } +#[turbo_tasks::value(transparent)] +pub struct ModuleGraphs(Vec>); + #[turbo_tasks::value_trait] pub trait Endpoint { #[turbo_tasks::function] @@ -65,6 +68,8 @@ pub trait Endpoint { fn additional_entries(self: Vc, _graph: Vc) -> Vc { GraphEntries::empty() } + #[turbo_tasks::function] + fn module_graphs(self: Vc) -> Vc; } #[turbo_tasks::value(transparent)] diff --git a/packages/next/errors.json b/packages/next/errors.json index 2951e118f075d..e8420b4ca8239 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -752,5 +752,8 @@ "751": "%s cannot not be used outside of a request context.", "752": "Route %s used \"connection\" inside \"use cache\". The \\`connection()\\` function is used to indicate the subsequent code must only run when there is an actual request, but caches must be able to be produced before a request, so this function is not allowed in this scope. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache", "753": "Route %s used \"connection\" inside \"use cache: private\". The \\`connection()\\` function is used to indicate the subsequent code must only run when there is an actual navigation request, but caches must be able to be produced before a navigation request, so this function is not allowed in this scope. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache", - "754": "%s cannot be used outside of a request context." + "754": "%s cannot be used outside of a request context.", + "755": "Route must be a string", + "756": "Route %s not found", + "757": "Unknown styled string type: %s" } diff --git a/packages/next/package.json b/packages/next/package.json index 4bf2eb78eeb35..8a99a6f8011ac 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -107,6 +107,7 @@ "styled-jsx": "5.1.6" }, "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.15.1", "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", @@ -121,6 +122,9 @@ "sass": { "optional": true }, + "@modelcontextprotocol/sdk": { + "optional": true + }, "@opentelemetry/api": { "optional": true }, @@ -161,6 +165,7 @@ "@hapi/accept": "5.0.2", "@jest/transform": "29.5.0", "@jest/types": "29.5.0", + "@modelcontextprotocol/sdk": "1.15.1", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", "@next/font": "15.4.2-canary.15", diff --git a/packages/next/src/build/swc/generated-native.d.ts b/packages/next/src/build/swc/generated-native.d.ts index 5785496b60eff..34bf982be9d21 100644 --- a/packages/next/src/build/swc/generated-native.d.ts +++ b/packages/next/src/build/swc/generated-native.d.ts @@ -64,9 +64,15 @@ export interface NapiWrittenEndpoint { serverPaths: Array config: NapiEndpointConfig } +export interface NapiModuleGraphSnapshots { + moduleGraphs: Array +} export declare function endpointWriteToDisk(endpoint: { __napiType: 'Endpoint' }): Promise +export declare function endpointModuleGraphs(endpoint: { + __napiType: 'Endpoint' +}): Promise export declare function endpointServerChangedSubscribe( endpoint: { __napiType: 'Endpoint' }, issues: boolean, @@ -76,6 +82,27 @@ export declare function endpointClientChangedSubscribe( endpoint: { __napiType: 'Endpoint' }, func: (...args: any[]) => any ): { __napiType: 'RootTask' } +export interface NapiModuleReference { + /** The index of the referenced/referencing module in the modules list. */ + index: number + /** The export used in the module reference. */ + export: string + /** The type of chunking for the module reference. */ + chunkingType: string +} +export interface NapiModuleInfo { + ident: RcStr + path: RcStr + depth: number + size: number + retainedSize: number + references: Array + incomingReferences: Array +} +export interface NapiModuleGraphSnapshot { + modules: Array + entries: Array +} export interface NapiEnvVar { name: RcStr value: RcStr @@ -286,6 +313,9 @@ export declare function projectWriteAllEntrypointsToDisk( project: { __napiType: 'Project' }, appDirOnly: boolean ): Promise +export declare function projectEntrypoints(project: { + __napiType: 'Project' +}): Promise export declare function projectEntrypointsSubscribe( project: { __napiType: 'Project' }, func: (...args: any[]) => any @@ -362,6 +392,9 @@ export declare function projectGetSourceMapSync( project: { __napiType: 'Project' }, filePath: RcStr ): string | null +export declare function projectModuleGraph(project: { + __napiType: 'Project' +}): Promise /** * A version of [`NapiNextTurbopackCallbacks`] that can accepted as an argument to a napi function. * diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index 0299b04ad697a..e3b4251e1e29a 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -19,6 +19,8 @@ import { isDeepStrictEqual } from 'util' import { type DefineEnvOptions, getDefineEnv } from '../define-env' import { getReactCompilerLoader } from '../get-babel-loader-config' import type { + NapiModuleGraphSnapshot, + NapiModuleGraphSnapshots, NapiPartialProjectOptions, NapiProjectOptions, NapiSourceDiagnostic, @@ -669,6 +671,14 @@ function bindingToApi( return napiEntrypointsToRawEntrypoints(napiEndpoints) } + async getEntrypoints() { + const napiEndpoints = (await binding.projectEntrypoints( + this._nativeProject + )) as TurbopackResult + + return napiEntrypointsToRawEntrypoints(napiEndpoints) + } + entrypointsSubscribe() { const subscription = subscribe>( false, @@ -742,6 +752,12 @@ function bindingToApi( ) } + moduleGraph(): Promise> { + return binding.projectModuleGraph(this._nativeProject) as Promise< + TurbopackResult + > + } + invalidatePersistentCache(): Promise { return binding.projectInvalidatePersistentCache(this._nativeProject) } @@ -793,6 +809,12 @@ function bindingToApi( await serverSubscription.next() return serverSubscription } + + async moduleGraphs(): Promise> { + return binding.endpointModuleGraphs(this._nativeEndpoint) as Promise< + TurbopackResult + > + } } /** diff --git a/packages/next/src/build/swc/types.ts b/packages/next/src/build/swc/types.ts index 6af182f331a39..8af31514e8471 100644 --- a/packages/next/src/build/swc/types.ts +++ b/packages/next/src/build/swc/types.ts @@ -5,6 +5,8 @@ import type { RefCell, NapiTurboEngineOptions, NapiSourceDiagnostic, + NapiModuleGraphSnapshots, + NapiModuleGraphSnapshot, } from './generated-native' export type { NapiTurboEngineOptions as TurboEngineOptions } @@ -216,6 +218,7 @@ export interface Project { appDirOnly: boolean ): Promise> + getEntrypoints(): Promise> entrypointsSubscribe(): AsyncIterableIterator> hmrEvents(identifier: string): AsyncIterableIterator> @@ -244,6 +247,8 @@ export interface Project { invalidatePersistentCache(): Promise + moduleGraph(): Promise> + shutdown(): Promise onExit(): Promise @@ -295,6 +300,11 @@ export interface Endpoint { serverChanged( includeIssues: boolean ): Promise> + + /** + * Gets a snapshot of the module graphs for the endpoint. + */ + moduleGraphs(): Promise> } interface EndpointConfig { diff --git a/packages/next/src/server/lib/router-utils/mcp.ts b/packages/next/src/server/lib/router-utils/mcp.ts new file mode 100644 index 0000000000000..a79d44dbc26e2 --- /dev/null +++ b/packages/next/src/server/lib/router-utils/mcp.ts @@ -0,0 +1,584 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { z } from 'next/dist/compiled/zod' +import type { NextJsHotReloaderInterface } from '../../dev/hot-reloader-types' +import type { + Endpoint, + Issue, + Route, + StyledString, +} from '../../../build/swc/types' +import type { CallToolResult } from '@modelcontextprotocol/sdk/types' +import type { + NapiModuleGraphSnapshot, + NapiModuleInfo, + NapiModuleReference, +} from '../../../build/swc/generated-native' +import { runInNewContext } from 'node:vm' +import * as Log from '../../../build/output/log' +import { formatImportTraces } from '../../../shared/lib/turbopack/utils' +import { inspect } from 'node:util' + +export { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' + +const QUERY_DESCRIPTION = `A piece of JavaScript code that will be executed. +It can access the module graph and extract information it finds useful. +The \`console.log\` function can be used to log messages, which will also be returned in the response. +No Node.js or browser APIs are available, but JavaScript language features are available. +When the user is interested in used exports of modules, you can use the \`export\` property of ModuleReferences (\`incomingReferences\`). +When the user is interested in the import path of a module, follow one of the \`incomingReferences\` with a smaller \`depth\` value than the current module until you hit the root. +Do not try to make any assumptions or estimations. See the typings to see which data you have available. If you don't have the data, tell the user that this data is not available and list alternatives that are available. +See the following TypeScript typings for reference: + +\`\`\` typescript +interface Module { + /// The identifier of the module, which is a unique string. + /// Example: "[project]/packages/next-app/src/app/folder/page.tsx [app-rsc] (ecmascript, Next.js Server Component)" + /// These layers exist in App Router: + /// * Server Components: [app-rsc], [app-edge-rsc] + /// * API routes: [app-route], [app-edge-route] + /// * Client Components: [app-client] + /// * Server Side Rendering of Client Components: [app-ssr], [app-edge-ssr] + /// These layers exist in Pages Router: + /// * Client-side rendering: [client] + /// * Server-side rendering: [ssr], [edge-ssr] + /// * API routes: [api], [edge-api] + /// And these layers also exist: + /// * Middleware: [middleware], [middleware-edge] + /// * Instrumentation: [instrumentation], [instrumentation-edge] + ident: string, + /// The path of the module. It's not unique as multiple modules can have the same path. + /// Separate between application code and node_modules (npm packages, vendor code). + /// Example: "[project]/pages/folder/index.js", + /// Example: "[project]/node_modules/.pnpm/next@file+..+next.js+packages+next_@babel+core@7.27.4_@opentelemetry+api@1.7.0_@playwright+te_5kenhtwdm6lrgjpao5hc34lkgy/node_modules/next/dist/compiled/fresh/index.js", + /// Example: "[project]/apps/site/node_modules/@opentelemtry/api/build/src/trace/instrumentation.js", + path: string, + /// The distance to the entries of the module graph. Use this to traverse the graph in the right direction. + /// This is useful when trying to find the path from a module to the root of the module graph. + /// Example: 0 for the entrypoint, 1 for the first layer of modules, etc. + depth: number, + /// The size of the source code of the module in bytes. + /// Note that it's not the final size of the generated code, but can be a good indicator of that. + /// It's only the size of this single module, not the size of the whole subgraph behind it (see retainedSize instead). + size: number, + /// The size of the whole subgraph behind this module in bytes. + /// Use this value if the user is interested in sizes of modules (except when they are interested in the size of the module itself). + /// Never try to compute the retained size yourself, but use this value instead. + retainedSize: number, + /// The modules that are referenced by this module. + /// Modules could be referenced by \`import\`, \`require\`, \`new URL\`, etc. + /// Beware cycles in the module graph. You can avoid that by only walking edges with a bigger \`depth\` value than the current module. + references: ModuleReference[], + /// The modules that reference this module. + /// Beware cycles in the module graph. + /// You can use this to walk up the graph up to the root. When doing this only walk edges with a smaller \`depth\` value than the current module. + incomingReferences: ModuleReference[], +} + +interface ModuleReference { + /// The referenced/referencing module. + module: Module + /// The thing that is used from the module. + /// export {name}: The named export that is used. + /// evaluation: Imported for side effects only. + /// all: All exports and the side effects. + export: "evaluation" | "all" | \`export \${string}\` + /// How this reference affects chunking of the module. + /// hoisted | sync: The module is placed in the same chunk group as the referencing module. + /// hoisted: The module is loaded before all "sync" modules. + /// async: The module forms a separate chunk group which is loaded asynchronously. + /// isolated: The module forms a separate chunk group which is loaded as separate entry. When it has a name, all modules imported with this name are placed in the same chunk group. + /// shared: The module forms a separate chunk group which is loaded before the current chunk group. When it has a name, all modules imported with this name are placed in the same chunk group. + /// traced: The module is not bundled, but the graph is still traced and all modules are included unbundled. + chunkingType: "hoisted" | "sync" | "async" | "isolated" | \`isolated \${string}\` | "shared" | \`shared \${string}\` | "traced" +} + +// The following global variables are available in the query: + +/// The entries of the module graph. +/// Note that this only includes the entrypoints of the module graph and not all modules. +/// You need to traverse it recursively to find not only children, but also grandchildren (resp, grandparents). +/// Prefer to use \`modules\` over \`entries\` as it contains all modules, not only the entrypoints. +const entries: Module[] + +/// All modules in the module graph. +/// Note that this array already contains all the modules as flat list. +/// Make sure to iterate over this array and not only consider the first one. +/// Prefer to use \`modules\` over \`entries\` as it contains all modules, not only the entrypoints. +const modules: Module[] + +const console: { + /// Logs a message to the console. + /// The message will be returned in the response. + /// The message can be a string or any other value that can be inspected. + log: (...data: any[]) => void +} +\`\`\` +` + +async function measureAndHandleErrors( + name: string, + fn: () => Promise +): Promise { + const start = performance.now() + let content: CallToolResult['content'] = [] + try { + content = await fn() + } catch (error) { + content.push({ + type: 'text', + text: `Error: ${error instanceof Error ? error.stack : String(error)}`, + }) + content.push({ + type: 'text', + text: 'Fix the error and try again.', + }) + } + const duration = performance.now() - start + const formatDurationText = + duration > 2000 + ? `${Math.round(duration / 100) / 10}s` + : `${Math.round(duration)}ms` + Log.event(`MCP ${name} in ${formatDurationText}`) + return { + content, + } +} + +function invariant(value: never, errorMessage: (value: any) => string): never { + throw new Error(errorMessage(value)) +} + +function styledStringToMarkdown( + styledString: StyledString | undefined +): string { + if (!styledString) { + return '' + } + switch (styledString.type) { + case 'text': + return styledString.value + case 'strong': + return `*${styledString.value}*` + case 'code': + return `\`${styledString.value}\`` + case 'line': + return styledString.value.map(styledStringToMarkdown).join('') + case 'stack': + return styledString.value.map(styledStringToMarkdown).join('\n\n') + default: + invariant(styledString, (s) => `Unknown styled string type: ${s.type}`) + } +} + +function indent(str: string, spaces: number = 2): string { + const indentStr = ' '.repeat(spaces) + return `${indentStr}${str.replace(/\n/g, `\n${indentStr}`)}` +} + +function issueToString(issue: Issue & { route: string }): string { + return [ + `${issue.severity} in ${issue.stage} on ${issue.route}`, + `File Path: ${issue.filePath}`, + issue.source && + `Source: + ${issue.source.source.ident} + ${issue.source.range ? `Range: ${issue.source.range?.start.line}:${issue.source.range?.start.column} - ${issue.source.range?.end.line}:${issue.source.range?.end.column}` : 'Unknown range'} +`, + `Title: ${styledStringToMarkdown(issue.title)}`, + issue.description && + `Description: +${indent(styledStringToMarkdown(issue.description))}`, + issue.detail && + `Details: +${indent(styledStringToMarkdown(issue.detail))}`, + issue.documentationLink && `Documentation: ${issue.documentationLink}`, + issue.importTraces && + issue.importTraces.length > 0 && + formatImportTraces(issue.importTraces), + ] + .filter(Boolean) + .join('\n') +} + +function issuesReference(issues: Issue[]): { type: 'text'; text: string } { + if (issues.length === 0) { + return { + type: 'text', + text: 'Note: There are no issues.', + } + } + + const countBySeverity = new Map() + + for (const issue of issues) { + const count = countBySeverity.get(issue.severity) || 0 + countBySeverity.set(issue.severity, count + 1) + } + + const text = [ + `Note: There are ${issues.length} issues in total, with the following severities: ${Array.from( + countBySeverity.entries() + ) + .map(([severity, count]) => `${count} x ${severity}`) + .join(', ')}.`, + ] + + return { + type: 'text', + text: text.join('\n'), + } +} + +function routeToTitle(route: Route): string { + switch (route.type) { + case 'page': + return 'A page using Pages Router.' + case 'app-page': + return `A page using App Router. Original names: ${route.pages.map((page) => page.originalName).join(', ')}.` + case 'page-api': + return 'An API route using Pages Router.' + case 'app-route': + return `A route using App Router. Original name: ${route.originalName}.` + case 'conflict': + return 'Multiple routes conflict on this path. This is an error in the folder structure.' + default: + invariant(route, (r) => `Unknown route type: ${r.type}`) + } +} + +function routeToEndpoints(route: Route): Endpoint[] { + switch (route.type) { + case 'page': + return [route.htmlEndpoint] + case 'app-page': + return route.pages.map((p) => p.htmlEndpoint) + case 'page-api': + return [route.endpoint] + case 'app-route': + return [route.endpoint] + case 'conflict': + return [] + default: + invariant(route, (r) => `Unknown route type: ${r.type}`) + } +} + +function arrayOrSingle(value: T | T[]): T[] { + return Array.isArray(value) ? value : [value] +} + +interface ModuleReference { + module: Module + export: string + chunkingType: string +} +interface Module { + /// The identifier of the module, which is a unique string. + ident: string + /// The path of the module. It's not unique as multiple modules can have the same path. + path: string + /// The distance to the entries of the module graph. Use this to traverse the graph in the right direction. + depth: number + /// The size of the source code of the module in bytes. + size: number + /// The size of the whole subgraph behind this module in bytes. + retainedSize: number + /// The modules that are referenced by this module. + references: ModuleReference[] + /// The modules that reference this module. + incomingReferences: ModuleReference[] +} + +function createModuleObject(rawModule: NapiModuleInfo): Module { + return { + ident: rawModule.ident, + path: rawModule.path, + depth: rawModule.depth, + size: rawModule.size, + retainedSize: rawModule.retainedSize, + references: [], + incomingReferences: [], + } +} + +function processModuleGraphSnapshot( + moduleGraph: NapiModuleGraphSnapshot, + modules: Module[], + entries: Module[] +) { + const queryModules = moduleGraph.modules.map(createModuleObject) + for (let i = 0; i < queryModules.length; i++) { + const rawModule = moduleGraph.modules[i] + const queryModule = queryModules[i] + + queryModule.references = rawModule.references.map( + (ref: NapiModuleReference) => ({ + module: queryModules[ref.index], + export: ref.export, + chunkingType: ref.chunkingType, + }) + ) + queryModule.incomingReferences = rawModule.incomingReferences.map( + (ref: NapiModuleReference) => ({ + module: queryModules[ref.index], + export: ref.export, + chunkingType: ref.chunkingType, + }) + ) + modules.push(queryModule) + } + for (const entry of moduleGraph.entries) { + const queryModule = queryModules[entry] + entries.push(queryModule) + } +} + +function runQuery( + query: string, + modules: Module[], + entries: Module[] +): CallToolResult['content'] { + const response: CallToolResult['content'] = [] + const proto = { + modules, + entries, + console: { + log: (...data: any[]) => { + response.push({ + type: 'text', + text: data + .map((item) => + typeof item === 'string' ? item : inspect(item, false, 2, false) + ) + .join(' '), + }) + }, + }, + } + const contextObject = Object.create(proto) + contextObject.global = contextObject + contextObject.self = contextObject + contextObject.globalThis = contextObject + runInNewContext(query, contextObject, { + displayErrors: true, + filename: 'query.js', + timeout: 20000, + contextName: 'Query Context', + }) + for (const [key, value] of Object.entries(contextObject)) { + if (typeof value === 'function') continue + if (key === 'global' || key === 'self' || key === 'globalThis') continue + response.push({ + type: 'text', + text: `Global variable \`${key}\` = ${inspect(value, false, 2, false)}`, + }) + } + return response +} + +const ROUTES_DESCRIPTION = + 'The routes from which to query the module graph. Can be a single string or an array of strings.' +export function createMcpServer( + hotReloader: NextJsHotReloaderInterface +): McpServer | undefined { + const turbopack = hotReloader.turbopackProject + if (!turbopack) return undefined + const server = new McpServer({ + name: 'next.js', + version: '1.0.0', + instructions: `This is a running next.js dev server with Turbopack. +You can use the Model Context Protocol to query information about pages and modules and their relations.`, + }) + + server.registerTool( + 'entrypoints', + { + title: 'Entrypoints', + description: + 'Get all entrypoints of a Turbopack project, which are all pages, routes and the middleware.', + }, + async () => + measureAndHandleErrors('entrypoints', async () => { + let entrypoints = await turbopack.getEntrypoints() + + const list = [] + + for (const [key, route] of entrypoints.routes.entries()) { + list.push(`\`${key}\` (${routeToTitle(route)})`) + } + + if (entrypoints.middleware) { + list.push('Middleware') + } + + if (entrypoints.instrumentation) { + list.push('Instrumentation') + } + + const content: CallToolResult['content'] = [ + issuesReference(entrypoints.issues), + { + type: 'text', + text: `These are the routes of the application: + +${list.map((e) => `- ${e}`).join('\n')}`, + }, + ] + return content + }) + ) + + server.registerTool( + 'query-routes-module-graph', + { + title: 'Query module graph of routes', + description: 'Query details about the module graph of routes.', + inputSchema: { + routes: z + .union([z.string(), z.array(z.string())]) + .describe(ROUTES_DESCRIPTION), + query: z.string().describe(QUERY_DESCRIPTION), + }, + }, + async ({ routes, query }) => + measureAndHandleErrors( + `module graph query on ${arrayOrSingle(routes).join(', ')}`, + async () => { + const entrypoints = await turbopack.getEntrypoints() + const endpoints = [] + for (const route of arrayOrSingle(routes)) { + const routeInfo = entrypoints.routes.get(route) + if (!routeInfo) { + throw new Error(`Route ${route} not found`) + } + endpoints.push(...routeToEndpoints(routeInfo)) + } + const issues = [] + const modules: Module[] = [] + const entries: Module[] = [] + + for (const endpoint of endpoints) { + const result = await endpoint.moduleGraphs() + issues.push(...result.issues) + const moduleGraphs = result.moduleGraphs + for (const moduleGraph of moduleGraphs) { + processModuleGraphSnapshot(moduleGraph, modules, entries) + } + } + const content: CallToolResult['content'] = [] + content.push(issuesReference(issues)) + const response = runQuery(query, modules, entries) + content.push(...response) + return content + } + ) + ) + + server.registerTool( + 'query-module-graph', + { + title: 'Query whole app module graph', + description: + 'Query details about the module graph the whole application. This is a expensive operation and should only be used when module graph of the whole application is needed.', + inputSchema: { + query: z.string().describe(QUERY_DESCRIPTION), + }, + }, + async ({ query }) => + measureAndHandleErrors(`whole app module graph query`, async () => { + const moduleGraph = await turbopack.moduleGraph() + const issues = moduleGraph.issues + const modules: Module[] = [] + const entries: Module[] = [] + processModuleGraphSnapshot(moduleGraph, modules, entries) + const content: CallToolResult['content'] = [] + content.push(issuesReference(issues)) + const response = runQuery(query, modules, entries) + content.push(...response) + return content + }) + ) + + server.registerTool( + 'query-issues', + { + title: 'Query issues of routes', + description: + 'Query issues (errors, warnings, lints, etc.) that are reported on routes.', + inputSchema: { + routes: z + .union([z.string(), z.array(z.string())]) + .describe(ROUTES_DESCRIPTION), + page: z + .optional(z.number()) + .describe( + 'Issues are paginated when there are more than 50 issues. The first page is number 0.' + ), + }, + }, + async ({ routes, page }) => + measureAndHandleErrors( + `issues on ${arrayOrSingle(routes).join(', ')}`, + async () => { + const entrypoints = await turbopack.getEntrypoints() + const issues = [] + for (const route of arrayOrSingle(routes)) { + const routeInfo = entrypoints.routes.get(route) + if (!routeInfo) { + throw new Error(`Route ${route} not found`) + } + for (const endpoint of routeToEndpoints(routeInfo)) { + const result = await endpoint.moduleGraphs() + for (const issue of result.issues) { + const issuesWithRoute = issue as Issue & { route: string } + issuesWithRoute.route = route + issues.push(issuesWithRoute) + } + } + } + const severitiesArray = [ + 'bug', + 'fatal', + 'error', + 'warning', + 'hint', + 'note', + 'suggestion', + 'info', + ] + const severities = new Map( + severitiesArray.map((severity, index) => [severity, index]) + ) + issues.sort((a, b) => { + const severityA = severities.get(a.severity) + const severityB = severities.get(b.severity) + if (severityA !== undefined && severityB !== undefined) { + return severityA - severityB + } + return 0 + }) + + const content: CallToolResult['content'] = [] + content.push(issuesReference(issues)) + page = page ?? 0 + const currentPage = issues.slice(page * 50, (page + 1) * 50) + for (const issue of currentPage) { + content.push({ + type: 'text', + text: issueToString(issue), + }) + } + if (issues.length >= (page + 1) * 50) { + content.push({ + type: 'text', + text: `Note: There are more issues available. Use the \`page\` parameter to query the next page.`, + }) + } + + return content + } + ) + ) + + return server +} diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index fb2a0c0301603..33d7d3b2722e1 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -85,6 +85,8 @@ import { getDefineEnv } from '../../../build/define-env' import { TurbopackInternalError } from '../../../shared/lib/turbopack/internal-error' import { normalizePath } from '../../../lib/normalize-path' import { JSON_CONTENT_TYPE_HEADER } from '../../../lib/constants' +import { parseBody } from '../../api-utils/node/parse-body' +import { timingSafeEqual } from 'crypto' export type SetupOpts = { renderServer: LazyRenderServerInstance @@ -969,9 +971,114 @@ async function startWatcher( const devTurbopackMiddlewareManifestPath = `/_next/${CLIENT_STATIC_FILES_PATH}/development/${TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST}` opts.fsChecker.devVirtualFsItems.add(devTurbopackMiddlewareManifestPath) + const mcpPath = `/_next/mcp` + opts.fsChecker.devVirtualFsItems.add(mcpPath) + + let mcpSecret = process.env.NEXT_EXPERIMENTAL_MCP_SECRET + ? Buffer.from(process.env.NEXT_EXPERIMENTAL_MCP_SECRET) + : undefined + + if (mcpSecret) { + try { + // eslint-disable-next-line import/no-extraneous-dependencies + require('@modelcontextprotocol/sdk/package.json') + } catch (error) { + Log.error( + 'To use the MCP server, please install the `@modelcontextprotocol/sdk` package.' + ) + mcpSecret = undefined + } + } + let createMcpServer: typeof import('./mcp').createMcpServer | undefined + let StreamableHTTPServerTransport: + | typeof import('./mcp').StreamableHTTPServerTransport + | undefined + if (mcpSecret) { + ;({ createMcpServer, StreamableHTTPServerTransport } = + require('./mcp') as typeof import('./mcp')) + Log.info( + `Experimental MCP server is available at: /_next/mcp?${mcpSecret.toString()}` + ) + } + async function requestHandler(req: IncomingMessage, res: ServerResponse) { const parsedUrl = url.parse(req.url || '/') + if (parsedUrl.pathname?.includes(mcpPath)) { + function sendMcpInternalError(message: string) { + res.statusCode = 500 + res.setHeader('Content-Type', 'application/json; charset=utf-8') + res.end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + // "Internal error" see https://www.jsonrpc.org/specification + code: -32603, + message, + }, + id: null, + }) + ) + return { finished: true } + } + if (!mcpSecret) { + Log.error('Next.js MCP server is not enabled') + Log.info( + 'To enable it, set the NEXT_EXPERIMENTAL_MCP_SECRET environment variable to a secret value. This will make the MCP server available at /_next/mcp?{NEXT_EXPERIMENTAL_MCP_SECRET}' + ) + return sendMcpInternalError( + 'Missing NEXT_EXPERIMENTAL_MCP_SECRET environment variable' + ) + } + if (!createMcpServer || !StreamableHTTPServerTransport) { + return sendMcpInternalError( + 'Model Context Protocol (MCP) server is not available' + ) + } + if (!parsedUrl.query) { + Log.error('No MCP secret provided in request query') + Log.info( + `Experimental MCP server is available at: /_next/mcp?${mcpSecret.toString()}` + ) + return sendMcpInternalError('No MCP secret provided in request query') + } + let mcpSecretQuery = Buffer.from(parsedUrl.query) + if ( + mcpSecretQuery.length !== mcpSecret.length || + !timingSafeEqual(mcpSecretQuery, mcpSecret) + ) { + Log.error('Invalid MCP secret provided in request query') + Log.info( + `Experimental MCP server is available at: /_next/mcp?${mcpSecret.toString()}` + ) + return sendMcpInternalError( + 'Invalid MCP secret provided in request query' + ) + } + + const server = createMcpServer(hotReloader) + if (server) { + try { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }) + res.on('close', () => { + transport.close() + server.close() + }) + await server.connect(transport) + const parsedBody = await parseBody(req, 1024 * 1024 * 1024) + await transport.handleRequest(req, res, parsedBody) + } catch (error) { + Log.error('Error handling MCP request:', error) + if (!res.headersSent) { + return sendMcpInternalError('Internal server error') + } + } + return { finished: true } + } + } + if (parsedUrl.pathname?.includes(clientPagesManifestPath)) { res.statusCode = 200 res.setHeader('Content-Type', JSON_CONTENT_TYPE_HEADER) diff --git a/packages/next/src/shared/lib/turbopack/utils.ts b/packages/next/src/shared/lib/turbopack/utils.ts index f88fc6ef6711f..22ca4707f06cf 100644 --- a/packages/next/src/shared/lib/turbopack/utils.ts +++ b/packages/next/src/shared/lib/turbopack/utils.ts @@ -185,36 +185,7 @@ export function formatIssue(issue: Issue) { if (importTraces?.length) { // This is the same logic as in turbopack/crates/turbopack-cli-utils/src/issue.rs - // We end up with multiple traces when the file with the error is reachable from multiple - // different entry points (e.g. ssr, client) - message += `Import trace${importTraces.length > 1 ? 's' : ''}:\n` - const everyTraceHasADistinctRootLayer = - new Set(importTraces.map(leafLayerName).filter((l) => l != null)).size === - importTraces.length - for (let i = 0; i < importTraces.length; i++) { - const trace = importTraces[i] - const layer = leafLayerName(trace) - let traceIndent = ' ' - // If this is true, layer must be present - if (everyTraceHasADistinctRootLayer) { - message += ` ${layer}:\n` - } else { - if (importTraces.length > 1) { - // Otherwise use simple 1 based indices to disambiguate - message += ` #${i + 1}` - if (layer) { - message += ` [${layer}]` - } - message += ':\n' - } else if (layer) { - message += ` [${layer}]:\n` - } else { - // If there is a single trace and no layer name just don't indent it. - traceIndent = ' ' - } - } - message += formatIssueTrace(trace, traceIndent, !identicalLayers(trace)) - } + message += formatImportTraces(importTraces) } if (documentationLink) { message += documentationLink + '\n\n' @@ -222,6 +193,40 @@ export function formatIssue(issue: Issue) { return message } +export function formatImportTraces(importTraces: PlainTraceItem[][]) { + // We end up with multiple traces when the file with the error is reachable from multiple + // different entry points (e.g. ssr, client) + let message = `Import trace${importTraces.length > 1 ? 's' : ''}:\n` + const everyTraceHasADistinctRootLayer = + new Set(importTraces.map(leafLayerName).filter((l) => l != null)).size === + importTraces.length + for (let i = 0; i < importTraces.length; i++) { + const trace = importTraces[i] + const layer = leafLayerName(trace) + let traceIndent = ' ' + // If this is true, layer must be present + if (everyTraceHasADistinctRootLayer) { + message += ` ${layer}:\n` + } else { + if (importTraces.length > 1) { + // Otherwise use simple 1 based indices to disambiguate + message += ` #${i + 1}` + if (layer) { + message += ` [${layer}]` + } + message += ':\n' + } else if (layer) { + message += ` [${layer}]:\n` + } else { + // If there is a single trace and no layer name just don't indent it. + traceIndent = ' ' + } + } + message += formatIssueTrace(trace, traceIndent, !identicalLayers(trace)) + } + return message +} + /** Returns the first present layer name in the trace */ function leafLayerName(items: PlainTraceItem[]): string | undefined { for (const item of items) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bba0fd43a895c..8f4b7178314f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1039,6 +1039,9 @@ importers: '@jest/types': specifier: 29.5.0 version: 29.5.0 + '@modelcontextprotocol/sdk': + specifier: 1.15.1 + version: 1.15.1 '@mswjs/interceptors': specifier: 0.23.0 version: 0.23.0 @@ -4424,6 +4427,10 @@ packages: '@types/react': 19.1.8 react: 19.2.0-canary-7513996f-20250722 + '@modelcontextprotocol/sdk@1.15.1': + resolution: {integrity: sha512-W/XlN9c528yYn+9MQkVjxiTPgPxoxt+oczfjHBDsJx0+59+O7B75Zhsp0B16Xbwbz8ANISDajh6+V7nIcPMc5w==} + engines: {node: '>=18'} + '@module-federation/error-codes@0.15.0': resolution: {integrity: sha512-CFJSF+XKwTcy0PFZ2l/fSUpR4z247+Uwzp1sXVkdIfJ/ATsnqf0Q01f51qqSEA6MYdQi6FKos9FIcu3dCpQNdg==} @@ -6188,6 +6195,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} @@ -6791,6 +6802,10 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + bonjour-service@1.3.0: resolution: {integrity: sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==} @@ -6942,6 +6957,10 @@ packages: resolution: {integrity: sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} @@ -6949,6 +6968,10 @@ packages: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + caller-callsite@2.0.0: resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==} engines: {node: '>=4'} @@ -7446,6 +7469,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + content-type@1.0.4: resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==} engines: {node: '>= 0.6'} @@ -7498,6 +7525,10 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.4.0: resolution: {integrity: sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==} engines: {node: '>= 0.6'} @@ -8506,6 +8537,10 @@ packages: downsample-lttb@0.0.1: resolution: {integrity: sha512-Olebo5gyh44OAXTd2BKdcbN5VaZOIKFzoeo9JUFwxDlGt6Sd8fUo6SKaLcafy8aP2UrsKmWDpsscsFEghMjeZA==} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + duplexer3@0.1.4: resolution: {integrity: sha512-CEj8FwwNA4cVH2uFCoHUrmojhYh1vmCdOaneKJXwkeY1i9jnlslVo9dx+hQ5Hl9GnH/Bwy/IjxAyOePyPKYnzA==} @@ -8653,6 +8688,10 @@ packages: resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} engines: {node: '>= 0.4'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} @@ -8671,6 +8710,10 @@ packages: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} engines: {node: '>= 0.4'} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + es-set-tostringtag@2.0.3: resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} engines: {node: '>= 0.4'} @@ -9057,6 +9100,14 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.3: + resolution: {integrity: sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==} + engines: {node: '>=20.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + evp_bytestokey@1.0.3: resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} @@ -9110,6 +9161,12 @@ packages: resolution: {integrity: sha512-WVi2V4iHKw/vHEyye00Q9CSZz7KHDbJkJyteUI8kTih9jiyMl3bIk7wLYFcY9D1Blnadlyb5w5NBuNjQBow99g==} engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0} + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@4.17.0: resolution: {integrity: sha512-1Z7/t3Z5ZnBG252gKUPyItc4xdeaA0X934ca2ewckAsVsw9EG71i++ZHZPYnus8g/s5Bty8IMpSVEuRkmwwPRQ==} engines: {node: '>= 0.10.0'} @@ -9118,6 +9175,10 @@ packages: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + ext@1.6.0: resolution: {integrity: sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==} @@ -9253,6 +9314,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + find-cache-dir@2.1.0: resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} engines: {node: '>=6'} @@ -9402,6 +9467,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + from@0.1.7: resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} @@ -9504,6 +9573,10 @@ packages: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} @@ -9523,6 +9596,10 @@ packages: resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} engines: {node: '>=8'} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stdin@4.0.1: resolution: {integrity: sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==} engines: {node: '>=0.10.0'} @@ -9705,6 +9782,10 @@ packages: gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + got@7.1.0: resolution: {integrity: sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==} engines: {node: '>=4'} @@ -9804,6 +9885,10 @@ packages: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + has-to-string-tag-x@1.4.1: resolution: {integrity: sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==} @@ -10564,6 +10649,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -11605,6 +11693,10 @@ packages: resolution: {integrity: sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==} engines: {node: '>=0.10.0'} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + maximatch@0.1.0: resolution: {integrity: sha512-9ORVtDUFk4u/NFfo0vG/ND/z7UQCVZBL539YW0+U1I7H1BkZwizcPx5foFv7LCPcBnm2U6RjFnQOsIvN4/Vm2A==} engines: {node: '>=0.10.0'} @@ -11710,6 +11802,10 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + memfs@3.5.3: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} @@ -11743,6 +11839,10 @@ packages: merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -11978,6 +12078,10 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.18: resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==} engines: {node: '>= 0.6'} @@ -11986,6 +12090,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -12240,6 +12348,10 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo-async@2.6.1: resolution: {integrity: sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==} @@ -12503,6 +12615,10 @@ packages: resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} engines: {node: '>= 0.4'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + object-is@1.0.2: resolution: {integrity: sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==} engines: {node: '>= 0.4'} @@ -12941,6 +13057,10 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + path-type@1.1.0: resolution: {integrity: sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==} engines: {node: '>=0.10.0'} @@ -13046,6 +13166,10 @@ packages: resolution: {integrity: sha512-ugJ4Imy92u55zeznaN/5d7iqOBIZjZ7q10/T+dcd0IuFtbLlsGDvAUabFu1cafER+G9f0T1WtTqvzm4KAdcDgQ==} engines: {node: '>=4.0.0', npm: '>=1.2.10'} + pkce-challenge@5.0.0: + resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==} + engines: {node: '>=16.20.0'} + pkg-dir@3.0.0: resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} engines: {node: '>=6'} @@ -13993,6 +14117,10 @@ packages: resolution: {integrity: sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==} engines: {node: '>=0.6'} + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + qs@6.5.2: resolution: {integrity: sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==} engines: {node: '>=0.6'} @@ -14067,6 +14195,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -14667,6 +14799,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-applescript@7.0.0: resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} engines: {node: '>=18'} @@ -14853,6 +14989,10 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + sentence-case@3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} @@ -14880,6 +15020,10 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} @@ -14946,10 +15090,26 @@ packages: engines: {node: '>=4'} hasBin: true + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + side-channel@1.0.6: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -15989,6 +16149,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + type@1.2.0: resolution: {integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==} @@ -16976,6 +17140,11 @@ packages: yoga-wasm-web@0.3.3: resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} + zod-to-json-schema@3.24.6: + resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} + peerDependencies: + zod: ^3.24.1 + zod-validation-error@3.4.0: resolution: {integrity: sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==} engines: {node: '>=18.0.0'} @@ -20118,6 +20287,23 @@ snapshots: '@types/react': 19.1.8 react: 19.2.0-canary-7513996f-20250722 + '@modelcontextprotocol/sdk@1.15.1': + dependencies: + ajv: 6.12.6 + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.3 + express: 5.1.0 + express-rate-limit: 7.5.1(express@5.1.0) + pkce-challenge: 5.0.0 + raw-body: 3.0.0 + zod: 3.25.76 + zod-to-json-schema: 3.24.6(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@module-federation/error-codes@0.15.0': {} '@module-federation/runtime-core@0.15.0': @@ -22359,6 +22545,11 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + acorn-globals@7.0.1: dependencies: acorn: 8.14.0 @@ -23030,6 +23221,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.0 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.0 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + bonjour-service@1.3.0: dependencies: fast-deep-equal: 3.1.3 @@ -23224,6 +23429,11 @@ snapshots: package-hash: 4.0.0 write-file-atomic: 3.0.3 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + call-bind@1.0.2: dependencies: function-bind: 1.1.2 @@ -23237,6 +23447,11 @@ snapshots: get-intrinsic: 1.2.4 set-function-length: 1.2.2 + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + caller-callsite@2.0.0: dependencies: callsites: 2.0.0 @@ -23781,6 +23996,10 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + content-type@1.0.4: {} content-type@1.0.5: {} @@ -23857,6 +24076,8 @@ snapshots: cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} + cookie@0.4.0: {} cookie@0.4.1: {} @@ -24985,6 +25206,12 @@ snapshots: downsample-lttb@0.0.1: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + duplexer3@0.1.4: {} duplexer@0.1.1: {} @@ -25187,6 +25414,8 @@ snapshots: dependencies: get-intrinsic: 1.2.4 + es-define-property@1.0.1: {} + es-errors@1.3.0: {} es-get-iterator@1.1.3: @@ -25224,6 +25453,10 @@ snapshots: dependencies: es-errors: 1.3.0 + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + es-set-tostringtag@2.0.3: dependencies: get-intrinsic: 1.2.4 @@ -25973,6 +26206,12 @@ snapshots: events@3.3.0: {} + eventsource-parser@3.0.3: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.3 + evp_bytestokey@1.0.3: dependencies: md5.js: 1.3.5 @@ -26073,6 +26312,10 @@ snapshots: jest-mock: 30.0.0-alpha.6 jest-util: 30.0.0-alpha.6 + express-rate-limit@7.5.1(express@5.1.0): + dependencies: + express: 5.1.0 + express@4.17.0: dependencies: accepts: 1.3.7 @@ -26144,6 +26387,38 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.2.2 + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + ext@1.6.0: dependencies: type: 2.5.0 @@ -26311,6 +26586,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.0: + dependencies: + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + find-cache-dir@2.1.0: dependencies: commondir: 1.0.1 @@ -26492,6 +26778,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + from@0.1.7: {} fromentries@1.3.2: {} @@ -26606,6 +26894,19 @@ snapshots: has-symbols: 1.0.3 hasown: 2.0.2 + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} get-package-type@0.1.0: {} @@ -26622,6 +26923,11 @@ snapshots: get-port@5.1.1: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stdin@4.0.1: {} get-stream@3.0.0: {} @@ -26874,6 +27180,8 @@ snapshots: dependencies: get-intrinsic: 1.2.4 + gopd@1.2.0: {} + got@7.1.0: dependencies: '@types/keyv': 3.1.1 @@ -26978,6 +27286,8 @@ snapshots: has-symbols@1.0.3: {} + has-symbols@1.1.0: {} + has-to-string-tag-x@1.4.1: dependencies: has-symbol-support-x: 1.4.2 @@ -27794,6 +28104,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.7 @@ -29202,6 +29514,8 @@ snapshots: markdown-extensions@1.1.1: {} + math-intrinsics@1.1.0: {} + maximatch@0.1.0: dependencies: array-differ: 1.0.0 @@ -29469,6 +29783,8 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + memfs@3.5.3: dependencies: fs-monkey: 1.0.6 @@ -29531,6 +29847,8 @@ snapshots: merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -30102,6 +30420,8 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.18: dependencies: mime-db: 1.33.0 @@ -30110,6 +30430,10 @@ snapshots: dependencies: mime-db: 1.52.0 + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} mime@2.5.2: {} @@ -30332,6 +30656,8 @@ snapshots: negotiator@0.6.3: {} + negotiator@1.0.0: {} + neo-async@2.6.1: {} neo-async@2.6.2: {} @@ -30662,6 +30988,8 @@ snapshots: object-inspect@1.13.2: {} + object-inspect@1.13.4: {} + object-is@1.0.2: {} object-is@1.1.6: @@ -31174,6 +31502,8 @@ snapshots: path-to-regexp@6.3.0: {} + path-to-regexp@8.2.0: {} + path-type@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -31260,6 +31590,8 @@ snapshots: postcss: 7.0.32 reduce-css-calc: 2.1.7 + pkce-challenge@5.0.0: {} + pkg-dir@3.0.0: dependencies: find-up: 3.0.0 @@ -32256,6 +32588,10 @@ snapshots: dependencies: side-channel: 1.0.6 + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + qs@6.5.2: {} qs@6.7.0: {} @@ -32320,6 +32656,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -33153,6 +33496,16 @@ snapshots: fsevents: 2.3.3 optional: true + router@2.2.0: + dependencies: + debug: 4.4.0 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.2.0 + transitivePeerDependencies: + - supports-color + run-applescript@7.0.0: {} run-async@2.4.1: {} @@ -33361,6 +33714,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.0: + dependencies: + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + sentence-case@3.0.4: dependencies: no-case: 3.0.4 @@ -33419,6 +33788,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + server-only@0.0.1: {} set-blocking@2.0.0: {} @@ -33508,6 +33886,26 @@ snapshots: interpret: 1.4.0 rechoir: 0.6.2 + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + side-channel@1.0.6: dependencies: call-bind: 1.0.7 @@ -33515,6 +33913,14 @@ snapshots: get-intrinsic: 1.2.4 object-inspect: 1.13.2 + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: optional: true @@ -34638,6 +35044,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + type@1.2.0: {} type@2.5.0: {} @@ -35891,6 +36303,10 @@ snapshots: yoga-wasm-web@0.3.3: {} + zod-to-json-schema@3.24.6(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod-validation-error@3.4.0(zod@3.25.76): dependencies: zod: 3.25.76 diff --git a/turbopack/crates/turbopack-core/src/module_graph/mod.rs b/turbopack/crates/turbopack-core/src/module_graph/mod.rs index 2d5bcc610a991..c844ef54957fa 100644 --- a/turbopack/crates/turbopack-core/src/module_graph/mod.rs +++ b/turbopack/crates/turbopack-core/src/module_graph/mod.rs @@ -1221,6 +1221,23 @@ impl ModuleGraph { Ok(idx) } + pub async fn entries(&self) -> Result>>> { + Ok(self + .get_graphs() + .await? + .iter() + .flat_map(|g| g.entries.iter()) + .flat_map(|e| e.entries()) + .collect()) + } + + pub async fn has_entry(&self, entry: ResolvedVc>) -> Result { + let graphs = self.get_graphs().await?; + Ok(graphs + .iter() + .any(|graph| graph.modules.contains_key(&entry))) + } + /// Traverses all reachable edges exactly once and calls the visitor with the edge source and /// target. /// From 1937a19fb9f342b0f5a4b982b88dff8971a27e38 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 24 Jul 2025 01:46:59 +0200 Subject: [PATCH 5/8] Turbopack: improve named spans in tracing (#81458) ### What? Show span name before attribute `name` in tracing when using custom named spans. Include category and original span name when grouping spans. Use more custom named spans. --- turbopack/crates/turbo-tasks-fs/src/lib.rs | 26 +++++------ .../turbopack-browser/src/chunking_context.rs | 2 +- .../crates/turbopack-core/src/resolve/mod.rs | 4 +- .../crates/turbopack-ecmascript/src/lib.rs | 2 +- .../src/references/mod.rs | 2 +- .../turbopack-nodejs/src/chunking_context.rs | 2 +- .../turbopack-trace-server/src/bottom_up.rs | 33 +++++++++----- .../crates/turbopack-trace-server/src/lib.rs | 1 + .../crates/turbopack-trace-server/src/main.rs | 1 + .../crates/turbopack-trace-server/src/span.rs | 2 +- .../src/span_bottom_up_ref.rs | 11 +++-- .../src/span_graph_ref.rs | 13 +++--- .../turbopack-trace-server/src/span_ref.rs | 43 +++++++++++-------- .../src/string_tuple_ref.rs | 28 ++++++++++++ 14 files changed, 108 insertions(+), 62 deletions(-) create mode 100644 turbopack/crates/turbopack-trace-server/src/string_tuple_ref.rs diff --git a/turbopack/crates/turbo-tasks-fs/src/lib.rs b/turbopack/crates/turbo-tasks-fs/src/lib.rs index 83d718a52b1b2..a9238567bf886 100644 --- a/turbopack/crates/turbo-tasks-fs/src/lib.rs +++ b/turbopack/crates/turbo-tasks-fs/src/lib.rs @@ -309,7 +309,7 @@ impl DiskFileSystemInner { } fn invalidate(&self) { - let _span = tracing::info_span!("invalidate filesystem", path = &*self.root).entered(); + let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered(); let span = tracing::Span::current(); let handle = tokio::runtime::Handle::current(); let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap()); @@ -332,7 +332,7 @@ impl DiskFileSystemInner { &self, reason: impl Fn(String) -> R + Sync, ) { - let _span = tracing::info_span!("invalidate filesystem", path = &*self.root).entered(); + let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered(); let span = tracing::Span::current(); let handle = tokio::runtime::Handle::current(); let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap()); @@ -388,7 +388,7 @@ impl DiskFileSystemInner { // create the directory for the filesystem on disk, if it doesn't exist retry_blocking(root_path.clone(), move |path| { let _tracing = - tracing::info_span!("create root directory", path = display(path.display())) + tracing::info_span!("create root directory", name = display(path.display())) .entered(); std::fs::create_dir_all(path) @@ -413,7 +413,7 @@ impl DiskFileSystemInner { .concurrency_limited(&self.semaphore) .instrument(tracing::info_span!( "create directory", - path = display(directory.display()) + name = display(directory.display()) )) .await?; ApplyEffectsContext::with(|fs_context: &mut DiskFileSystemApplyContext| { @@ -558,7 +558,7 @@ impl FileSystem for DiskFileSystem { .concurrency_limited(&self.inner.semaphore) .instrument(tracing::info_span!( "read file", - path = display(full_path.display()) + name = display(full_path.display()) )) .await { @@ -583,7 +583,7 @@ impl FileSystem for DiskFileSystem { // node-file-trace let read_dir = match retry_blocking(full_path.clone(), |path| { let _span = - tracing::info_span!("read directory", path = display(path.display())).entered(); + tracing::info_span!("read directory", name = display(path.display())).entered(); std::fs::read_dir(path) }) .concurrency_limited(&self.inner.semaphore) @@ -640,7 +640,7 @@ impl FileSystem for DiskFileSystem { .concurrency_limited(&self.inner.semaphore) .instrument(tracing::info_span!( "read symlink", - path = display(full_path.display()) + name = display(full_path.display()) )) .await { @@ -745,7 +745,7 @@ impl FileSystem for DiskFileSystem { .concurrency_limited(&inner.semaphore) .instrument(tracing::info_span!( "read file before write", - path = display(full_path.display()) + name = display(full_path.display()) )) .await?; if compare == FileComparison::Equal { @@ -812,7 +812,7 @@ impl FileSystem for DiskFileSystem { .concurrency_limited(&inner.semaphore) .instrument(tracing::info_span!( "write file", - path = display(full_path.display()) + name = display(full_path.display()) )) .await .with_context(|| format!("failed to write to {}", full_path.display()))?; @@ -824,7 +824,7 @@ impl FileSystem for DiskFileSystem { .concurrency_limited(&inner.semaphore) .instrument(tracing::info_span!( "remove file", - path = display(full_path.display()) + name = display(full_path.display()) )) .await .or_else(|err| { @@ -873,7 +873,7 @@ impl FileSystem for DiskFileSystem { .concurrency_limited(&inner.semaphore) .instrument(tracing::info_span!( "read symlink before write", - path = display(full_path.display()) + name = display(full_path.display()) )) .await { @@ -923,7 +923,7 @@ impl FileSystem for DiskFileSystem { retry_blocking(target_path, move |target_path| { let _span = tracing::info_span!( "write symlink", - path = display(target_path.display()) + name = display(target_path.display()) ) .entered(); // we use the sync std method here because `symlink` is fast @@ -980,7 +980,7 @@ impl FileSystem for DiskFileSystem { .concurrency_limited(&self.inner.semaphore) .instrument(tracing::info_span!( "read metadata", - path = display(full_path.display()) + name = display(full_path.display()) )) .await .with_context(|| format!("reading metadata for {}", full_path.display()))?; diff --git a/turbopack/crates/turbopack-browser/src/chunking_context.rs b/turbopack/crates/turbopack-browser/src/chunking_context.rs index eeeb27aebb195..0b70e283cd3e0 100644 --- a/turbopack/crates/turbopack-browser/src/chunking_context.rs +++ b/turbopack/crates/turbopack-browser/src/chunking_context.rs @@ -571,7 +571,7 @@ impl ChunkingContext for BrowserChunkingContext { module_graph: Vc, availability_info: AvailabilityInfo, ) -> Result> { - let span = tracing::info_span!("chunking", ident = ident.to_string().await?.to_string()); + let span = tracing::info_span!("chunking", name = ident.to_string().await?.to_string()); async move { let this = self.await?; let modules = chunk_group.entries(); diff --git a/turbopack/crates/turbopack-core/src/resolve/mod.rs b/turbopack/crates/turbopack-core/src/resolve/mod.rs index 00b0b9805cfaa..ad3a1947df191 100644 --- a/turbopack/crates/turbopack-core/src/resolve/mod.rs +++ b/turbopack/crates/turbopack-core/src/resolve/mod.rs @@ -1573,7 +1573,7 @@ pub async fn resolve_inline( tracing::info_span!( "resolving", lookup_path = lookup_path, - request = request, + name = request, reference_type = display(&reference_type), ) }; @@ -1778,7 +1778,7 @@ async fn resolve_internal_inline( tracing::info_span!( "internal resolving", lookup_path = lookup_path, - request = request + name = request ) }; async move { diff --git a/turbopack/crates/turbopack-ecmascript/src/lib.rs b/turbopack/crates/turbopack-ecmascript/src/lib.rs index 33f6c48a2d95a..498efdee64ead 100644 --- a/turbopack/crates/turbopack-ecmascript/src/lib.rs +++ b/turbopack/crates/turbopack-ecmascript/src/lib.rs @@ -810,7 +810,7 @@ impl EcmascriptChunkItem for ModuleChunkItem { ) -> Result> { let span = tracing::info_span!( "code generation", - module = self.asset_ident().to_string().await?.to_string() + name = self.asset_ident().to_string().await?.to_string() ); async { let this = self.await?; diff --git a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs index 2383f46054a0f..d24e50a94dfda 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs @@ -495,7 +495,7 @@ pub(crate) async fn analyse_ecmascript_module( ) -> Result> { let span = { let module = module.ident().to_string().await?.to_string(); - tracing::info_span!("analyse ecmascript module", module = module) + tracing::info_span!("analyse ecmascript module", name = module) }; let result = analyse_ecmascript_module_internal(module, part) .instrument(span) diff --git a/turbopack/crates/turbopack-nodejs/src/chunking_context.rs b/turbopack/crates/turbopack-nodejs/src/chunking_context.rs index 57c992875d25b..535fd67cd6fbf 100644 --- a/turbopack/crates/turbopack-nodejs/src/chunking_context.rs +++ b/turbopack/crates/turbopack-nodejs/src/chunking_context.rs @@ -384,7 +384,7 @@ impl ChunkingContext for NodeJsChunkingContext { module_graph: Vc, availability_info: AvailabilityInfo, ) -> Result> { - let span = tracing::info_span!("chunking", module = ident.to_string().await?.to_string()); + let span = tracing::info_span!("chunking", name = ident.to_string().await?.to_string()); async move { let modules = chunk_group.entries(); let MakeChunkGroupResult { diff --git a/turbopack/crates/turbopack-trace-server/src/bottom_up.rs b/turbopack/crates/turbopack-trace-server/src/bottom_up.rs index 76397fc0ea558..c29ef2f5328d9 100644 --- a/turbopack/crates/turbopack-trace-server/src/bottom_up.rs +++ b/turbopack/crates/turbopack-trace-server/src/bottom_up.rs @@ -5,12 +5,13 @@ use hashbrown::HashMap; use crate::{ span::{SpanBottomUp, SpanIndex}, span_ref::SpanRef, + string_tuple_ref::StringTupleRef, }; pub struct SpanBottomUpBuilder { // These values won't change after creation: pub self_spans: Vec, - pub children: HashMap, + pub children: HashMap<(String, String), SpanBottomUpBuilder>, pub example_span: SpanIndex, } @@ -42,7 +43,7 @@ pub fn build_bottom_up_graph<'a>( .ok() .and_then(|s| s.parse().ok()) .unwrap_or(usize::MAX); - let mut roots: HashMap = HashMap::default(); + let mut roots: HashMap<(String, String), SpanBottomUpBuilder> = HashMap::default(); // unfortunately there is a rustc bug that fails the typechecking here // when using Either. This error appears @@ -52,30 +53,40 @@ pub fn build_bottom_up_graph<'a>( let mut current_iterators: Vec>>> = vec![Box::new(spans.flat_map(|span| span.children()))]; - let mut current_path: Vec<(&'_ str, SpanIndex)> = vec![]; + let mut current_path: Vec<((&'_ str, &'_ str), SpanIndex)> = vec![]; while let Some(mut iter) = current_iterators.pop() { if let Some(child) = iter.next() { current_iterators.push(iter); - let name = child.group_name(); + let (category, name) = child.group_name(); let (_, mut bottom_up) = roots .raw_entry_mut() - .from_key(name) - .or_insert_with(|| (name.to_string(), SpanBottomUpBuilder::new(child.index()))); + .from_key(&StringTupleRef(category, name)) + .or_insert_with(|| { + ( + (category.to_string(), name.to_string()), + SpanBottomUpBuilder::new(child.index()), + ) + }); bottom_up.self_spans.push(child.index()); let mut prev = None; - for &(name, example_span) in current_path.iter().rev().take(max_depth) { - if prev == Some(name) { + for &((category, title), example_span) in current_path.iter().rev().take(max_depth) { + if prev == Some((category, title)) { continue; } let (_, child_bottom_up) = bottom_up .children .raw_entry_mut() - .from_key(name) - .or_insert_with(|| (name.to_string(), SpanBottomUpBuilder::new(example_span))); + .from_key(&StringTupleRef(category, title)) + .or_insert_with(|| { + ( + (category.to_string(), title.to_string()), + SpanBottomUpBuilder::new(example_span), + ) + }); child_bottom_up.self_spans.push(child.index()); bottom_up = child_bottom_up; - prev = Some(name); + prev = Some((category, title)); } current_path.push((child.group_name(), child.index())); diff --git a/turbopack/crates/turbopack-trace-server/src/lib.rs b/turbopack/crates/turbopack-trace-server/src/lib.rs index ef164659a8a6e..0a08698545521 100644 --- a/turbopack/crates/turbopack-trace-server/src/lib.rs +++ b/turbopack/crates/turbopack-trace-server/src/lib.rs @@ -17,6 +17,7 @@ mod span_graph_ref; mod span_ref; mod store; mod store_container; +mod string_tuple_ref; mod timestamp; mod u64_empty_string; mod u64_string; diff --git a/turbopack/crates/turbopack-trace-server/src/main.rs b/turbopack/crates/turbopack-trace-server/src/main.rs index c3f9ddaa96549..7800b96a8d6ad 100644 --- a/turbopack/crates/turbopack-trace-server/src/main.rs +++ b/turbopack/crates/turbopack-trace-server/src/main.rs @@ -18,6 +18,7 @@ mod span_graph_ref; mod span_ref; mod store; mod store_container; +mod string_tuple_ref; mod timestamp; mod u64_empty_string; mod u64_string; diff --git a/turbopack/crates/turbopack-trace-server/src/span.rs b/turbopack/crates/turbopack-trace-server/src/span.rs index 0cca4825fbb42..1fb66072851e1 100644 --- a/turbopack/crates/turbopack-trace-server/src/span.rs +++ b/turbopack/crates/turbopack-trace-server/src/span.rs @@ -71,7 +71,7 @@ pub struct SpanExtra { pub struct SpanNames { // These values are computed when accessed (and maybe deleted during writing): pub nice_name: OnceLock<(String, String)>, - pub group_name: OnceLock, + pub group_name: OnceLock<(String, String)>, } impl Span { diff --git a/turbopack/crates/turbopack-trace-server/src/span_bottom_up_ref.rs b/turbopack/crates/turbopack-trace-server/src/span_bottom_up_ref.rs index f152d22d44eb0..02f36e3e96efb 100644 --- a/turbopack/crates/turbopack-trace-server/src/span_bottom_up_ref.rs +++ b/turbopack/crates/turbopack-trace-server/src/span_bottom_up_ref.rs @@ -6,9 +6,9 @@ use std::{ use crate::{ FxIndexMap, - span::{SpanBottomUp, SpanGraphEvent, SpanIndex}, + span::{SpanBottomUp, SpanGraphEvent}, span_graph_ref::{SpanGraphEventRef, SpanGraphRef, event_map_to_list}, - span_ref::SpanRef, + span_ref::{GroupNameToDirectAndRecusiveSpans, SpanRef}, store::{SpanId, Store}, timestamp::Timestamp, }; @@ -54,7 +54,7 @@ impl<'a> SpanBottomUpRef<'a> { self.bottom_up.self_spans.len() } - pub fn group_name(&self) -> &'a str { + pub fn group_name(&self) -> (&'a str, &'a str) { self.first_span().group_name() } @@ -62,7 +62,7 @@ impl<'a> SpanBottomUpRef<'a> { if self.count() == 1 { self.example_span().nice_name() } else { - ("", self.example_span().group_name()) + self.example_span().group_name() } } @@ -85,8 +85,7 @@ impl<'a> SpanBottomUpRef<'a> { let _ = self.first_span().graph(); self.first_span().extra().graph.get().unwrap().clone() } else { - let mut map: FxIndexMap<&str, (Vec, Vec)> = - FxIndexMap::default(); + let mut map: GroupNameToDirectAndRecusiveSpans = FxIndexMap::default(); let mut queue = VecDeque::with_capacity(8); for child in self.spans() { let name = child.group_name(); diff --git a/turbopack/crates/turbopack-trace-server/src/span_graph_ref.rs b/turbopack/crates/turbopack-trace-server/src/span_graph_ref.rs index 31e25659e3a00..3538f68a40aaf 100644 --- a/turbopack/crates/turbopack-trace-server/src/span_graph_ref.rs +++ b/turbopack/crates/turbopack-trace-server/src/span_graph_ref.rs @@ -9,9 +9,9 @@ use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use crate::{ FxIndexMap, bottom_up::build_bottom_up_graph, - span::{SpanGraph, SpanGraphEvent, SpanIndex}, + span::{SpanGraph, SpanGraphEvent}, span_bottom_up_ref::SpanBottomUpRef, - span_ref::SpanRef, + span_ref::{GroupNameToDirectAndRecusiveSpans, SpanRef}, store::{SpanId, Store}, timestamp::Timestamp, }; @@ -40,7 +40,7 @@ impl<'a> SpanGraphRef<'a> { if self.count() == 1 { self.first_span().nice_name() } else { - ("", self.first_span().group_name()) + self.first_span().group_name() } } @@ -87,8 +87,7 @@ impl<'a> SpanGraphRef<'a> { self.first_span().extra().graph.get().unwrap().clone() } else { let self_group = self.first_span().group_name(); - let mut map: FxIndexMap<&str, (Vec, Vec)> = - FxIndexMap::default(); + let mut map: GroupNameToDirectAndRecusiveSpans = FxIndexMap::default(); let mut queue = VecDeque::with_capacity(8); for span in self.recursive_spans() { for span in span.children() { @@ -303,9 +302,7 @@ impl<'a> SpanGraphRef<'a> { } } -pub fn event_map_to_list( - map: FxIndexMap<&str, (Vec, Vec)>, -) -> Vec { +pub fn event_map_to_list(map: GroupNameToDirectAndRecusiveSpans) -> Vec { map.into_iter() .map(|(_, (root_spans, recursive_spans))| { let graph = SpanGraph { diff --git a/turbopack/crates/turbopack-trace-server/src/span_ref.rs b/turbopack/crates/turbopack-trace-server/src/span_ref.rs index 124b94727043f..25adc8a234cb6 100644 --- a/turbopack/crates/turbopack-trace-server/src/span_ref.rs +++ b/turbopack/crates/turbopack-trace-server/src/span_ref.rs @@ -19,6 +19,9 @@ use crate::{ timestamp::Timestamp, }; +pub type GroupNameToDirectAndRecusiveSpans<'l> = + FxIndexMap<(&'l str, &'l str), (Vec, Vec)>; + #[derive(Copy, Clone)] pub struct SpanRef<'a> { pub(crate) span: &'a Span, @@ -89,18 +92,17 @@ impl<'a> SpanRef<'a> { .find(|&(k, _)| k == "name") .map(|(_, v)| v.as_str()) { - if matches!( + if matches!(self.span.name.as_str(), "turbo_tasks::function") { + (self.span.name.clone(), name.to_string()) + } else if matches!( self.span.name.as_str(), "turbo_tasks::resolve_call" | "turbo_tasks::resolve_trait_call" ) { - ( - format!("{} {}", self.span.name, self.span.category), - format!("*{name}"), - ) + (self.span.name.clone(), format!("*{name}")) } else { ( - format!("{} {}", self.span.name, self.span.category), - name.to_string(), + self.span.category.clone(), + format!("{} {name}", self.span.name), ) } } else { @@ -110,29 +112,37 @@ impl<'a> SpanRef<'a> { (category, title) } - pub fn group_name(&self) -> &'a str { - self.names().group_name.get_or_init(|| { + pub fn group_name(&self) -> (&'a str, &'a str) { + let (category, title) = self.names().group_name.get_or_init(|| { if matches!(self.span.name.as_str(), "turbo_tasks::function") { - self.span + let name = self + .span .args .iter() .find(|&(k, _)| k == "name") .map(|(_, v)| v.to_string()) - .unwrap_or_else(|| self.span.name.to_string()) + .unwrap_or_else(|| self.span.name.to_string()); + (self.span.name.clone(), name) } else if matches!( self.span.name.as_str(), "turbo_tasks::resolve_call" | "turbo_tasks::resolve_trait_call" ) { - self.span + let name = self + .span .args .iter() .find(|&(k, _)| k == "name") .map(|(_, v)| format!("*{v}")) - .unwrap_or_else(|| self.span.name.to_string()) + .unwrap_or_else(|| self.span.name.to_string()); + ( + self.span.category.clone(), + format!("{} {name}", self.span.name), + ) } else { - self.span.name.to_string() + (self.span.category.clone(), self.span.name.clone()) } - }) + }); + (category.as_str(), title.as_str()) } pub fn args(&self) -> impl Iterator { @@ -349,8 +359,7 @@ impl<'a> SpanRef<'a> { Entry { span, recursive } }) .collect_vec_list(); - let mut map: FxIndexMap<&str, (Vec, Vec)> = - FxIndexMap::default(); + let mut map: GroupNameToDirectAndRecusiveSpans = FxIndexMap::default(); for Entry { span, mut recursive, diff --git a/turbopack/crates/turbopack-trace-server/src/string_tuple_ref.rs b/turbopack/crates/turbopack-trace-server/src/string_tuple_ref.rs new file mode 100644 index 0000000000000..4b7a24a754253 --- /dev/null +++ b/turbopack/crates/turbopack-trace-server/src/string_tuple_ref.rs @@ -0,0 +1,28 @@ +use hashbrown::Equivalent; + +#[derive(Hash)] +pub struct StringTupleRef<'a>(pub &'a str, pub &'a str); + +impl<'a> Equivalent<(String, String)> for StringTupleRef<'a> { + fn equivalent(&self, other: &(String, String)) -> bool { + self.0 == other.0 && self.1 == other.1 + } +} + +#[cfg(test)] +mod string_tuple_ref_tests { + use std::hash::RandomState; + + use super::*; + + #[test] + fn test_string_tuple_ref_hash() { + use std::hash::BuildHasher; + + let s = RandomState::new(); + assert_eq!( + s.hash_one(StringTupleRef("abc", "def")), + s.hash_one(&("abc".to_string(), "def".to_string())) + ); + } +} From 63dba672268b8ab57e93cac59c64b2b0e6ba597d Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 24 Jul 2025 02:23:21 +0200 Subject: [PATCH 6/8] Turbopack: update mimalloc (#81993) ### What? This fixes a bug where memory is not released back to the OS. --- Cargo.lock | 8 ++++---- turbopack/crates/turbo-tasks-malloc/Cargo.toml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b4d54706d069..8180ae40586cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3655,9 +3655,9 @@ dependencies = [ [[package]] name = "libmimalloc-sys" -version = "0.1.38" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7bb23d733dfcc8af652a78b7bf232f0e967710d044732185e561e47c0336b6" +checksum = "bf88cd67e9de251c1781dbe2f641a1a3ad66eaae831b8a2c38fbdc5ddae16d4d" dependencies = [ "cc", "libc", @@ -4034,9 +4034,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.42" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9186d86b79b52f4a77af65604b51225e8db1d6ee7e3f41aec1e40829c71a176" +checksum = "b1791cbe101e95af5764f06f20f6760521f7158f69dbf9d6baf941ee1bf6bc40" dependencies = [ "libmimalloc-sys", ] diff --git a/turbopack/crates/turbo-tasks-malloc/Cargo.toml b/turbopack/crates/turbo-tasks-malloc/Cargo.toml index b3331d1286798..1743f2329a001 100644 --- a/turbopack/crates/turbo-tasks-malloc/Cargo.toml +++ b/turbopack/crates/turbo-tasks-malloc/Cargo.toml @@ -10,10 +10,10 @@ autobenches = false bench = false [target.'cfg(not(any(target_os = "linux", target_family = "wasm", target_env = "musl")))'.dependencies] -mimalloc = { version = "0.1.42", features = [], optional = true } +mimalloc = { version = "0.1.47", features = [], optional = true } [target.'cfg(all(target_os = "linux", not(any(target_family = "wasm", target_env = "musl"))))'.dependencies] -mimalloc = { version = "0.1.42", features = [ +mimalloc = { version = "0.1.47", features = [ "local_dynamic_tls", ], optional = true } From 7a183a0f3f035d4e31bfa66f1c5ab037d56adec2 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 23 Jul 2025 20:45:00 -0400 Subject: [PATCH 7/8] Fix: Unresolved param in x-nextjs-rewritten-query (#81991) Fixes an issue with the x-nextjs-rewritten-query header where it responded without the dynamic parts of the URL filled in. For example, when rewriting to `/hello?id=123` to `/hello/123`: - Before: `x-nextjs-rewritten-query: id=__ESC_COLON_id` - After: `x-nextjs-rewritten-query: id=123` --- .../server/lib/router-utils/resolve-routes.ts | 22 +++++-------- .../lib/router/utils/prepare-destination.ts | 16 ++++++++-- .../app-dir/rewrite-headers/next.config.js | 4 +++ .../rewrite-headers/rewrite-headers.test.ts | 31 +++++++++++++++++++ 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/packages/next/src/server/lib/router-utils/resolve-routes.ts b/packages/next/src/server/lib/router-utils/resolve-routes.ts index c9c9ed1064bb3..7b3f5c05ced75 100644 --- a/packages/next/src/server/lib/router-utils/resolve-routes.ts +++ b/packages/next/src/server/lib/router-utils/resolve-routes.ts @@ -33,7 +33,6 @@ import { addRequestMeta } from '../../request-meta' import { compileNonPath, matchHas, - parseDestination, prepareDestination, } from '../../../shared/lib/router/utils/prepare-destination' import type { TLSSocket } from 'tls' @@ -761,16 +760,6 @@ export function getResolveRoutes( // so we'll just use the params from the route matcher } - // We extract the search params of the destination so we can set it on - // the response headers. We don't want to use the following - // `parsedDestination` as the query object is mutated. - const { search: destinationSearch, pathname: destinationPathname } = - parseDestination({ - destination: route.destination, - params: rewriteParams, - query: parsedUrl.query, - }) - const { parsedDestination } = prepareDestination({ appendParamsToQuery: true, destination: route.destination, @@ -790,14 +779,17 @@ export function getResolveRoutes( if (req.headers[RSC_HEADER.toLowerCase()] === '1') { // We set the rewritten path and query headers on the response now // that we know that the it's not an external rewrite. - if (parsedUrl.pathname !== destinationPathname) { - res.setHeader(NEXT_REWRITTEN_PATH_HEADER, destinationPathname) + if (parsedUrl.pathname !== parsedDestination.pathname) { + res.setHeader( + NEXT_REWRITTEN_PATH_HEADER, + parsedDestination.pathname + ) } - if (destinationSearch) { + if (parsedUrl.search !== parsedDestination.search) { res.setHeader( NEXT_REWRITTEN_QUERY_HEADER, // remove the leading ? from the search - destinationSearch.slice(1) + parsedDestination.search.slice(1) ) } } diff --git a/packages/next/src/shared/lib/router/utils/prepare-destination.ts b/packages/next/src/shared/lib/router/utils/prepare-destination.ts index 25f3d26ad31f2..9cc4320a291a0 100644 --- a/packages/next/src/shared/lib/router/utils/prepare-destination.ts +++ b/packages/next/src/shared/lib/router/utils/prepare-destination.ts @@ -193,12 +193,18 @@ export function parseDestination(args: { hash = unescapeSegments(hash) } + let search = parsed.search + if (search) { + search = unescapeSegments(search) + } + return { ...parsed, pathname, hostname, href, hash, + search, } } @@ -210,7 +216,11 @@ export function prepareDestination(args: { }) { const parsedDestination = parseDestination(args) - const { hostname: destHostname, query: destQuery } = parsedDestination + const { + hostname: destHostname, + query: destQuery, + search: destSearch, + } = parsedDestination // The following code assumes that the pathname here includes the hash if it's // present. @@ -311,7 +321,9 @@ export function prepareDestination(args: { } parsedDestination.pathname = pathname parsedDestination.hash = `${hash ? '#' : ''}${hash || ''}` - delete (parsedDestination as any).search + parsedDestination.search = destSearch + ? compileNonPath(destSearch, args.params) + : '' } catch (err: any) { if (err.message.match(/Expected .*? to not repeat, but got an array/)) { throw new Error( diff --git a/test/e2e/app-dir/rewrite-headers/next.config.js b/test/e2e/app-dir/rewrite-headers/next.config.js index f994c79c6f8a1..26712a0385318 100644 --- a/test/e2e/app-dir/rewrite-headers/next.config.js +++ b/test/e2e/app-dir/rewrite-headers/next.config.js @@ -18,6 +18,10 @@ module.exports = { source: '/hello/(.*)/google', destination: 'https://www.google.$1/', }, + { + source: '/rewrites-to-query/:name', + destination: '/other?key=:name', + }, ] }, } diff --git a/test/e2e/app-dir/rewrite-headers/rewrite-headers.test.ts b/test/e2e/app-dir/rewrite-headers/rewrite-headers.test.ts index 3384d52ece8a0..9f4247ab1e870 100644 --- a/test/e2e/app-dir/rewrite-headers/rewrite-headers.test.ts +++ b/test/e2e/app-dir/rewrite-headers/rewrite-headers.test.ts @@ -368,6 +368,37 @@ const cases: { 'x-nextjs-rewritten-query': 'key=value', }, }, + { + name: 'next.config.js rewrites with path-matched query HTML', + pathname: '/rewrites-to-query/andrew', + expected: { + 'x-nextjs-rewritten-path': null, + 'x-nextjs-rewritten-query': null, + }, + }, + { + name: 'next.config.js rewrites with path-matched query RSC', + pathname: '/rewrites-to-query/andrew', + headers: { + RSC: '1', + }, + expected: { + 'x-nextjs-rewritten-path': '/other', + 'x-nextjs-rewritten-query': 'key=andrew', + }, + }, + { + name: 'next.config.js rewrites with path-matched query Prefetch RSC', + pathname: '/rewrites-to-query/andrew', + headers: { + RSC: '1', + 'Next-Router-Prefetch': '1', + }, + expected: { + 'x-nextjs-rewritten-path': '/other', + 'x-nextjs-rewritten-query': 'key=andrew', + }, + }, ] describe('rewrite-headers', () => { From 56fed4d6b489b9b60cd3b7647b21597f09768be9 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Wed, 23 Jul 2025 18:02:54 -0700 Subject: [PATCH 8/8] Turbopack: Add an option to use system TLS certificates (fixes #79060, fixes #79059) (#81818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It's common in enterprise environments for employers to MITM all HTTPS traffic on employee machines to enforce network policies or to detect and block malware. For this to work, they install custom CA roots into the system store. Applications must read from this store. The default behavior of `reqwests` is to bundle the mozilla CA roots into the application, and only trust those. This is reasonable given some of the tradeoffs of the current rustls resolver implementations they use, but long-term it's better for most applications to just use the system CA store. This provides an opt-in experimental option for using the system CA store. We may use system CA certs by default in the future once https://github.com/seanmonstar/reqwest/issues/2159 is resolved. Fixes #79059 Fixes #79060 ### Testing - Install [mitmproxy](https://docs.mitmproxy.org/stable/overview/installation/). - Install the generated mitmproxy CA cert to the system store: https://docs.mitmproxy.org/stable/concepts/certificates/ - Run `./mitmdump` (with no arguments) - Run an example app that uses google fonts: ``` pnpm pack-next --project ~/shadcn-ui/apps/v4 --no-js-build -- --release cd ~/shadcn-ui/apps/v4 pnpm i NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS=0 HTTP_PROXY=localhost:8080 HTTPS_PROXY=localhost:8080 pnpm dev NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS=1 HTTP_PROXY=localhost:8080 HTTPS_PROXY=localhost:8080 pnpm dev ``` When system TLS certs are disabled, we get warnings like this: ``` ⚠ [next]/internal/font/google/geist_mono_77f2790.module.css Error while requesting resource There was an issue establishing a connection while requesting https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400&display=swap. Import trace: [next]/internal/font/google/geist_mono_77f2790.module.css [next]/internal/font/google/geist_mono_77f2790.js ./apps/v4/lib/fonts.ts ./apps/v4/app/layout.tsx ``` And mitmproxy shows the handshakes failing ![Screenshot 2025-07-21 at 1.11.11 PM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/HAZVitxRNnZz8QMiPn4a/2efc4b97-f50e-412c-9daf-5cdd00a6d82b.png) When system TLS certs are enabled, the app works. --- .changeset/silent-houses-lay.md | 5 +++ Cargo.lock | 30 ++++++++++++-- Cargo.toml | 2 +- crates/next-core/src/next_config.rs | 32 ++++++++++++++- crates/next-core/src/next_font/google/mod.rs | 37 ++++++++++++++--- crates/next-core/src/next_import_map.rs | 4 +- packages/next/src/server/config-schema.ts | 20 +++++++++ turbopack/crates/turbo-tasks-fetch/Cargo.toml | 3 +- .../src/reqwest_client_cache.rs | 34 +++++++++++++++ .../crates/turbo-tasks-fetch/tests/fetch.rs | 41 ++++++++++++------- 10 files changed, 180 insertions(+), 28 deletions(-) create mode 100644 .changeset/silent-houses-lay.md diff --git a/.changeset/silent-houses-lay.md b/.changeset/silent-houses-lay.md new file mode 100644 index 0000000000000..41d24de307ca3 --- /dev/null +++ b/.changeset/silent-houses-lay.md @@ -0,0 +1,5 @@ +--- +'@next/swc': patch +--- + +Added an experimental option for using the system CA store for fetching Google Fonts in Turbopack diff --git a/Cargo.lock b/Cargo.lock index 8180ae40586cf..28d5eaf6bf1a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2869,6 +2869,7 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", @@ -5945,9 +5946,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.20" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64 0.22.1", "bytes", @@ -5966,6 +5967,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls", + "rustls-native-certs", "rustls-pki-types", "serde", "serde_json", @@ -6194,6 +6196,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.10.1" @@ -9313,7 +9337,7 @@ dependencies = [ "anyhow", "mockito", "quick_cache", - "reqwest 0.12.20", + "reqwest 0.12.22", "serde", "tokio", "turbo-rcstr", diff --git a/Cargo.toml b/Cargo.toml index beb1e1c57e32f..f0ba997758b03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -400,7 +400,7 @@ rand = "0.9.0" rayon = "1.10.0" regex = "1.10.6" regress = "0.10.3" -reqwest = { version = "0.12.20", default-features = false } +reqwest = { version = "0.12.22", default-features = false } ringmap = "0.1.3" roaring = "0.10.10" rstest = "0.16.0" diff --git a/crates/next-core/src/next_config.rs b/crates/next-core/src/next_config.rs index 627443e51c692..3f8d1f853c7fa 100644 --- a/crates/next-core/src/next_config.rs +++ b/crates/next-core/src/next_config.rs @@ -3,12 +3,13 @@ use rustc_hash::FxHashSet; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value as JsonValue; use turbo_esregex::EsRegex; -use turbo_rcstr::RcStr; +use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ FxIndexMap, NonLocalValue, OperationValue, ResolvedVc, TaskInput, Vc, debug::ValueDebugFormat, trace::TraceRawVcs, }; -use turbo_tasks_env::EnvMap; +use turbo_tasks_env::{EnvMap, ProcessEnv}; +use turbo_tasks_fetch::ReqwestClientConfig; use turbo_tasks_fs::FileSystemPath; use turbopack::module_options::{ ConditionItem, ConditionPath, LoaderRuleItem, OptionWebpackRules, @@ -801,6 +802,7 @@ pub struct ExperimentalConfig { turbopack_source_maps: Option, turbopack_tree_shaking: Option, turbopack_scope_hoisting: Option, + turbopack_use_system_tls_certs: Option, // Whether to enable the global-not-found convention global_not_found: Option, /// Defaults to false in development mode, true in production mode. @@ -1721,6 +1723,32 @@ impl NextConfig { pub fn output_file_tracing_excludes(&self) -> Vc { Vc::cell(self.output_file_tracing_excludes.clone()) } + + #[turbo_tasks::function] + pub async fn reqwest_client_config( + &self, + env: Vc>, + ) -> Result> { + // Support both an env var and the experimental flag to provide more flexibility to + // developers on locked down systems, depending on if they want to configure this on a + // per-system or per-project basis. + let use_system_tls_certs = env + .read(rcstr!("NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS")) + .await? + .as_ref() + .and_then(|env_value| { + // treat empty value same as an unset value + (!env_value.is_empty()).then(|| env_value == "1" || env_value == "true") + }) + .or(self.experimental.turbopack_use_system_tls_certs) + .unwrap_or(false); + Ok(ReqwestClientConfig { + proxy: None, + tls_built_in_webpki_certs: !use_system_tls_certs, + tls_built_in_native_certs: use_system_tls_certs, + } + .cell()) + } } /// A subset of ts/jsconfig that next.js implicitly diff --git a/crates/next-core/src/next_font/google/mod.rs b/crates/next-core/src/next_font/google/mod.rs index 85ac1e7b42e02..cf59798e14526 100644 --- a/crates/next-core/src/next_font/google/mod.rs +++ b/crates/next-core/src/next_font/google/mod.rs @@ -182,6 +182,7 @@ pub struct NextFontGoogleCssModuleReplacer { project_path: FileSystemPath, execution_context: ResolvedVc, next_mode: ResolvedVc, + reqwest_client_config: ResolvedVc, } #[turbo_tasks::value_impl] @@ -191,11 +192,13 @@ impl NextFontGoogleCssModuleReplacer { project_path: FileSystemPath, execution_context: ResolvedVc, next_mode: ResolvedVc, + reqwest_client_config: ResolvedVc, ) -> Vc { Self::cell(NextFontGoogleCssModuleReplacer { project_path, execution_context, next_mode, + reqwest_client_config, }) } @@ -228,7 +231,14 @@ impl NextFontGoogleCssModuleReplacer { let stylesheet_str = mocked_responses_path .as_ref() .map_or_else( - || fetch_real_stylesheet(stylesheet_url.clone(), css_virtual_path.clone()).boxed(), + || { + fetch_real_stylesheet( + stylesheet_url.clone(), + css_virtual_path.clone(), + *self.reqwest_client_config, + ) + .boxed() + }, |p| get_mock_stylesheet(stylesheet_url.clone(), p, *self.execution_context).boxed(), ) .await?; @@ -365,13 +375,20 @@ struct NextFontGoogleFontFileOptions { #[turbo_tasks::value(shared)] pub struct NextFontGoogleFontFileReplacer { project_path: FileSystemPath, + reqwest_client_config: ResolvedVc, } #[turbo_tasks::value_impl] impl NextFontGoogleFontFileReplacer { #[turbo_tasks::function] - pub fn new(project_path: FileSystemPath) -> Vc { - Self::cell(NextFontGoogleFontFileReplacer { project_path }) + pub fn new( + project_path: FileSystemPath, + reqwest_client_config: ResolvedVc, + ) -> Vc { + Self::cell(NextFontGoogleFontFileReplacer { + project_path, + reqwest_client_config, + }) } } @@ -426,7 +443,12 @@ impl ImportMappingReplacement for NextFontGoogleFontFileReplacer { // doesn't seem ideal to download the font into a string, but probably doesn't // really matter either. - let Some(font) = fetch_from_google_fonts(url.into(), font_virtual_path.clone()).await? + let Some(font) = fetch_from_google_fonts( + url.into(), + font_virtual_path.clone(), + *self.reqwest_client_config, + ) + .await? else { return Ok(ImportMapResult::Result(ResolveResult::unresolvable()).cell()); }; @@ -650,8 +672,10 @@ fn font_file_options_from_query_map(query: &RcStr) -> Result, ) -> Result>> { - let body = fetch_from_google_fonts(stylesheet_url, css_virtual_path).await?; + let body = + fetch_from_google_fonts(stylesheet_url, css_virtual_path, reqwest_client_config).await?; Ok(body.map(|body| body.to_string())) } @@ -659,11 +683,12 @@ async fn fetch_real_stylesheet( async fn fetch_from_google_fonts( url: RcStr, virtual_path: FileSystemPath, + reqwest_client_config: Vc, ) -> Result>> { let result = fetch( url, Some(rcstr!(USER_AGENT_FOR_GOOGLE_FONTS)), - ReqwestClientConfig { proxy: None }.cell(), + reqwest_client_config, ) .await?; diff --git a/crates/next-core/src/next_import_map.rs b/crates/next-core/src/next_import_map.rs index 6685ba4f6d39e..83e9a623a6cea 100644 --- a/crates/next-core/src/next_import_map.rs +++ b/crates/next-core/src/next_import_map.rs @@ -1022,6 +1022,7 @@ async fn insert_next_shared_aliases( next_font_google_replacer_mapping, ); + let reqwest_client_config = next_config.reqwest_client_config(execution_context.env()); import_map.insert_alias( AliasPattern::exact("@vercel/turbopack-next/internal/font/google/cssmodule.module.css"), ImportMapping::Dynamic(ResolvedVc::upcast( @@ -1029,6 +1030,7 @@ async fn insert_next_shared_aliases( project_path.clone(), execution_context, next_mode, + reqwest_client_config, ) .to_resolved() .await?, @@ -1039,7 +1041,7 @@ async fn insert_next_shared_aliases( import_map.insert_alias( AliasPattern::exact(GOOGLE_FONTS_INTERNAL_PREFIX), ImportMapping::Dynamic(ResolvedVc::upcast( - NextFontGoogleFontFileReplacer::new(project_path.clone()) + NextFontGoogleFontFileReplacer::new(project_path.clone(), reqwest_client_config) .to_resolved() .await?, )) diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index e971530f4ff41..e37b2487980e6 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -462,6 +462,26 @@ export const configSchema: zod.ZodType = z.lazy(() => turbopackTreeShaking: z.boolean().optional(), turbopackRemoveUnusedExports: z.boolean().optional(), turbopackScopeHoisting: z.boolean().optional(), + /** + * Use the system-provided CA roots instead of bundled CA roots for external HTTPS requests + * made by Turbopack. Currently this is only used for fetching data from Google Fonts. + * + * This may be useful in cases where you or an employer are MITMing traffic. + * + * This option is experimental because: + * - This may cause small performance problems, as it uses [`rustls-native-certs`]( + * https://github.com/rustls/rustls-native-certs). + * - In the future, this may become the default, and this option may be eliminated, once + * is resolved. + * + * Users who need to configure this behavior system-wide can override the project + * configuration using the `NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS=1` environment + * variable. + * + * This option is ignored on Windows on ARM, where the native TLS implementation is always + * used. + */ + turbopackUseSystemTlsCerts: z.boolean().optional(), optimizePackageImports: z.array(z.string()).optional(), optimizeServerReact: z.boolean().optional(), clientTraceMetadata: z.array(z.string()).optional(), diff --git a/turbopack/crates/turbo-tasks-fetch/Cargo.toml b/turbopack/crates/turbo-tasks-fetch/Cargo.toml index 5aec449b4fda8..2113a046d64c0 100644 --- a/turbopack/crates/turbo-tasks-fetch/Cargo.toml +++ b/turbopack/crates/turbo-tasks-fetch/Cargo.toml @@ -20,8 +20,9 @@ workspace = true [target.'cfg(all(target_os = "windows", target_arch = "aarch64"))'.dependencies] reqwest = { workspace = true, features = ["native-tls"] } +# If you modify this cfg, make sure you update `ReqwestClientConfig::try_build`. [target.'cfg(not(any(all(target_os = "windows", target_arch = "aarch64"), target_arch="wasm32")))'.dependencies] -reqwest = { workspace = true, features = ["rustls-tls"] } +reqwest = { workspace = true, features = ["rustls-tls-webpki-roots", "rustls-tls-native-roots"] } [dependencies] anyhow = { workspace = true } diff --git a/turbopack/crates/turbo-tasks-fetch/src/reqwest_client_cache.rs b/turbopack/crates/turbo-tasks-fetch/src/reqwest_client_cache.rs index 2a695dc640f9d..4c9e278bd2b83 100644 --- a/turbopack/crates/turbo-tasks-fetch/src/reqwest_client_cache.rs +++ b/turbopack/crates/turbo-tasks-fetch/src/reqwest_client_cache.rs @@ -26,6 +26,29 @@ pub enum ProxyConfig { #[derive(Hash)] pub struct ReqwestClientConfig { pub proxy: Option, + /// Whether to load embedded webpki root certs with rustls. Default is true. + /// + /// Ignored for: + /// - Windows on ARM, which uses `native-tls` instead of `rustls-tls`. + /// - Ignored for WASM targets, which use the runtime's TLS implementation. + pub tls_built_in_webpki_certs: bool, + /// Whether to load native root certs using the `rustls-native-certs` crate. This may make + /// reqwest client initialization slower, so it's not used by default. + /// + /// Ignored for: + /// - Windows on ARM, which uses `native-tls` instead of `rustls-tls`. + /// - Ignored for WASM targets, which use the runtime's TLS implementation. + pub tls_built_in_native_certs: bool, +} + +impl Default for ReqwestClientConfig { + fn default() -> Self { + Self { + proxy: None, + tls_built_in_webpki_certs: true, + tls_built_in_native_certs: false, + } + } } impl ReqwestClientConfig { @@ -40,6 +63,17 @@ impl ReqwestClientConfig { } None => {} }; + + // make sure this cfg matches the one in `Cargo.toml`! + #[cfg(not(any( + all(target_os = "windows", target_arch = "aarch64"), + target_arch = "wasm32" + )))] + let client_builder = client_builder + .tls_built_in_root_certs(false) + .tls_built_in_webpki_certs(self.tls_built_in_webpki_certs) + .tls_built_in_native_certs(self.tls_built_in_native_certs); + client_builder.build() } } diff --git a/turbopack/crates/turbo-tasks-fetch/tests/fetch.rs b/turbopack/crates/turbo-tasks-fetch/tests/fetch.rs index d32431550cd0d..31fd898a14aa4 100644 --- a/turbopack/crates/turbo-tasks-fetch/tests/fetch.rs +++ b/turbopack/crates/turbo-tasks-fetch/tests/fetch.rs @@ -6,7 +6,7 @@ use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::Vc; use turbo_tasks_fetch::{ __test_only_reqwest_client_cache_clear, __test_only_reqwest_client_cache_len, FetchErrorKind, - ProxyConfig, ReqwestClientConfig, fetch, + ReqwestClientConfig, fetch, }; use turbo_tasks_fs::{DiskFileSystem, FileSystem, FileSystemPath}; use turbo_tasks_testing::{Registration, register, run}; @@ -29,7 +29,7 @@ async fn basic_get() { .create_async() .await; - let config_vc = ReqwestClientConfig { proxy: None }.cell(); + let config_vc = ReqwestClientConfig::default().cell(); let response = &*fetch( RcStr::from(format!("{}/foo.woff", server.url())), /* user_agent */ None, @@ -63,7 +63,7 @@ async fn sends_user_agent() { eprintln!("{}", server.url()); - let config_vc = ReqwestClientConfig { proxy: None }.cell(); + let config_vc = ReqwestClientConfig::default().cell(); let response = &*fetch( RcStr::from(format!("{}/foo.woff", server.url())), Some(rcstr!("mock-user-agent")), @@ -98,7 +98,7 @@ async fn invalidation_does_not_invalidate() { .await; let url = RcStr::from(format!("{}/foo.woff", server.url())); - let config_vc = ReqwestClientConfig { proxy: None }.cell(); + let config_vc = ReqwestClientConfig::default().cell(); let response = &*fetch(url.clone(), /* user_agent */ None, config_vc) .await? .unwrap() @@ -136,7 +136,7 @@ async fn errors_on_failed_connection() { // `ECONNREFUSED`. // Other values (e.g. domain name, reserved IP address block) may result in long timeouts. let url = rcstr!("http://127.0.0.1:0/foo.woff"); - let config_vc = ReqwestClientConfig { proxy: None }.cell(); + let config_vc = ReqwestClientConfig::default().cell(); let response_vc = fetch(url.clone(), None, config_vc); let err_vc = &*response_vc.await?.unwrap_err(); let err = err_vc.await?; @@ -171,7 +171,7 @@ async fn errors_on_404() { .await; let url = RcStr::from(server.url()); - let config_vc = ReqwestClientConfig { proxy: None }.cell(); + let config_vc = ReqwestClientConfig::default().cell(); let response_vc = fetch(url.clone(), None, config_vc); let err_vc = &*response_vc.await?.unwrap_err(); let err = err_vc.await?; @@ -225,26 +225,39 @@ async fn client_cache() { __test_only_reqwest_client_cache_clear(); assert_eq!(__test_only_reqwest_client_cache_len(), 0); - simple_fetch("/foo", ReqwestClientConfig { proxy: None }) - .await - .unwrap(); + simple_fetch( + "/foo", + ReqwestClientConfig { + tls_built_in_native_certs: false, + ..Default::default() + }, + ) + .await + .unwrap(); assert_eq!(__test_only_reqwest_client_cache_len(), 1); // the client is reused if the config is the same (by equality) - simple_fetch("/bar", ReqwestClientConfig { proxy: None }) - .await - .unwrap(); + simple_fetch( + "/bar", + ReqwestClientConfig { + tls_built_in_native_certs: false, + ..Default::default() + }, + ) + .await + .unwrap(); assert_eq!(__test_only_reqwest_client_cache_len(), 1); // the client is recreated if the config is different simple_fetch( "/bar", ReqwestClientConfig { - proxy: Some(ProxyConfig::Http(RcStr::from("http://127.0.0.1:0/"))), + tls_built_in_native_certs: true, + ..Default::default() }, ) .await - .unwrap_err(); + .unwrap(); assert_eq!(__test_only_reqwest_client_cache_len(), 2); Ok(())