Skip to content
Merged
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` instead of importing it from an external `react-compiler-runtime` module. This prevents a runtime crash (`TypeError: (0, t.c) is not a function`) that could occur when a consumer's bundle resolved a skewed or stale `react-compiler-runtime` across independently-cached chunks.
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
34 changes: 31 additions & 3 deletions packages/react/rolldown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,40 @@ function getEntrypointsFromInput(input: ReadonlySet<string>) {
)
}

// The React Compiler emits imports of the memo helper `c` from
// `react-compiler-runtime`. That package is CommonJS and, left external, ships a
// bare cross-chunk import that crashes ("(0, t.c) is not a function") when a
// consumer's bundle resolves a skewed/duplicate copy across independently-cached
// chunks. Instead we resolve those imports to a small local ESM shim that is
// bundled into the output, so the helper is always self-contained and imports
// `react` as a normal ES module (no CommonJS `require` interop).
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 intentionally not external: it is aliased to a
// local shim (see `reactCompilerRuntimeAlias`) and bundled into the output.
.filter(name => name !== 'react-compiler-runtime')
.map(name => {
// Anchor the package-name boundary so a dependency name is not treated as a
// prefix of another (e.g. `react` must not match `react-compiler-runtime`).
return new RegExp(`^${name}($|/)`)
})
Comment on lines 64 to +76

const external = [
// Exclude package dependencies
Expand All @@ -66,6 +93,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 @@ -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',
]

Expand Down
Original file line number Diff line number Diff line change
@@ -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<symbol, unknown>)[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)
})
})
})
48 changes: 48 additions & 0 deletions packages/react/src/utils/react-compiler-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, {useMemo} from 'react'

/**
* Local, bundled replacement for the `c` helper exported by
* `react-compiler-runtime`.
*
* The React Compiler emits `import {c} from 'react-compiler-runtime'`. The build
* aliases that import to this module so the helper is bundled into
* `@primer/react` as plain ES modules instead of being resolved as an external
* CommonJS dependency. Leaving it external ships a bare cross-chunk import that
* can crash (`TypeError: (0, t.c) is not a function`) when a consumer's bundle
* graph resolves a skewed or duplicate copy across independently-cached chunks.
*
* The behavior mirrors `react-compiler-runtime`: prefer React's built-in
* compiler runtime when it is available (React 19+), otherwise fall back to a
* `useMemo`-backed cache. This module is excluded from the React Compiler (see
* `script/react-compiler.mjs`) so it does not attempt to compile the helper that
* backs the compiler's own runtime.
*/

const MEMO_CACHE_SENTINEL = Symbol.for('react.memo_cache_sentinel')

type MemoCache = Array<unknown>

type ReactCompilerRuntime = {
c?: (size: number) => MemoCache
}

// 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<symbol, unknown>
for (let index = 0; index < size; index++) {
cache[index] = MEMO_CACHE_SENTINEL
}
// Mark the cache as freshly allocated, matching `react-compiler-runtime`.
cache[MEMO_CACHE_SENTINEL] = true
return cache
// `size` is a stable per-call-site constant; the cache must be allocated
// exactly once, matching `react-compiler-runtime`.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
}

const builtinRuntime = (React as typeof React & {__COMPILER_RUNTIME?: ReactCompilerRuntime}).__COMPILER_RUNTIME

export const c: (size: number) => MemoCache = typeof builtinRuntime?.c === 'function' ? builtinRuntime.c : useMemoCache
Loading