37908114fc
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)
143 lines
4.1 KiB
TypeScript
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;
|
|
};
|