From 4a1cf3e577f3ccbb67232d80ac106d75974df8dd Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 08:22:12 -0700 Subject: [PATCH 1/3] Upgrade frontend to react-router 8.3.0 react-router-dom is gone in v8, so swap the dependency for react-router and update the four files that imported from it. This clears Dependabot alert GHSA-qwww-vcr4-c8h2; the advisory only affects the unstable RSC APIs, which this client-side SPA does not use. react-router v8 ships ESM only, so it and its cookie-es dependency join the transformIgnorePatterns allowlist. ts-jest applies astTransformers to TypeScript sources only, which left react-router's import.meta.hot guard intact and unparseable once emitted as CommonJS; JavaScript now goes through a thin wrapper transformer that neutralizes import.meta first. react-router@8.3.0 requires Node >=22.22.0, so bump the frontend workflow's NODE_VERSION from 20 to 22. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9009f005-342b-41a1-8513-74cbf282a9a4 --- .github/workflows/frontend_tests.yml | 2 +- frontend/jest-esm-js-transformer.cjs | 31 +++++++++++++++ frontend/jest.config.ts | 33 ++++++++-------- frontend/package-lock.json | 56 +++++++--------------------- frontend/package.json | 2 +- frontend/src/App.test.tsx | 2 +- frontend/src/App.tsx | 2 +- frontend/src/main.tsx | 2 +- frontend/src/setupTests.ts | 2 +- 9 files changed, 68 insertions(+), 64 deletions(-) create mode 100644 frontend/jest-esm-js-transformer.cjs diff --git a/.github/workflows/frontend_tests.yml b/.github/workflows/frontend_tests.yml index ceee609379..015a938dd7 100644 --- a/.github/workflows/frontend_tests.yml +++ b/.github/workflows/frontend_tests.yml @@ -19,7 +19,7 @@ concurrency: cancel-in-progress: true env: - NODE_VERSION: "20" + NODE_VERSION: "22" PYTHON_VERSION: "3.11" jobs: diff --git a/frontend/jest-esm-js-transformer.cjs b/frontend/jest-esm-js-transformer.cjs new file mode 100644 index 0000000000..76b223cfb6 --- /dev/null +++ b/frontend/jest-esm-js-transformer.cjs @@ -0,0 +1,31 @@ +/** + * Jest transformer for the ESM-only JavaScript dependencies that have to be + * down-compiled to CommonJS (see `transformIgnorePatterns` in jest.config.ts). + * + * ts-jest only runs `astTransformers` over TypeScript sources, so those + * packages keep their `import.meta` references and fail to parse once emitted + * as CommonJS — react-router's `import.meta.hot` guard is one such case. + * Neutralize `import.meta` in the source before delegating to ts-jest. + */ +const tsJest = require('ts-jest') + +const createTsJestTransformer = (tsJest.default ?? tsJest).createTransformer + +const neutralizeImportMeta = (source) => + source.replace(/\bimport\.meta\.env\b/g, 'process.env').replace(/\bimport\.meta\b/g, '({})') + +module.exports = { + createTransformer(config) { + const transformer = createTsJestTransformer(config) + + for (const method of ['process', 'processAsync', 'getCacheKey', 'getCacheKeyAsync']) { + const original = transformer[method] + if (typeof original !== 'function') continue + const bound = original.bind(transformer) + transformer[method] = (source, path, options) => + bound(neutralizeImportMeta(source), path, options) + } + + return transformer + }, +} diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts index d9d1351901..49d85bd959 100644 --- a/frontend/jest.config.ts +++ b/frontend/jest.config.ts @@ -1,5 +1,13 @@ import type { Config } from "jest"; +const tsJestOptions = { + tsconfig: "tsconfig.test.json", + useESM: false, + astTransformers: { + before: [{ path: "./jest-import-meta-transformer.ts" }], + }, +}; + const config: Config = { preset: "ts-jest", testEnvironment: "jsdom", @@ -27,26 +35,21 @@ const config: Config = { }, }, transform: { - // Also transform .js/.jsx so ts-jest can down-compile the ESM-only - // react-markdown dependency chain (whitelisted below) to CommonJS. - "^.+\\.[tj]sx?$": [ - "ts-jest", - { - tsconfig: "tsconfig.test.json", - useESM: false, - astTransformers: { - before: [{ path: "./jest-import-meta-transformer.ts" }], - }, - }, - ], + "^.+\\.tsx?$": ["ts-jest", tsJestOptions], + // Also transform JavaScript so ts-jest can down-compile the ESM-only + // react-markdown and react-router dependency chains (whitelisted below) + // to CommonJS. Those go through a wrapper because ts-jest applies + // `astTransformers` to TypeScript sources only. + "^.+\\.[cm]?jsx?$": ["/jest-esm-js-transformer.cjs", tsJestOptions], }, moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], testPathIgnorePatterns: ["/node_modules/", "/dist/", "/e2e/"], // By default Jest never transforms node_modules. react-markdown and its - // remark/micromark/unified/mdast/hast/unist dependencies are ESM-only, so - // they must be transformed. Everything else in node_modules stays ignored. + // remark/micromark/unified/mdast/hast/unist dependencies are ESM-only, as are + // react-router and its cookie-es dependency, so they must be transformed. + // Everything else in node_modules stays ignored. transformIgnorePatterns: [ - "/node_modules/(?!(@fluentui|axios|react-markdown|remark-.*|rehype-.*|micromark.*|mdast-.*|hast-.*|unist-.*|unified|bail|trough|vfile.*|is-plain-obj|trim-lines|property-information|comma-separated-tokens|space-separated-tokens|decode-named-character-reference|character-entities.*|html-url-attributes|devlop|zwitch|longest-streak|markdown-table|ccount|escape-string-regexp|estree-util-.*|hastscript|web-namespaces|stringify-entities|inline-style-parser|style-to-object)/)", + "/node_modules/(?!(@fluentui|axios|react-markdown|react-router|cookie-es|remark-.*|rehype-.*|micromark.*|mdast-.*|hast-.*|unist-.*|unified|bail|trough|vfile.*|is-plain-obj|trim-lines|property-information|comma-separated-tokens|space-separated-tokens|decode-named-character-reference|character-entities.*|html-url-attributes|devlop|zwitch|longest-streak|markdown-table|ccount|escape-string-regexp|estree-util-.*|hastscript|web-namespaces|stringify-entities|inline-style-parser|style-to-object)/)", ], globals: {}, }; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 087236d393..6dd4837a54 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,7 +18,7 @@ "react-error-boundary": "6.1.2", "react-joyride": "^3.2.0", "react-markdown": "^9.0.1", - "react-router-dom": "7.18.1", + "react-router": "8.3.0", "remark-gfm": "^4.0.0" }, "devDependencies": { @@ -5727,18 +5727,11 @@ "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } + "node_modules/cookie-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" }, "node_modules/create-require": { "version": "1.1.1", @@ -10350,20 +10343,19 @@ } }, "node_modules/react-router": { - "version": "7.18.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", - "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-8.3.0.tgz", + "integrity": "sha512-qyPMvW83jGIct3yiieisxdk9M745anqhpIMKN5m1t6yBMfgVPpt77aHOqs5fUlEJRMCGffg9BaQLH9oPVOL7xQ==", "license": "MIT", "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" + "cookie-es": "^3.1.1" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.22.0" }, "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" + "react": ">=19.2.7", + "react-dom": ">=19.2.7" }, "peerDependenciesMeta": { "react-dom": { @@ -10371,22 +10363,6 @@ } } }, - "node_modules/react-router-dom": { - "version": "7.18.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", - "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", - "license": "MIT", - "dependencies": { - "react-router": "7.18.1" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -10601,12 +10577,6 @@ "node": ">=10" } }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 257a970d37..b1ceb49e32 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,7 +32,7 @@ "react-error-boundary": "6.1.2", "react-joyride": "^3.2.0", "react-markdown": "^9.0.1", - "react-router-dom": "7.18.1", + "react-router": "8.3.0", "remark-gfm": "^4.0.0" }, "devDependencies": { diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 9f5df1d3be..a850f054b0 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -4,7 +4,7 @@ */ import { render, screen, fireEvent, waitFor } from "@testing-library/react"; -import { MemoryRouter } from "react-router-dom"; +import { MemoryRouter } from "react-router"; import App from "./App"; import { ThemeProvider } from "./hooks/useTheme"; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a099e5fd38..58df0dd3a6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import { useState, useCallback, useEffect, useRef, useMemo } from 'react' -import { Routes, Route, Navigate, useNavigate, useLocation, useSearchParams, matchPath } from 'react-router-dom' +import { Routes, Route, Navigate, useNavigate, useLocation, useSearchParams, matchPath } from 'react-router' import { useMsal } from '@azure/msal-react' import { Joyride } from 'react-joyride' import { useTheme } from './hooks/useTheme' diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index a940760bfd..8bf976690d 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,6 +1,6 @@ import React from 'react' import ReactDOM from 'react-dom/client' -import { BrowserRouter } from 'react-router-dom' +import { BrowserRouter } from 'react-router' import App from './App' import { AuthProvider } from './auth/AuthProvider' import { ThemeProvider } from './hooks/useTheme' diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts index 207373d071..8ad475b685 100644 --- a/frontend/src/setupTests.ts +++ b/frontend/src/setupTests.ts @@ -6,7 +6,7 @@ import { TextEncoder, TextDecoder } from "util"; // dialogs (tabster modalizer + Textarea) can take longer to mount under load. configure({ asyncUtilTimeout: 5000 }); -// jsdom omits TextEncoder/TextDecoder, which react-router v7 references at +// jsdom omits TextEncoder/TextDecoder, which react-router references at // import time. Node's util provides spec-compatible implementations. global.TextEncoder = TextEncoder; global.TextDecoder = TextDecoder as typeof global.TextDecoder; From f3d7cdd9b3fecc3f9d044fd14e88768dac9c3835 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 08:26:41 -0700 Subject: [PATCH 2/3] Document that ts-jest astTransformers never run over .js The split transform in jest.config.ts is load-bearing, not cosmetic: ts-jest applies astTransformers to TypeScript sources only, so jest-import-meta-transformer.ts silently misses every node_modules dependency. Spell that out where someone would be tempted to merge the two patterns back together, and note the scope limit on the AST transformer itself. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9009f005-342b-41a1-8513-74cbf282a9a4 --- frontend/jest-import-meta-transformer.ts | 5 +++++ frontend/jest.config.ts | 12 +++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/frontend/jest-import-meta-transformer.ts b/frontend/jest-import-meta-transformer.ts index 7db12df0c6..61e995616b 100644 --- a/frontend/jest-import-meta-transformer.ts +++ b/frontend/jest-import-meta-transformer.ts @@ -2,6 +2,11 @@ * ts-jest AST transformer that replaces `import.meta.env.X` with * `process.env.X` so that ts-jest (CommonJS mode) can parse files * using Vite's `import.meta.env` convention. + * + * Scope: TypeScript sources only. ts-jest never runs `astTransformers` over + * .js files, so node_modules dependencies are handled by + * jest-esm-js-transformer.cjs instead — extending this file does not reach + * them. */ import ts from 'typescript' diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts index 49d85bd959..ab216aba11 100644 --- a/frontend/jest.config.ts +++ b/frontend/jest.config.ts @@ -35,11 +35,17 @@ const config: Config = { }, }, transform: { + // Two entries on purpose — do NOT collapse them back into a single + // `^.+\.[tj]sx?$` pattern. ts-jest runs `astTransformers` over TypeScript + // sources only, so `jest-import-meta-transformer.ts` never sees the .js in + // node_modules and any `import.meta` there survives into the CommonJS + // output, where it is a syntax error. Every ESM-only dependency added to + // the allowlist below depends on the wrapper to neutralize it first + // (react-router's `import.meta.hot` guard is the current example). "^.+\\.tsx?$": ["ts-jest", tsJestOptions], // Also transform JavaScript so ts-jest can down-compile the ESM-only - // react-markdown and react-router dependency chains (whitelisted below) - // to CommonJS. Those go through a wrapper because ts-jest applies - // `astTransformers` to TypeScript sources only. + // react-markdown and react-router dependency chains to CommonJS. The + // `[cm]?` covers .mjs/.cjs dist files such as cookie-es/dist/index.mjs. "^.+\\.[cm]?jsx?$": ["/jest-esm-js-transformer.cjs", tsJestOptions], }, moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], From ee1141e0938c1124523efeadbf41e89b50916fad Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 09:14:57 -0700 Subject: [PATCH 3/3] DOC: Require Node 22+ for local frontend development react-router 8.3.0 declares engines node >=22.22.0, so the local dev setup guide telling contributors that Node 18 is enough is now wrong. Other Node references were already consistent: the devcontainer installs 24.x, prepare_package.py points at 24.x, and CI now uses 22. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9009f005-342b-41a1-8513-74cbf282a9a4 --- doc/getting_started/install_local_dev.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/getting_started/install_local_dev.md b/doc/getting_started/install_local_dev.md index 0a1929e364..14a3342497 100644 --- a/doc/getting_started/install_local_dev.md +++ b/doc/getting_started/install_local_dev.md @@ -40,7 +40,7 @@ Set up a PyRIT development environment on your local machine. git clone https://github.com/microsoft/PyRIT ``` -4. **Node.js and npm**. Required for building the TypeScript/React frontend. Download [Node.js](https://nodejs.org/) (which includes npm). Version 18 or higher is recommended. +4. **Node.js and npm**. Required for building the TypeScript/React frontend. Download [Node.js](https://nodejs.org/) (which includes npm). Version 22 or higher is required (the frontend's `react-router` dependency requires Node >= 22.22.0). ### Installation