Files
twenty/packages/twenty-front-component-renderer/src/remote/components/FrontComponentWorkerEffect.tsx
T
Paul Rastoin 37908114fc [SDK] Extract twenty-front-component-renderer outside of twenty-sdk ( 2.8MB ) (#19021)
Followup https://github.com/twentyhq/twenty/pull/19010

## Dependency diagram

```
┌─────────────────────┐
│     twenty-front    │
│   (React frontend)  │
└─────────┬───────────┘
          │ imports runtime:
          │   FrontComponentRenderer
          │   FrontComponentRendererWithSdkClient
          │   useFrontComponentExecutionContext
          ▼
┌──────────────────────────────────┐         ┌─────────────────────────┐
│ twenty-front-component-renderer  │────────▶│       twenty-sdk        │
│   (remote-dom host + worker)     │         │  (app developer SDK)    │
│                                  │         │                         │
│  imports from twenty-sdk:        │         │  Public API:            │
│   • types only:                  │         │   defineFrontComponent  │
│     FrontComponentExecutionContext│         │   navigate, closeSide…  │
│     NavigateFunction             │         │   useFrontComponent…    │
│     CloseSidePanelFunction       │         │   Command components    │
│     CommandConfirmation…         │         │   conditional avail.    │
│     OpenCommandConfirmation…     │         │                         │
│     EnqueueSnackbarFunction      │         │  Internal only:         │
│     etc.                         │         │   frontComponentHost…   │
│                                  │         │   front-component-build │
│  owns locally:                   │         │   esbuild plugins       │
│   • ALLOWED_HTML_ELEMENTS        │         │                         │
│   • EVENT_TO_REACT               │         └────────────┬────────────┘
│   • HTML_TAG_TO_CUSTOM_ELEMENT…  │                      │
│   • SerializedEventData          │                      │ types
│   • PropertySchema               │                      ▼
│   • frontComponentHostComm…      │         ┌─────────────────────────┐
│     (local ref to globalThis)    │         │     twenty-shared       │
│   • setFrontComponentExecution…  │         │  (common types/utils)   │
│     (local impl, same keys)      │         │   AppPath, SidePanelP…  │
│                                  │         │   EnqueueSnackbarParams │
└──────────────────────────────────┘         │   isDefined, …          │
          │                                  └─────────────────────────┘
          │ also depends on
          ▼
    twenty-shared (types)
    @remote-dom/* (runtime)
    @quilted/threads (runtime)
    react (runtime)
```

**Key points:**

- **`twenty-front`** depends on the renderer, **not** on `twenty-sdk`
directly (for rendering)
- **`twenty-front-component-renderer`** depends on `twenty-sdk` for
**types only** (function signatures, `FrontComponentExecutionContext`).
The runtime bridge (`frontComponentHostCommunicationApi`) is shared via
`globalThis` keys, not module imports
- **`twenty-sdk`** has no dependency on the renderer — clean one-way
dependency
- The renderer owns all remote-dom infrastructure (element schemas,
event mappings, custom element tags) that was previously leaking through
the SDK's public API
- The SDK's `./build` entry point was removed entirely (unused)
2026-03-30 17:06:06 +00:00

143 lines
4.1 KiB
TypeScript

import { ThreadWebWorker, release, retain } from '@quilted/threads';
import { RemoteReceiver } from '@remote-dom/core/receivers';
import { useEffect, useRef } from 'react';
import { type ConfirmationModalCaller } from 'twenty-shared/types';
import { type CommandConfirmationModalResult } from 'twenty-sdk';
import { type FrontComponentHostCommunicationApi } from '../../types/FrontComponentHostCommunicationApi';
import { type SdkClientUrls } from '../../types/HostToWorkerRenderContext';
import { type WorkerExports } from '../../types/WorkerExports';
import { createRemoteWorker } from '../worker/utils/createRemoteWorker';
// Must match COMMAND_MENU_ITEM_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME in twenty-front
const COMMAND_MENU_ITEM_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME =
'command-menu-item-confirmation-modal-result';
type CommandMenuItemConfirmationModalResultBrowserEventDetail = {
caller: ConfirmationModalCaller;
confirmationResult: CommandConfirmationModalResult;
};
type FrontComponentWorkerEffectProps = {
componentUrl: string;
applicationAccessToken?: string;
apiUrl?: string;
sdkClientUrls?: SdkClientUrls;
frontComponentId: string;
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
setReceiver: React.Dispatch<React.SetStateAction<RemoteReceiver | null>>;
setThread: React.Dispatch<
React.SetStateAction<ThreadWebWorker<
WorkerExports,
FrontComponentHostCommunicationApi
> | null>
>;
setError: React.Dispatch<React.SetStateAction<Error | null>>;
};
export const FrontComponentWorkerEffect = ({
componentUrl,
applicationAccessToken,
apiUrl,
sdkClientUrls,
frontComponentId,
frontComponentHostCommunicationApi,
setReceiver,
setThread,
setError,
}: FrontComponentWorkerEffectProps) => {
const isInitializedRef = useRef(false);
useEffect(() => {
if (isInitializedRef.current) {
return;
}
const newReceiver = new RemoteReceiver({ retain, release });
const worker = createRemoteWorker();
worker.onerror = (event: ErrorEvent) => {
const workerError =
event.error ?? new Error(event.message || 'Unknown worker error');
console.error('[FrontComponentRenderer] Worker error:', workerError);
setError(workerError);
};
const thread = new ThreadWebWorker<
WorkerExports,
FrontComponentHostCommunicationApi
>(worker, {
exports: frontComponentHostCommunicationApi,
});
const handleCommandMenuItemConfirmationModalResultBrowserEvent = (
event: CustomEvent<CommandMenuItemConfirmationModalResultBrowserEventDetail>,
) => {
const commandMenuItemConfirmationModalResultBrowserEventDetail =
event.detail;
const caller =
commandMenuItemConfirmationModalResultBrowserEventDetail.caller;
if (
caller.type !== 'frontComponent' ||
caller.frontComponentId !== frontComponentId
) {
return;
}
thread.imports
.onConfirmationModalResult(
commandMenuItemConfirmationModalResultBrowserEventDetail.confirmationResult,
)
.catch((error: Error) => {
setError(error);
});
};
window.addEventListener(
COMMAND_MENU_ITEM_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME,
handleCommandMenuItemConfirmationModalResultBrowserEvent as EventListener,
);
setThread(thread);
thread.imports
.render(newReceiver.connection, {
componentUrl,
applicationAccessToken,
apiUrl,
sdkClientUrls,
})
.catch((error: Error) => {
setError(error);
});
setReceiver(newReceiver);
isInitializedRef.current = true;
return () => {
window.removeEventListener(
COMMAND_MENU_ITEM_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME,
handleCommandMenuItemConfirmationModalResultBrowserEvent as EventListener,
);
setThread(null);
worker.terminate();
isInitializedRef.current = false;
};
}, [
componentUrl,
applicationAccessToken,
apiUrl,
sdkClientUrls,
frontComponentId,
setError,
setReceiver,
setThread,
frontComponentHostCommunicationApi,
]);
return null;
};