diff --git a/.changeset/bundle-react-compiler-runtime.md b/.changeset/bundle-react-compiler-runtime.md new file mode 100644 index 00000000000..8eced70cf99 --- /dev/null +++ b/.changeset/bundle-react-compiler-runtime.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Bundle the React Compiler memo helper (`c`) into `@primer/react` via a local ESM shim instead of importing it from the external `react-compiler-runtime` package. This prevents a runtime crash (`TypeError: (0, l.c) is not a function`) when a consumer's bundle cannot resolve a callable `c`. diff --git a/package-lock.json b/package-lock.json index 8852950f973..b5004376e87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,8 +83,8 @@ "react-dom": "^18.3.1" }, "devDependencies": { - "@primer/react": "38.35.1", - "@primer/styled-react": "1.1.0", + "@primer/react": "38.36.0", + "@primer/styled-react": "1.1.1", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^6.0.2", @@ -97,8 +97,8 @@ "name": "example-nextjs", "version": "0.0.0", "dependencies": { - "@primer/react": "38.35.1", - "@primer/styled-react": "1.1.0", + "@primer/react": "38.36.0", + "@primer/styled-react": "1.1.1", "next": "^16.3.0", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -140,7 +140,7 @@ "version": "0.0.0", "dependencies": { "@primer/octicons-react": "^19.28.1", - "@primer/react": "38.35.1", + "@primer/react": "38.36.0", "clsx": "^2.1.1", "next": "^16.3.0", "react": "^19.2.0", @@ -22886,6 +22886,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/react-compiler-runtime/-/react-compiler-runtime-1.0.0.tgz", "integrity": "sha512-rRfjYv66HlG8896yPUDONgKzG5BxZD1nV9U6rkm+7VCuvQc903C4MjcoZR4zPw53IKSOX9wMQVpA1IAbRtzQ7w==", + "dev": true, "license": "MIT", "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental" @@ -29115,7 +29116,7 @@ }, "packages/react": { "name": "@primer/react", - "version": "38.35.1", + "version": "38.36.0", "license": "MIT", "dependencies": { "@github/mini-throttle": "^2.1.1", @@ -29135,7 +29136,6 @@ "hsluv": "1.0.1", "lodash.isempty": "^4.4.0", "lodash.isobject": "^3.0.2", - "react-compiler-runtime": "^1.0.0", "react-intersection-observer": "^10.0.3" }, "devDependencies": { @@ -29158,7 +29158,7 @@ "@storybook/addon-links": "10.5.7", "@storybook/addon-mcp": "^0.7.0", "@storybook/icons": "^2.1.0", - "@storybook/react-vite": "^10.5.7", + "@storybook/react-vite": "10.5.7", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.4.5", "@testing-library/react": "^16.3.0", @@ -29207,6 +29207,7 @@ "postcss-preset-primer": "^0.0.0", "publint": "^0.3.15", "react": "18.3.1", + "react-compiler-runtime": "^1.0.0", "react-dom": "18.3.1", "react-is": "18.3.1", "recast": "0.23.7", @@ -30090,7 +30091,7 @@ }, "packages/styled-react": { "name": "@primer/styled-react", - "version": "1.1.0", + "version": "1.1.1", "dependencies": { "@styled-system/css": "^5.1.5", "@styled-system/props": "^5.1.5", @@ -30108,9 +30109,9 @@ "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", "@primer/primitives": "10.x || 11.x", - "@primer/react": "^38.26.0", + "@primer/react": "^38.36.0", "@rolldown/plugin-babel": "^0.2.3", - "@storybook/react-vite": "^10.5.7", + "@storybook/react-vite": "10.5.7", "@types/babel__core": "^7.20.5", "@types/react": "18.3.11", "@types/react-dom": "18.3.1", diff --git a/packages/react/package.json b/packages/react/package.json index 1affcd7692b..ef5510d16c6 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -90,7 +90,6 @@ "hsluv": "1.0.1", "lodash.isempty": "^4.4.0", "lodash.isobject": "^3.0.2", - "react-compiler-runtime": "^1.0.0", "react-intersection-observer": "^10.0.3" }, "devDependencies": { @@ -162,6 +161,7 @@ "postcss-preset-primer": "^0.0.0", "publint": "^0.3.15", "react": "18.3.1", + "react-compiler-runtime": "^1.0.0", "react-dom": "18.3.1", "react-is": "18.3.1", "recast": "0.23.7", diff --git a/packages/react/rolldown.config.ts b/packages/react/rolldown.config.ts index e3c65d4e847..bd44b7df39c 100644 --- a/packages/react/rolldown.config.ts +++ b/packages/react/rolldown.config.ts @@ -40,13 +40,37 @@ function getEntrypointsFromInput(input: ReadonlySet) { ) } +// The React Compiler emits `import {c} from 'react-compiler-runtime'`. Alias it +// to a local ESM shim that is bundled into the output, so the memo helper is +// self-contained instead of an external CommonJS dependency that can fail to +// resolve a callable `c` in a consumer's bundle. +const reactCompilerRuntimeShim = path.resolve('src/utils/react-compiler-runtime.ts') + +function reactCompilerRuntimeAlias() { + return { + name: 'react-compiler-runtime-alias', + resolveId(source: string) { + if (source === 'react-compiler-runtime') { + return {id: reactCompilerRuntimeShim, external: false} + } + return null + }, + } +} + const dependencies = [ ...Object.keys(packageMetadata.peerDependencies ?? {}), ...Object.keys(packageMetadata.dependencies ?? {}), ...Object.keys(packageMetadata.devDependencies ?? {}), -].map(name => { - return new RegExp(`^${name}(/.*)?`) -}) +] + // `react-compiler-runtime` is aliased to a local shim and bundled, so it must + // not be external. + .filter(name => name !== 'react-compiler-runtime') + // Anchor to a package-name boundary so a name isn't treated as a prefix of + // another (e.g. `react` must not match `react-compiler-runtime`). + .map(name => { + return new RegExp(`^${name}($|/)`) + }) const external = [ // Exclude package dependencies @@ -66,6 +90,7 @@ export default defineConfig([ { input, plugins: [ + reactCompilerRuntimeAlias(), babel({ include: /\.(?:js|jsx|ts|tsx)$/, exclude: /node_modules/, diff --git a/packages/react/script/react-compiler.mjs b/packages/react/script/react-compiler.mjs index 74ee5a4e7e0..4aeef8d1cff 100644 --- a/packages/react/script/react-compiler.mjs +++ b/packages/react/script/react-compiler.mjs @@ -58,6 +58,7 @@ const unsupportedPatterns = [ 'src/internal/hooks/useDevOnlyEffect.ts', 'src/stories/deprecated/ActionList.stories.tsx', 'src/utils/StressTest.tsx', + 'src/utils/react-compiler-runtime.ts', 'src/utils/use-force-update.ts', ] diff --git a/packages/react/src/utils/__tests__/react-compiler-runtime.test.tsx b/packages/react/src/utils/__tests__/react-compiler-runtime.test.tsx new file mode 100644 index 00000000000..b40e0b8ed0a --- /dev/null +++ b/packages/react/src/utils/__tests__/react-compiler-runtime.test.tsx @@ -0,0 +1,33 @@ +import {describe, expect, it} from 'vitest' +import {renderHook} from '@testing-library/react' +import {c, useMemoCache} from '../react-compiler-runtime' + +const MEMO_CACHE_SENTINEL = Symbol.for('react.memo_cache_sentinel') + +describe('react-compiler-runtime shim', () => { + it('exports a callable `c` (the compiler memo helper)', () => { + expect(typeof c).toBe('function') + }) + + describe('useMemoCache fallback', () => { + it('allocates a cache of the requested size seeded with the sentinel', () => { + const {result} = renderHook(() => useMemoCache(6)) + const cache = result.current + + expect(cache).toHaveLength(6) + for (let index = 0; index < 6; index++) { + expect(cache[index]).toBe(MEMO_CACHE_SENTINEL) + } + expect((cache as unknown as Record)[MEMO_CACHE_SENTINEL]).toBe(true) + }) + + it('reuses the same cache across re-renders', () => { + const {result, rerender} = renderHook(() => useMemoCache(4)) + const first = result.current + + rerender() + + expect(result.current).toBe(first) + }) + }) +}) diff --git a/packages/react/src/utils/react-compiler-runtime.ts b/packages/react/src/utils/react-compiler-runtime.ts new file mode 100644 index 00000000000..9575eda761f --- /dev/null +++ b/packages/react/src/utils/react-compiler-runtime.ts @@ -0,0 +1,29 @@ +import React, {useMemo} from 'react' + +// Local replacement for the `c` helper from `react-compiler-runtime`. The build +// aliases the compiler's `import {c} from 'react-compiler-runtime'` to this file +// so only `c` is bundled (as clean ESM), instead of externalizing the whole +// CommonJS package. Mirrors the upstream behavior: prefer React's built-in +// compiler runtime (React 19+), otherwise fall back to a `useMemo`-backed cache. +const MEMO_CACHE_SENTINEL = Symbol.for('react.memo_cache_sentinel') + +type MemoCache = Array + +// Exported for testing: the `useMemo`-backed fallback used when React does not +// provide a built-in compiler runtime. +export function useMemoCache(size: number): MemoCache { + return useMemo(() => { + const cache = new Array(size) as MemoCache & Record + for (let index = 0; index < size; index++) { + cache[index] = MEMO_CACHE_SENTINEL + } + cache[MEMO_CACHE_SENTINEL] = true + return cache + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) +} + +const builtinRuntime = (React as typeof React & {__COMPILER_RUNTIME?: {c?: (size: number) => MemoCache}}) + .__COMPILER_RUNTIME + +export const c: (size: number) => MemoCache = typeof builtinRuntime?.c === 'function' ? builtinRuntime.c : useMemoCache