Skip to content

Commit cf050a0

Browse files
Copilothotlong
andcommitted
fix: replace dynamic require() with static imports for Turbopack SSR compatibility
- Replace require('@object-ui/react') with static import in LookupField.tsx - Add @object-ui/react as dependency of @object-ui/fields - Fix components Vite build to externalize react/jsx-runtime subpaths - Add ESM shims for use-sync-external-store CJS modules - All 23 turbo build tasks now pass including site prerender Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> Agent-Logs-Url: https://github.com/objectstack-ai/objectui/sessions/ee7c3441-a9e4-4974-ac49-c0f22129cbd8
1 parent 08e3a54 commit cf050a0

File tree

6 files changed

+127
-25
lines changed

6 files changed

+127
-25
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* ESM re-export of useSyncExternalStore from React.
3+
*
4+
* React 18+ ships useSyncExternalStore natively. The CJS shim package
5+
* (`use-sync-external-store/shim`) uses `require("react")` which
6+
* produces a Rolldown `require` polyfill incompatible with Next.js
7+
* Turbopack SSR. This module provides the same API surface using a
8+
* static ESM import instead.
9+
*/
10+
export { useSyncExternalStore } from 'react';
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* ESM re-export of useSyncExternalStoreWithSelector.
3+
*
4+
* The CJS shim package uses `require("react")` which produces a
5+
* Rolldown `require` polyfill incompatible with Next.js Turbopack SSR.
6+
* This module provides a pure-ESM implementation using React 18+
7+
* native useSyncExternalStore.
8+
*/
9+
import { useRef, useEffect, useMemo, useDebugValue, useSyncExternalStore } from 'react';
10+
11+
function is(x: unknown, y: unknown): boolean {
12+
return (x === y && (0 !== x || 1 / (x as number) === 1 / (y as number))) || (x !== x && y !== y);
13+
}
14+
15+
const objectIs: (x: unknown, y: unknown) => boolean =
16+
typeof Object.is === 'function' ? Object.is : is;
17+
18+
export function useSyncExternalStoreWithSelector<Snapshot, Selection>(
19+
subscribe: (onStoreChange: () => void) => () => void,
20+
getSnapshot: () => Snapshot,
21+
getServerSnapshot: undefined | null | (() => Snapshot),
22+
selector: (snapshot: Snapshot) => Selection,
23+
isEqual?: (a: Selection, b: Selection) => boolean,
24+
): Selection {
25+
const instRef = useRef<{
26+
hasValue: boolean;
27+
value: Selection;
28+
} | null>(null);
29+
let inst: { hasValue: boolean; value: Selection };
30+
if (instRef.current === null) {
31+
inst = { hasValue: false, value: null as Selection };
32+
instRef.current = inst;
33+
} else {
34+
inst = instRef.current;
35+
}
36+
37+
const [getSelection, getServerSelection] = useMemo(() => {
38+
let hasMemo = false;
39+
let memoizedSnapshot: Snapshot;
40+
let memoizedSelection: Selection;
41+
const memoizedSelector = (nextSnapshot: Snapshot): Selection => {
42+
if (!hasMemo) {
43+
hasMemo = true;
44+
memoizedSnapshot = nextSnapshot;
45+
const nextSelection = selector(nextSnapshot);
46+
if (isEqual !== undefined) {
47+
if (inst.hasValue) {
48+
const currentSelection = inst.value;
49+
if (isEqual(currentSelection, nextSelection)) {
50+
memoizedSelection = currentSelection;
51+
return currentSelection;
52+
}
53+
}
54+
}
55+
memoizedSelection = nextSelection;
56+
return nextSelection;
57+
}
58+
const prevSnapshot = memoizedSnapshot;
59+
const prevSelection = memoizedSelection;
60+
if (objectIs(prevSnapshot, nextSnapshot)) {
61+
return prevSelection;
62+
}
63+
const nextSelection = selector(nextSnapshot);
64+
if (isEqual !== undefined && isEqual(prevSelection, nextSelection)) {
65+
memoizedSnapshot = nextSnapshot;
66+
return prevSelection;
67+
}
68+
memoizedSnapshot = nextSnapshot;
69+
memoizedSelection = nextSelection;
70+
return nextSelection;
71+
};
72+
const maybeGetServerSelection =
73+
getServerSnapshot === undefined || getServerSnapshot === null
74+
? undefined
75+
: () => memoizedSelector(getServerSnapshot());
76+
return [() => memoizedSelector(getSnapshot()), maybeGetServerSelection];
77+
// eslint-disable-next-line react-hooks/exhaustive-deps
78+
}, [getSnapshot, getServerSnapshot, selector, isEqual]);
79+
80+
const value = useSyncExternalStore(subscribe, getSelection, getServerSelection);
81+
82+
useEffect(() => {
83+
inst.hasValue = true;
84+
inst.value = value;
85+
// eslint-disable-next-line react-hooks/exhaustive-deps
86+
}, [value]);
87+
88+
useDebugValue(value);
89+
return value;
90+
}

