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)
85 lines
2.4 KiB
TypeScript
85 lines
2.4 KiB
TypeScript
import type { Project, SourceFile } from 'ts-morph';
|
|
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { CUSTOM_ELEMENT_NAMES } from './constants';
|
|
import { type ComponentSchema } from './schemas';
|
|
|
|
const getCustomRendererImports = (
|
|
components: ComponentSchema[],
|
|
): Map<string, string[]> => {
|
|
const importsByPath = new Map<string, string[]>();
|
|
|
|
for (const component of components) {
|
|
if (
|
|
isDefined(component.customHostRenderer) &&
|
|
isDefined(component.customHostRendererPath)
|
|
) {
|
|
const existing =
|
|
importsByPath.get(component.customHostRendererPath) ?? [];
|
|
|
|
if (!existing.includes(component.customHostRenderer)) {
|
|
existing.push(component.customHostRenderer);
|
|
}
|
|
|
|
importsByPath.set(component.customHostRendererPath, existing);
|
|
}
|
|
}
|
|
|
|
return importsByPath;
|
|
};
|
|
|
|
const generateRegistryEntries = (components: ComponentSchema[]): string => {
|
|
const entries = components
|
|
.map((component) => {
|
|
if (isDefined(component.customHostRenderer)) {
|
|
return ` ['${component.customElementName}', createRemoteComponentRenderer(${component.customHostRenderer})],`;
|
|
}
|
|
|
|
return ` ['${component.customElementName}', createRemoteComponentRenderer(createHtmlHostWrapper('${component.htmlTag}'))],`;
|
|
})
|
|
.join('\n');
|
|
|
|
return `type ComponentRegistryValue =
|
|
| ReturnType<typeof createRemoteComponentRenderer>
|
|
| typeof RemoteFragmentRenderer;
|
|
|
|
export const componentRegistry: Map<string, ComponentRegistryValue> = new Map([
|
|
${entries}
|
|
['${CUSTOM_ELEMENT_NAMES.FRAGMENT}', RemoteFragmentRenderer],
|
|
]);`;
|
|
};
|
|
|
|
export const generateHostRegistry = (
|
|
project: Project,
|
|
components: ComponentSchema[],
|
|
): SourceFile => {
|
|
const sourceFile = project.createSourceFile(
|
|
'host-component-registry.ts',
|
|
'',
|
|
{ overwrite: true },
|
|
);
|
|
|
|
sourceFile.addImportDeclaration({
|
|
moduleSpecifier: '@remote-dom/react/host',
|
|
namedImports: ['RemoteFragmentRenderer', 'createRemoteComponentRenderer'],
|
|
});
|
|
|
|
sourceFile.addImportDeclaration({
|
|
moduleSpecifier: '../utils/createHtmlHostWrapper',
|
|
namedImports: ['createHtmlHostWrapper'],
|
|
});
|
|
|
|
const customRendererImports = getCustomRendererImports(components);
|
|
|
|
for (const [modulePath, namedImports] of customRendererImports) {
|
|
sourceFile.addImportDeclaration({
|
|
moduleSpecifier: modulePath,
|
|
namedImports,
|
|
});
|
|
}
|
|
|
|
sourceFile.addStatements(generateRegistryEntries(components));
|
|
|
|
return sourceFile;
|
|
};
|