Files
twenty/packages/twenty-front-component-renderer/src/polyfills/installStyleBridge.ts
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

253 lines
7.2 KiB
TypeScript

import { type RemoteRootElement } from '@remote-dom/core/elements';
import { type RemoteStyleProperties } from '@/remote/generated/remote-elements';
import { MockCSSStyleSheet } from './MockCSSStyleSheet';
export const installStyleBridge = (remoteRoot: RemoteRootElement): void => {
const styleElementMap = new WeakMap<
Element,
Element & RemoteStyleProperties
>();
const styleObserverMap = new WeakMap<Element, { disconnect: () => void }>();
const trackStyleElement = (styleElement: Element): void => {
if (styleElementMap.has(styleElement)) {
return;
}
const remoteStyleElement = document.createElement(
'remote-style',
) as Element & RemoteStyleProperties;
styleElementMap.set(styleElement, remoteStyleElement);
const attributeNames = styleElement.getAttributeNames?.() ?? [];
const dataAttributes = attributeNames
.filter((attributeName: string) => attributeName.startsWith('data-'))
.map(
(attributeName: string) =>
`${attributeName}=${styleElement.getAttribute(attributeName) ?? ''}`,
)
.join(';');
if (dataAttributes.length > 0) {
remoteStyleElement.styleKey = dataAttributes;
}
const syncCssFromStyleElement = () => {
remoteStyleElement.cssText = styleElement.textContent ?? '';
};
const mockSheet = new MockCSSStyleSheet((cssText: string) => {
remoteStyleElement.cssText = cssText;
});
try {
Object.defineProperty(styleElement, 'sheet', {
get: () => mockSheet,
configurable: true,
});
} catch {
void 0;
}
const prototypeChain = Object.getPrototypeOf(styleElement);
const textContentDescriptor =
Object.getOwnPropertyDescriptor(styleElement, 'textContent') ??
Object.getOwnPropertyDescriptor(prototypeChain, 'textContent') ??
Object.getOwnPropertyDescriptor(
Object.getPrototypeOf(prototypeChain),
'textContent',
);
if (textContentDescriptor?.set) {
const originalTextContentSet = textContentDescriptor.set;
try {
Object.defineProperty(styleElement, 'textContent', {
get: textContentDescriptor.get,
set(value: string) {
originalTextContentSet.call(this, value);
remoteStyleElement.cssText = value ?? '';
},
configurable: true,
});
} catch {
void 0;
}
}
const originalAppendChild = styleElement.appendChild.bind(styleElement);
try {
(
styleElement as Element & { appendChild: typeof originalAppendChild }
).appendChild = (child: Node) => {
const result = originalAppendChild(child);
syncCssFromStyleElement();
return result;
};
} catch {
void 0;
}
const originalInsertBeforeOnStyle =
styleElement.insertBefore.bind(styleElement);
try {
(
styleElement as Element & {
insertBefore: typeof originalInsertBeforeOnStyle;
}
).insertBefore = <T extends Node>(child: T, ref: Node | null): T => {
const result = originalInsertBeforeOnStyle(child, ref);
syncCssFromStyleElement();
return result;
};
} catch {
void 0;
}
const originalRemoveChildOnStyle =
styleElement.removeChild.bind(styleElement);
try {
(
styleElement as Element & {
removeChild: typeof originalRemoveChildOnStyle;
}
).removeChild = <T extends Node>(child: T): T => {
const result = originalRemoveChildOnStyle(child);
syncCssFromStyleElement();
return result;
};
} catch {
void 0;
}
if (typeof MutationObserver === 'function') {
try {
const styleObserver = new MutationObserver(() => {
syncCssFromStyleElement();
});
if (typeof styleObserver.observe === 'function') {
styleObserver.observe(styleElement, {
subtree: true,
childList: true,
characterData: true,
});
styleObserverMap.set(styleElement, styleObserver);
}
} catch {
void 0;
}
}
const existingContent = styleElement.textContent;
if (existingContent) {
remoteStyleElement.cssText = existingContent;
}
remoteRoot.appendChild(remoteStyleElement);
};
const untrackStyleElement = (styleElement: Element): void => {
const styleObserver = styleObserverMap.get(styleElement);
if (styleObserver) {
styleObserver.disconnect();
styleObserverMap.delete(styleElement);
}
const remoteStyleElement = styleElementMap.get(styleElement);
if (remoteStyleElement && remoteStyleElement.parentNode) {
remoteStyleElement.parentNode.removeChild(remoteStyleElement);
styleElementMap.delete(styleElement);
}
};
const headElement = document.head;
const originalAppendChild = headElement.appendChild.bind(headElement);
const originalInsertBefore = headElement.insertBefore.bind(headElement);
const originalAppend = headElement.append.bind(headElement);
const originalPrepend = headElement.prepend.bind(headElement);
const originalReplaceChild = headElement.replaceChild.bind(headElement);
const originalRemoveChild = headElement.removeChild.bind(headElement);
headElement.appendChild = <T extends Node>(child: T): T => {
const result = originalAppendChild(child);
if (isStyleElement(child)) {
trackStyleElement(child as unknown as Element);
}
return result;
};
headElement.insertBefore = <T extends Node>(
child: T,
ref: Node | null,
): T => {
const result = originalInsertBefore(child, ref);
if (isStyleElement(child)) {
trackStyleElement(child as unknown as Element);
}
return result;
};
headElement.append = (...nodes: (Node | string)[]) => {
originalAppend(...nodes);
for (const node of nodes) {
if (isStyleElement(node)) {
trackStyleElement(node as Element);
}
}
};
headElement.prepend = (...nodes: (Node | string)[]) => {
originalPrepend(...nodes);
for (const node of nodes) {
if (isStyleElement(node)) {
trackStyleElement(node as Element);
}
}
};
headElement.replaceChild = <T extends Node>(
newChild: Node,
oldChild: T,
): T => {
const result = originalReplaceChild(newChild, oldChild);
if (isStyleElement(oldChild)) {
untrackStyleElement(oldChild as unknown as Element);
}
if (isStyleElement(newChild)) {
trackStyleElement(newChild as unknown as Element);
}
return result;
};
headElement.removeChild = <T extends Node>(child: T): T => {
const result = originalRemoveChild(child);
if (isStyleElement(child)) {
untrackStyleElement(child as unknown as Element);
}
return result;
};
const existingStyleElements = Array.from(
headElement.querySelectorAll('style'),
);
for (const styleElement of existingStyleElements) {
trackStyleElement(styleElement);
}
Object.defineProperty(document, 'styleSheets', {
get: () => [],
configurable: true,
});
};
const isStyleElement = (node: Node | string): node is Element => {
return (
typeof node !== 'string' &&
'localName' in node &&
node.localName === 'style'
);
};