packages/components/vite.config.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,22 @@ export default defineConfig({
2020
}),
2121
],
2222
resolve: {
23-
alias: {
24-
'@': resolve(__dirname, './src'),
25-
'@object-ui/core': resolve(__dirname, '../core/src'),
26-
'@object-ui/types': resolve(__dirname, '../types/src'),
27-
'@object-ui/react': resolve(__dirname, '../react/src'),
28-
'@object-ui/components': resolve(__dirname, './src'), // Self-reference for vitest.setup.tsx
29-
'@object-ui/fields': resolve(__dirname, '../fields/src'),
30-
'@object-ui/plugin-dashboard': resolve(__dirname, '../plugin-dashboard/src'),
31-
'@object-ui/plugin-grid': resolve(__dirname, '../plugin-grid/src'),
32-
},
23+
alias: [
24+
{ find: '@', replacement: resolve(__dirname, './src') },
25+
{ find: '@object-ui/core', replacement: resolve(__dirname, '../core/src') },
26+
{ find: '@object-ui/types', replacement: resolve(__dirname, '../types/src') },
27+
{ find: '@object-ui/react', replacement: resolve(__dirname, '../react/src') },
28+
{ find: '@object-ui/components', replacement: resolve(__dirname, './src') }, // Self-reference for vitest.setup.tsx
29+
{ find: '@object-ui/fields', replacement: resolve(__dirname, '../fields/src') },
30+
{ find: '@object-ui/plugin-dashboard', replacement: resolve(__dirname, '../plugin-dashboard/src') },
31+
{ find: '@object-ui/plugin-grid', replacement: resolve(__dirname, '../plugin-grid/src') },
32+
// The CJS shims use require("react") which produces a Rolldown
33+
// require polyfill incompatible with Next.js Turbopack SSR.
34+
// Alias to ESM modules that re-export from React 18+ directly.
35+
{ find: /^use-sync-external-store\/shim\/with-selector(\.js)?$/, replacement: resolve(__dirname, 'src/lib/use-sync-external-store-with-selector-shim.ts') },
36+
{ find: /^use-sync-external-store\/shim(\.js)?$/, replacement: resolve(__dirname, 'src/lib/use-sync-external-store-shim.ts') },
37+
{ find: /^use-sync-external-store\/with-selector(\.js)?$/, replacement: resolve(__dirname, 'src/lib/use-sync-external-store-with-selector-shim.ts') },
38+
],
3339
},
3440
build: {
3541
lib: {
@@ -38,7 +44,9 @@ export default defineConfig({
3844
fileName: 'index',
3945
},
4046
rollupOptions: {
41-
external: ['react', 'react-dom', '@object-ui/core', '@object-ui/react', '@object-ui/types'],
47+
// Use a function to match subpath imports (e.g. react/jsx-runtime)
48+
// so Rolldown does not bundle CJS wrappers that use require().
49+
external: (id) => /^(react|react-dom|@object-ui\/(core|react|types))(\/|$)/.test(id),
4250
output: {
4351
globals: {
4452
react: 'React',

packages/fields/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"dependencies": {
3232
"@object-ui/components": "workspace:*",
3333
"@object-ui/core": "workspace:*",
34+
"@object-ui/react": "workspace:*",
3435
"@object-ui/types": "workspace:*",
3536
"clsx": "^2.1.1",
3637
"lucide-react": "^0.577.0",

packages/fields/src/widgets/LookupField.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { DataSource, QueryParams, LookupColumnDef } from '@object-ui/types'
1313
import { RecordPickerDialog } from './RecordPickerDialog';
1414
import type { RecordPickerFilterColumn } from './RecordPickerDialog';
1515
import { getCellRendererResolver } from './_cell-renderer-bridge';
16+
import { SchemaRendererContext as ImportedSchemaRendererContext } from '@object-ui/react';
1617

1718
export interface LookupOption {
1819
value: string | number;
@@ -25,21 +26,10 @@ export interface LookupOption {
2526
const LOOKUP_PAGE_SIZE = 50;
2627

2728
/**
28-
* Resolve SchemaRendererContext from @object-ui/react at runtime.
29-
* Uses the same dynamic-require fallback that plugin-view uses to avoid
30-
* a hard dependency on @object-ui/react (which would create a cycle).
29+
* SchemaRendererContext is created by @object-ui/react.
30+
* Using a static import to be compatible with Next.js Turbopack SSR.
3131
*/
32-
const FallbackContext = React.createContext<any>(null);
33-
let SchemaRendererContext: React.Context<any> = FallbackContext;
34-
try {
35-
// eslint-disable-next-line @typescript-eslint/no-require-imports
36-
const mod = require('@object-ui/react');
37-
if (mod.SchemaRendererContext) {
38-
SchemaRendererContext = mod.SchemaRendererContext;
39-
}
40-
} catch {
41-
// @object-ui/react not available — dataSource must be passed via props
42-
}
32+
const SchemaRendererContext: React.Context<any> = ImportedSchemaRendererContext;
4333

4434
/**
4535
* Map a raw record to a LookupOption using a display field and an id field.

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)