Files
twenty/packages/twenty-sdk/src/front-component-renderer/remote/worker/remote-worker.ts
T
Raphaël Bosi 7da8450075 [FRONT COMPONENTS] Headless components (#18096)
## Description

- Add `isHeadless` field to `FrontComponent` entity so front components
can run without rendering UI in the command menu
- Introduce headless front component mounting logic:
`HeadlessFrontComponentMountRoot` at the application root,
`useMountHeadlessFrontComponent`, and `useUnmountHeadlessFrontComponent`
hooks to mount/unmount headless components
- Expand the SDK with new action components (`Action`, `ActionLink`,
`ActionOpenSidePanelPage`) and host communication functions
(`openSidePanelPage`, `unmountFrontComponent`)
- Move `CommandMenuPages` type to twenty-shared so the SDK can reference
it for side panel navigation

## Video QA



https://github.com/user-attachments/assets/4f9e3bb1-fcd1-42be-b3f4-a97e80c2add2
2026-02-20 14:14:42 +00:00

110 lines
3.7 KiB
TypeScript

import '@remote-dom/core/polyfill';
import '@remote-dom/react/polyfill';
import '../generated/remote-elements';
import { ThreadWebWorker } from '@quilted/threads';
import {
BatchingRemoteConnection,
type RemoteConnection,
type RemoteRootElement,
} from '@remote-dom/core/elements';
import { isDefined } from 'twenty-shared/utils';
import { installStyleBridge } from '@/front-component-renderer/polyfills/installStyleBridge';
import { installStylePropertyOnRemoteElements } from '@/front-component-renderer/remote/utils/installStylePropertyOnRemoteElements';
import { patchRemoteElementSetAttribute } from '@/front-component-renderer/remote/utils/patchRemoteElementSetAttribute';
import { HTML_TAG_TO_CUSTOM_ELEMENT_TAG } from '@/sdk/front-component-api/constants/HtmlTagToRemoteComponent';
import { setFrontComponentExecutionContext } from '@/sdk/front-component-api/context/frontComponentContext';
import { type FrontComponentExecutionContext } from '../../types/FrontComponentExecutionContext';
import { type FrontComponentHostCommunicationApi } from '../../types/FrontComponentHostCommunicationApi';
import { type HostToWorkerRenderContext } from '../../types/HostToWorkerRenderContext';
import { type WorkerExports } from '../../types/WorkerExports';
import { exposeGlobals } from '../utils/exposeGlobals';
import { setWorkerEnv } from './utils/setWorkerEnv';
installStylePropertyOnRemoteElements();
patchRemoteElementSetAttribute();
exposeGlobals({
__HTML_TAG_TO_CUSTOM_ELEMENT_TAG__: HTML_TAG_TO_CUSTOM_ELEMENT_TAG,
});
const render: WorkerExports['render'] = async (
connection: RemoteConnection,
renderContext: HostToWorkerRenderContext,
) => {
const batchedConnection = new BatchingRemoteConnection(connection);
const root = document.createElement('remote-root') as RemoteRootElement;
const renderContainer = document.createElement('remote-fragment');
root.connect(batchedConnection);
root.append(renderContainer);
document.body.append(root);
installStyleBridge(root);
if (
isDefined(renderContext.applicationAccessToken) &&
isDefined(renderContext.apiUrl)
) {
setWorkerEnv({
TWENTY_APP_ACCESS_TOKEN: renderContext.applicationAccessToken,
TWENTY_API_URL: renderContext.apiUrl,
});
}
const response = await fetch(renderContext.componentUrl, {
headers: isDefined(renderContext.applicationAccessToken)
? { Authorization: `Bearer ${renderContext.applicationAccessToken}` }
: undefined,
});
if (!response.ok) {
throw new Error(
`Failed to fetch front component from ${renderContext.componentUrl}: ${response.status} ${response.statusText}`,
);
}
const responseText = await response.text();
const blob = new Blob([responseText], {
type: 'application/javascript',
});
const importUrl = URL.createObjectURL(blob);
try {
/* @vite-ignore */
const componentModule = await import(importUrl);
componentModule.default(renderContainer);
} finally {
URL.revokeObjectURL(importUrl);
}
};
const initializeHostCommunicationApi: WorkerExports['initializeHostCommunicationApi'] =
async () => {
const hostApi =
ThreadWebWorker.self.import<FrontComponentHostCommunicationApi>();
frontComponentHostCommunicationApi.navigate = hostApi.navigate;
frontComponentHostCommunicationApi.openSidePanelPage =
hostApi.openSidePanelPage;
frontComponentHostCommunicationApi.unmountFrontComponent =
hostApi.unmountFrontComponent;
};
const updateContext: WorkerExports['updateContext'] = async (
context: FrontComponentExecutionContext,
) => {
setFrontComponentExecutionContext(context);
};
ThreadWebWorker.self.export({
render,
initializeHostCommunicationApi,
updateContext,
});