[FRONT COMPONENTS] Navigate from the remote (#17762)

## PR Description

This PR:
- Introduces a `FrontComponentHostCommunicationApi`, which allows us to
pass functions to be executed from the worker
- Exposes a navigate function from the host to the front component
remote workers, enabling SDK components to trigger in-app navigation
- Gets rid of `useSyncExternalStore` and makes the execution context
reactive without it
- Refactors and improves the `esbuild` plugin system

## Video


https://github.com/user-attachments/assets/7b26a1c2-f85f-4898-a71d-f60c70e61711
This commit is contained in:
Raphaël Bosi
2026-02-09 14:38:35 +01:00
committed by GitHub
parent 2106f46e9e
commit 2b29918bf8
39 changed files with 689 additions and 293 deletions
@@ -1,107 +1,29 @@
/* eslint-disable */
import { isDefined } from 'twenty-shared/utils';
const mockFrontComponentCode = `
// react-globals:react
var useState = globalThis.React.useState;
var useEffect = globalThis.React.useEffect;
var useSyncExternalStore = globalThis.React.useSyncExternalStore;
var navigate = /* @__PURE__ */ (() => globalThis.TwentySdk.navigate)();
// react-globals:react/jsx-runtime
var jsx = globalThis.jsx;
var jsxs = globalThis.jsxs;
var Fragment = globalThis.React.Fragment;
var AppPath = /* @__PURE__ */ (() => globalThis.TwentyShared["types"].AppPath)();
var Button = /* @__PURE__ */ (() => globalThis.RemoteComponents.TwentyUiButton)();
var jsx = /* @__PURE__ */ (() => globalThis.jsx)();
// src/tata/test-component.front-component.tsx
var RemoteComponents = globalThis.RemoteComponents;
var getStore = () => {
const store = globalThis.frontComponentExecutionContextStore;
if (store === void 0) {
throw new Error(
"frontComponentExecutionContextStore not found on globalThis. This hook must be used within a front component running in the worker."
);
}
return store;
};
var useFrontComponentExecutionContext = () => {
const store = getStore();
return useSyncExternalStore(store.subscribe, store.getSnapshot);
};
var TestComponent = () => {
const [count, setCount] = useState(0);
const [timer, setTimer] = useState(0);
const context = useFrontComponentExecutionContext();
useEffect(() => {
const interval = setInterval(() => {
setTimer((prevTimer) => prevTimer + 1);
}, 1e3);
return () => clearInterval(interval);
}, []);
return /* @__PURE__ */ jsxs(
RemoteComponents.HtmlDiv,
return /* @__PURE__ */ jsx(
Button,
{
style: {
padding: "20px",
fontFamily: "Arial, sans-serif",
maxWidth: "400px",
margin: "0 auto",
display: "flex",
flexDirection: "column",
alignItems: "center"
},
children: [
/* @__PURE__ */ jsx(RemoteComponents.HtmlH1, { style: { color: "#333", marginBottom: "20px", fontSize: "24px" }, children: "Test Component" }),
/* @__PURE__ */ jsxs(RemoteComponents.HtmlP, { style: { fontSize: "18px", marginBottom: "10px", color: "#666" }, children: [
"Count: ",
count
] }),
/* @__PURE__ */ jsxs(RemoteComponents.HtmlP, { style: { fontSize: "18px", marginBottom: "20px", color: "#666" }, children: [
"Timer: ",
timer,
"s"
] }),
/* @__PURE__ */ jsxs(RemoteComponents.HtmlP, { style: { fontSize: "18px", marginBottom: "20px", color: "#666" }, children: [
"User ID: ",
context?.userId
] }),
/* @__PURE__ */ jsx(
RemoteComponents.HtmlButton,
{
onClick: () => setCount(count + 1),
style: {
padding: "10px 20px",
fontSize: "16px",
backgroundColor: "#007bff",
color: "white",
border: "none",
borderRadius: "5px",
cursor: "pointer",
transition: "all 0.3s ease",
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.2)"
},
onMouseEnter: (e) => {
e.currentTarget.style.backgroundColor = "#0056b3";
e.currentTarget.style.transform = "translateY(-2px)";
e.currentTarget.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.3)";
},
onMouseLeave: (e) => {
e.currentTarget.style.backgroundColor = "#007bff";
e.currentTarget.style.transform = "translateY(0)";
e.currentTarget.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.2)";
},
children: "Increment"
}
)
]
title: "Navigate to people index page",
onClick: () => navigate?.(AppPath.RecordIndexPage, {
objectNamePlural: "people"
})
}
);
};
var test_component_front_component_default = globalThis.jsx(TestComponent, {});
export {
test_component_front_component_default as default,
useFrontComponentExecutionContext
test_component_front_component_default as default
};
//# sourceMappingURL=test-component.front-component.mjs.map
`;
let cachedMockBlobUrl: string | null = null;