Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bundle-react-compiler-runtime.md
Original file line number Diff line number Diff line change
@@ -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`.
31 changes: 28 additions & 3 deletions packages/react/rolldown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,37 @@ function getEntrypointsFromInput(input: ReadonlySet<string>) {
)
}

// 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
Expand All @@ -66,6 +90,7 @@ export default defineConfig([
{
input,
plugins: [
reactCompilerRuntimeAlias(),
babel({
include: /\.(?:js|jsx|ts|tsx)$/,
exclude: /node_modules/,
Expand Down
1 change: 1 addition & 0 deletions packages/react/script/react-compiler.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const unsupportedPatterns = [
'src/hooks/useResizeObserver.ts',
'src/hooks/useSafeTimeout.ts',
'src/TooltipV2/Tooltip.tsx',
'src/utils/react-compiler-runtime.ts',
]

const unsupported = new Set(
Expand Down
27 changes: 27 additions & 0 deletions packages/react/src/utils/react-compiler-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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<unknown>

function useMemoCache(size: number): MemoCache {
return useMemo(() => {
const cache = new Array(size) as MemoCache & Record<symbol, unknown>
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