Improve building of twenty-sdk (#17913)

## Split twenty-sdk build into separate Node and browser targets

The SDK was bundling Node.js code (CLI, SDK API) and browser code (UI
components, front-component renderer) through a single Vite config. This
caused incorrect externalization — Node builtins leaked into browser
bundles and browser-specific chunking logic applied to CLI output.

This PR splits the build into `vite.config.node.ts` and
`vite.config.browser.ts` so each target gets the right externals and
output format.

Also includes a few housekeeping renames:
- `front-component` export path → `front-component-renderer` (matches
what it actually is)
- `front-component-common` merged into `front-component-api` (was a
needless extra module)
This commit is contained in:
Charles Bochet
2026-02-13 15:58:19 +01:00
committed by GitHub
parent 463ce43442
commit f17cc4d190
64 changed files with 1575 additions and 217 deletions
@@ -4,7 +4,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useTheme } from '@emotion/react';
import { t } from '@lingui/core/macro';
import { useCallback } from 'react';
import { FrontComponentRenderer as SharedFrontComponentRenderer } from 'twenty-sdk/front-component';
import { FrontComponentRenderer as SharedFrontComponentRenderer } from 'twenty-sdk/front-component-renderer';
import { isDefined } from 'twenty-shared/utils';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useFindOneFrontComponentQuery } from '~/generated-metadata/graphql';
@@ -2,7 +2,7 @@ import { useRecoilValue } from 'recoil';
import {
type FrontComponentExecutionContext,
type FrontComponentHostCommunicationApi,
} from 'twenty-sdk/front-component';
} from 'twenty-sdk/front-component-renderer';
import { type AppPath } from 'twenty-shared/types';
import { currentUserState } from '@/auth/states/currentUserState';
+1 -1
View File
@@ -1,4 +1,4 @@
node_modules
.twenty
storybook-static
src/front-component/__stories__/built
src/front-component-renderer/__stories__/example-sources-built
+14 -1
View File
@@ -1,12 +1,15 @@
import type { StorybookConfig } from '@storybook/react-vite';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import tsconfigPaths from 'vite-tsconfig-paths';
const dirname =
typeof __dirname !== 'undefined'
? __dirname
: path.dirname(fileURLToPath(import.meta.url));
const sdkRoot = path.resolve(dirname, '..');
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
@@ -16,7 +19,7 @@ const config: StorybookConfig = {
staticDirs: [
{
from: '../src/front-component/__stories__/built',
from: '../src/front-component-renderer/__stories__/example-sources-built',
to: '/built',
},
],
@@ -31,11 +34,21 @@ const config: StorybookConfig = {
'@': path.resolve(dirname, '../src'),
},
},
plugins: [
...(viteConfig.plugins ?? []),
tsconfigPaths({ root: sdkRoot }),
],
optimizeDeps: {
...viteConfig.optimizeDeps,
include: [
...(viteConfig.optimizeDeps?.include ?? []),
'transliteration',
'@remote-dom/core/polyfill',
'@remote-dom/react/polyfill',
'@remote-dom/core/elements',
'@remote-dom/react',
'react-dom/client',
'react/jsx-runtime',
],
},
};
+11 -9
View File
@@ -3,7 +3,7 @@
"version": "0.5.2",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"types": "dist/sdk/index.d.ts",
"bin": {
"twenty": "dist/cli.cjs"
},
@@ -13,7 +13,7 @@
"package.json"
],
"scripts": {
"build": "npx rimraf dist && npx vite build",
"build": "npx rimraf dist && npx vite build -c vite.config.node.ts && npx vite build -c vite.config.browser.ts",
"prepublishOnly": "tsx ../twenty-utils/pack-scripts/pre-publish-only.ts",
"postpublish": "tsx ../twenty-utils/pack-scripts/post-publish.ts"
},
@@ -27,7 +27,7 @@
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"types": "./dist/sdk/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
@@ -36,14 +36,16 @@
"import": "./dist/ui/index.mjs",
"require": "./dist/ui/index.cjs"
},
"./front-component": {
"types": "./dist/front-component/index.d.ts",
"import": "./dist/front-component/index.mjs",
"require": "./dist/front-component/index.cjs"
"./front-component-renderer": {
"types": "./dist/front-component-renderer/index.d.ts",
"import": "./dist/front-component-renderer/index.mjs",
"require": "./dist/front-component-renderer/index.cjs"
}
},
"license": "AGPL-3.0",
"dependencies": {
"@chakra-ui/react": "^3.33.0",
"@emotion/react": "^11.14.0",
"@genql/cli": "^3.0.3",
"@quilted/threads": "^4.0.1",
"@remote-dom/core": "^1.10.1",
@@ -102,8 +104,8 @@
"ui": [
"dist/ui/index.d.ts"
],
"front-component": [
"dist/front-component/index.d.ts"
"front-component-renderer": [
"dist/front-component-renderer/index.d.ts"
]
}
}
+59 -29
View File
@@ -3,18 +3,28 @@
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/twenty-sdk/src",
"projectType": "library",
"tags": ["scope:sdk", "scope:shared"],
"tags": [
"scope:sdk",
"scope:shared"
],
"targets": {
"build": {
"executor": "nx:run-commands",
"cache": true,
"inputs": ["production", "^production"],
"dependsOn": ["^build", "generate-remote-dom-elements"],
"outputs": ["{projectRoot}/dist"],
"inputs": [
"production",
"^production"
],
"dependsOn": [
"^build"
],
"outputs": [
"{projectRoot}/dist"
],
"options": {
"cwd": "{projectRoot}",
"commands": [
"npx rimraf dist && npx vite build",
"npx rimraf dist && npx vite build -c vite.config.node.ts && npx vite build -c vite.config.browser.ts",
"tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src && npx tsc-alias -p tsconfig.lib.json --outDir dist"
],
"parallel": false
@@ -22,15 +32,19 @@
},
"dev": {
"executor": "nx:run-commands",
"dependsOn": ["^build"],
"dependsOn": [
"^build"
],
"options": {
"cwd": "packages/twenty-sdk",
"command": "npx rimraf dist && npx vite build --watch"
"command": "npx rimraf dist && npx vite build -c vite.config.node.ts && npx vite build -c vite.config.browser.ts && tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src && npx tsc-alias -p tsconfig.lib.json --outDir dist && npx vite build -c vite.config.node.ts --watch & npx vite build -c vite.config.browser.ts --watch"
}
},
"start": {
"executor": "nx:run-commands",
"dependsOn": ["build"],
"dependsOn": [
"build"
],
"options": {
"cwd": "packages/twenty-sdk",
"command": "node dist/cli.cjs"
@@ -39,12 +53,16 @@
"typecheck": {},
"lint": {
"options": {
"lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"],
"lintFilePatterns": [
"{projectRoot}/src/**/*.{ts,json}"
],
"maxWarnings": 0
},
"configurations": {
"ci": {
"lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"],
"lintFilePatterns": [
"{projectRoot}/src/**/*.{ts,json}"
],
"maxWarnings": 0
},
"fix": {}
@@ -52,7 +70,9 @@
},
"test": {
"executor": "@nx/vitest:test",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"outputs": [
"{workspaceRoot}/coverage/{projectRoot}"
],
"options": {
"config": "{projectRoot}/vitest.config.ts"
},
@@ -99,16 +119,18 @@
"generate-remote-dom-elements": {
"executor": "nx:run-commands",
"cache": true,
"dependsOn": ["^build"],
"dependsOn": [
"^build"
],
"inputs": [
"{projectRoot}/scripts/remote-dom/**/*",
"{projectRoot}/src/sdk/front-component-common/**/*",
"{projectRoot}/src/sdk/front-component-api/**/*",
"{workspaceRoot}/packages/twenty-ui/src/**/index.ts",
"{workspaceRoot}/packages/twenty-ui/src/**/*.tsx"
],
"outputs": [
"{projectRoot}/src/front-component/host/generated/*",
"{projectRoot}/src/front-component/remote/generated/*"
"{projectRoot}/src/front-component-renderer/host/generated/*",
"{projectRoot}/src/front-component-renderer/remote/generated/*"
],
"options": {
"cwd": "packages/twenty-sdk",
@@ -123,29 +145,33 @@
"storybook:prebuild": {
"executor": "nx:run-commands",
"cache": true,
"dependsOn": ["generate-remote-dom-elements"],
"inputs": [
"{projectRoot}/src/front-component/__stories__/mocks/**/*",
"{projectRoot}/src/front-component/__stories__/utils/**/*",
"{projectRoot}/src/cli/utilities/build/common/front-component-build/**/*",
"{projectRoot}/src/front-component-constants/**/*",
"{projectRoot}/src/sdk/**/*"
"dependsOn": [
"generate-remote-dom-elements"
],
"inputs": [
"{projectRoot}/scripts/front-component-stories/**/*",
"{projectRoot}/src/front-component-renderer/__stories__/example-sources/*",
"{projectRoot}/src/cli/utilities/build/**/*"
],
"outputs": [
"{projectRoot}/src/front-component-renderer/__stories__/example-sources-built/*"
],
"outputs": ["{projectRoot}/src/front-component/__stories__/built/*"],
"options": {
"command": "tsx {projectRoot}/src/front-component/__stories__/utils/buildMockComponents.ts"
"command": "tsx {projectRoot}/scripts/front-component-stories/build-source-examples.ts"
}
},
"storybook:build": {
"dependsOn": ["storybook:prebuild"],
"dependsOn": [
"storybook:prebuild"
],
"configurations": {
"test": {}
}
},
"storybook:serve:dev": {
"dependsOn": ["storybook:prebuild"],
"executor": "nx:run-commands",
"options": {
"port": 6008
"command": "echo 'storybook:serve:dev is disabled for twenty-sdk, use storybook:serve:static instead'"
}
},
"storybook:serve:static": {
@@ -158,13 +184,17 @@
}
},
"storybook:test": {
"dependsOn": ["storybook:prebuild"],
"dependsOn": [
"storybook:prebuild"
],
"options": {
"command": "vitest run --coverage --config vitest.storybook.config.ts --shard={args.shard}"
}
},
"storybook:test:no-coverage": {
"dependsOn": ["storybook:prebuild"],
"dependsOn": [
"storybook:prebuild"
],
"options": {
"command": "vitest run --config vitest.storybook.config.ts --shard={args.shard}"
}
@@ -0,0 +1,53 @@
import * as esbuild from 'esbuild';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createFrontComponentBuildOptions } from './utils/create-front-component-build-options';
const dirname = path.dirname(fileURLToPath(import.meta.url));
const exampleSourcesDir = path.resolve(dirname, '../../src/front-component-renderer/__stories__/example-sources');
const exampleSourcesBuiltDir = path.resolve(dirname, '../../src/front-component-renderer/__stories__/example-sources-built');
const sdkRoot = path.resolve(dirname, '../../../..');
const STORY_COMPONENTS = [
'static.front-component',
'interactive.front-component',
'lifecycle.front-component',
'chakra-example.front-component',
'tailwind-example.front-component',
];
export const buildSourceExamples = async (): Promise<void> => {
fs.mkdirSync(exampleSourcesBuiltDir, { recursive: true });
const entryPoints: Record<string, string> = {};
for (const name of STORY_COMPONENTS) {
const filePath = path.join(exampleSourcesDir, `${name}.tsx`);
if (!fs.existsSync(filePath)) {
throw new Error(
`Story component source file not found: ${filePath}\n` +
`Ensure the file exists in ${exampleSourcesDir} and the name in STORY_COMPONENTS is correct.`,
);
}
entryPoints[name] = filePath;
}
const buildOptions = createFrontComponentBuildOptions({
entryPoints,
outdir: exampleSourcesBuiltDir,
tsconfigPath: path.join(dirname, '../../tsconfig.json'),
});
await esbuild.build(buildOptions);
console.log(
`Built ${STORY_COMPONENTS.length} story components to ${exampleSourcesBuiltDir}`,
);
};
buildSourceExamples().catch((error) => {
console.error('Failed to build mock components:', error);
process.exit(1);
});
@@ -1,7 +1,7 @@
import type * as esbuild from 'esbuild';
import { FRONT_COMPONENT_EXTERNAL_MODULES } from '../constants/front-component-external-modules';
import { getFrontComponentBuildPlugins } from './get-front-component-build-plugins';
import { FRONT_COMPONENT_EXTERNAL_MODULES } from '../../../src/cli/utilities/build/common/front-component-build/constants/front-component-external-modules';
import { getFrontComponentBuildPlugins } from '../../../src/cli/utilities/build/common/front-component-build/utils/get-front-component-build-plugins';
export type FrontComponentBuildOptions = {
entryPoints: esbuild.BuildOptions['entryPoints'];
@@ -4,10 +4,10 @@ import * as path from 'path';
import { fileURLToPath } from 'url';
import { IndentationText, Project, QuoteKind } from 'ts-morph';
import { ALLOWED_HTML_ELEMENTS } from '../../src/sdk/front-component-common/AllowedHtmlElements';
import { COMMON_HTML_EVENTS } from '../../src/sdk/front-component-common/CommonHtmlEvents';
import { EVENT_TO_REACT } from '../../src/sdk/front-component-common/EventToReact';
import { HTML_COMMON_PROPERTIES } from '../../src/sdk/front-component-common/HtmlCommonProperties';
import { ALLOWED_HTML_ELEMENTS } from '../../src/sdk/front-component-api/constants/AllowedHtmlElements';
import { COMMON_HTML_EVENTS } from '../../src/sdk/front-component-api/constants/CommonHtmlEvents';
import { EVENT_TO_REACT } from '../../src/sdk/front-component-api/constants/EventToReact';
import { HTML_COMMON_PROPERTIES } from '../../src/sdk/front-component-api/constants/HtmlCommonProperties';
import {
type ComponentSchema,
@@ -39,7 +39,7 @@ const parseVerboseFlag = (): boolean => {
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const PACKAGE_PATH = path.resolve(SCRIPT_DIR, '../..');
const FRONT_COMPONENT_PATH = path.join(PACKAGE_PATH, 'src/front-component');
const FRONT_COMPONENT_PATH = path.join(PACKAGE_PATH, 'src/front-component-renderer');
const HOST_GENERATED_DIR = path.join(FRONT_COMPONENT_PATH, 'host/generated');
const REMOTE_GENERATED_DIR = path.join(
FRONT_COMPONENT_PATH,
@@ -1,6 +1,6 @@
import type { Project, SourceFile } from 'ts-morph';
import { EVENT_TO_REACT } from '@/sdk/front-component-common/EventToReact';
import { EVENT_TO_REACT } from '@/sdk/front-component-api/constants/EventToReact';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
import { CUSTOM_ELEMENT_NAMES } from './constants';
import { type ComponentSchema } from './schemas';
@@ -275,7 +275,7 @@ export const generateHostRegistry = (
});
sourceFile.addImportDeclaration({
moduleSpecifier: '../../../sdk/front-component-common/SerializedEventData',
moduleSpecifier: '../../../sdk/front-component-api/constants/SerializedEventData',
namedImports: [{ name: 'SerializedEventData', isTypeOnly: true }],
});
@@ -1,6 +1,6 @@
import type { Project, SourceFile } from 'ts-morph';
import { EVENT_TO_REACT } from '@/sdk/front-component-common/EventToReact';
import { EVENT_TO_REACT } from '@/sdk/front-component-api/constants/EventToReact';
import { type ComponentSchema } from './schemas';
import { addExportedConst, addFileHeader } from './utils';
@@ -356,7 +356,7 @@ export const generateRemoteElements = (
});
sourceFile.addImportDeclaration({
moduleSpecifier: '../../../sdk/front-component-common/SerializedEventData',
moduleSpecifier: '../../../sdk/front-component-api/constants/SerializedEventData',
namedImports: [{ name: 'SerializedEventData', isTypeOnly: true }],
});
@@ -1,4 +1,4 @@
import { EVENT_TO_REACT } from '@/sdk/front-component-common/EventToReact';
import { EVENT_TO_REACT } from '@/sdk/front-component-api/constants/EventToReact';
export const REACT_PROP_TO_DOM_EVENT: Record<string, string> =
Object.fromEntries(
@@ -2,7 +2,7 @@ import * as path from 'path';
import { type ExportSpecifier, Project } from 'ts-morph';
import { isDefined, pascalToKebab } from 'twenty-shared/utils';
import { type PropertySchema } from '@/front-component/types/PropertySchema';
import { type PropertySchema } from '@/front-component-renderer/types/PropertySchema';
import {
logCategory,
@@ -1,6 +1,6 @@
import { type Type } from 'ts-morph';
import { type PropertySchema } from '@/front-component/types/PropertySchema';
import { type PropertySchema } from '@/front-component-renderer/types/PropertySchema';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
import { REACT_PROP_TO_DOM_EVENT } from '../constants/ReactPropToDomEvent';
import { mapTypeToPropertySchema } from './map-type-to-property-schema';
@@ -1,6 +1,6 @@
import { type Type } from 'ts-morph';
import { type PropertySchema } from '@/front-component/types/PropertySchema';
import { type PropertySchema } from '@/front-component-renderer/types/PropertySchema';
import { isNonEmptyArray } from 'twenty-shared/utils';
type PropertyType = PropertySchema['type'];
@@ -2,7 +2,7 @@ import { isDefined } from 'twenty-shared/utils';
import { type ParsedImportSpecifier } from '../types/ParsedImportSpecifier';
const ALIASED_IMPORT_PATTERN = /^(\w+)\s+as\s+(\w+)$/;
const ALIASED_IMPORT_PATTERN = /^([\w$]+)\s+as\s+([\w$]+)$/;
export const extractNamesFromImportSpecifier = (
importSpecifier: string,
@@ -1,6 +1,6 @@
import { isDefined } from 'twenty-shared/utils';
import { HTML_TAG_TO_REMOTE_COMPONENT } from '../../../../../../sdk/front-component-common';
import { HTML_TAG_TO_REMOTE_COMPONENT } from '../../../../../../sdk/front-component-api';
const REMOTE_COMPONENTS_GLOBAL_NAMESPACE = 'RemoteComponents';
@@ -3,7 +3,7 @@ import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
import { FrontComponentRenderer } from '../host/components/FrontComponentRenderer';
import { getBuiltComponentPath } from './utils/loadBuiltComponent';
import { getBuiltStoryComponentPathForRender } from './utils/getBuiltStoryComponentPathForRender';
const errorHandler = fn();
@@ -27,7 +27,7 @@ type Story = StoryObj<typeof FrontComponentRenderer>;
export const Static: Story = {
args: {
componentUrl: getBuiltComponentPath('static.front-component'),
componentUrl: getBuiltStoryComponentPathForRender('static.front-component'),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
@@ -35,7 +35,7 @@ export const Static: Story = {
const container = await canvas.findByTestId(
'static-component',
{},
{ timeout: 5000 },
{ timeout: 30000 },
);
expect(container).toBeVisible();
expect(container).toHaveStyle({
@@ -55,12 +55,12 @@ export const Static: Story = {
export const Interactive: Story = {
args: {
componentUrl: getBuiltComponentPath('interactive.front-component'),
componentUrl: getBuiltStoryComponentPathForRender('interactive.front-component'),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId('interactive-component', {}, { timeout: 5000 });
await canvas.findByTestId('interactive-component', {}, { timeout: 10000 });
expect(await canvas.findByText('Count: 0')).toBeVisible();
@@ -75,12 +75,12 @@ export const Interactive: Story = {
export const Lifecycle: Story = {
args: {
componentUrl: getBuiltComponentPath('lifecycle.front-component'),
componentUrl: getBuiltStoryComponentPathForRender('lifecycle.front-component'),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId('lifecycle-component', {}, { timeout: 5000 });
await canvas.findByTestId('lifecycle-component', {}, { timeout: 10000 });
expect(await canvas.findByText('Mounted')).toBeVisible();
@@ -89,21 +89,37 @@ export const Lifecycle: Story = {
const tickElement = canvas.getByTestId('tick-count');
expect(tickElement.textContent).toMatch(/Ticks: [1-9]\d*/);
},
{ timeout: 5000 },
{ timeout: 10000 },
);
},
};
export const ChakraExample: Story = {
args: {
componentUrl: getBuiltStoryComponentPathForRender(
'chakra-example.front-component',
),
},
};
export const TailwindExample: Story = {
args: {
componentUrl: getBuiltStoryComponentPathForRender(
'tailwind-example.front-component',
),
},
};
export const ErrorHandling: Story = {
args: {
componentUrl: '/built/nonexistent.front-component.mjs',
componentUrl: getBuiltStoryComponentPathForRender('nonexistent.front-component'),
},
play: async () => {
await waitFor(
() => {
expect(errorHandler).toHaveBeenCalled();
},
{ timeout: 5000 },
{ timeout: 10000 },
);
},
};
@@ -0,0 +1,21 @@
import { Button, ChakraProvider, defaultSystem } from '@chakra-ui/react';
import { defineFrontComponent } from 'twenty-sdk';
export const ChakraComponent = () => {
return (
<ChakraProvider value={defaultSystem}>
<div style={{ padding: '20px' }}>
<Button colorPalette="blue" size="md">
Click me
</Button>
</div>
</ChakraProvider>
);
};
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'chakra-component',
description: 'A front component with a Chakra UI button',
component: ChakraComponent,
});
@@ -0,0 +1,76 @@
import { defineFrontComponent } from '@/sdk';
import { useState } from 'react';
// Tailwind CSS subset (only the utilities used by this component)
// In a real setup, this would be generated by the Tailwind CLI at build time.
const TAILWIND_CSS = `
.p-5{padding:1.25rem}
.mb-4{margin-bottom:1rem}
.mb-2{margin-bottom:.5rem}
.space-y-3>:not(:first-child){margin-top:.75rem}
.rounded-lg{border-radius:.5rem}
.rounded-md{border-radius:.375rem}
.border{border-width:1px}
.border-gray-200{border-color:#e5e7eb}
.bg-white{background-color:#fff}
.bg-blue-600{background-color:#2563eb}
.bg-blue-700{background-color:#1d4ed8}
.bg-gray-50{background-color:#f9fafb}
.px-4{padding-left:1rem;padding-right:1rem}
.py-2{padding-top:.5rem;padding-bottom:.5rem}
.text-sm{font-size:.875rem;line-height:1.25rem}
.text-lg{font-size:1.125rem;line-height:1.75rem}
.text-2xl{font-size:1.5rem;line-height:2rem}
.font-semibold{font-weight:600}
.font-bold{font-weight:700}
.text-gray-500{color:#6b7280}
.text-gray-900{color:#111827}
.text-blue-600{color:#2563eb}
.text-white{color:#fff}
.shadow-sm{box-shadow:0 1px 2px 0 rgb(0 0 0/.05)}
.cursor-pointer{cursor:pointer}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
`;
const TailwindComponent = () => {
const [count, setCount] = useState(0);
return (
<>
<style>{TAILWIND_CSS}</style>
<div
data-testid="tailwind-component"
className="p-5 bg-white rounded-lg border border-gray-200 shadow-sm space-y-3"
>
<h2 className="text-lg font-bold text-gray-900">
Tailwind CSS Component
</h2>
<p className="text-sm text-gray-500">
This component uses Tailwind utility classes via className.
</p>
<div className="bg-gray-50 rounded-md p-5">
<span
data-testid="tailwind-count"
className="text-2xl font-semibold text-blue-600"
>
Count: {count}
</span>
</div>
<button
data-testid="tailwind-button"
className="px-4 py-2 bg-blue-600 text-white font-semibold rounded-md text-sm cursor-pointer"
onClick={() => setCount((previous) => previous + 1)}
>
Increment
</button>
</div>
</>
);
};
export default defineFrontComponent({
universalIdentifier: 'test-tailwind-0000-0000-0000-000000000005',
name: 'tailwind-component',
description: 'A front component using Tailwind CSS utility classes',
component: TailwindComponent,
});
@@ -0,0 +1,9 @@
// Returns an absolute URL because the worker runs inside a Blob URL
// where relative paths cannot be resolved.
export const getBuiltStoryComponentPathForRender = (
componentName: string,
): string => {
const origin = typeof window !== 'undefined' ? window.location.origin : '';
return `${origin}/built/${componentName}.mjs`;
};
@@ -1,9 +1,9 @@
import { FrontComponentErrorEffect } from '@/front-component/remote/components/FrontComponentErrorEffect';
import { FrontComponentHostCommunicationApiEffect } from '@/front-component/remote/components/FrontComponentHostCommunicationApiEffect';
import { FrontComponentUpdateContextEffect } from '@/front-component/remote/components/FrontComponentUpdateContextEffect';
import { type FrontComponentExecutionContext } from '@/front-component/types/FrontComponentExecutionContext';
import { type FrontComponentHostCommunicationApi } from '@/front-component/types/FrontComponentHostCommunicationApi';
import { type WorkerExports } from '@/front-component/types/WorkerExports';
import { FrontComponentErrorEffect } from '@/front-component-renderer/remote/components/FrontComponentErrorEffect';
import { FrontComponentHostCommunicationApiEffect } from '@/front-component-renderer/remote/components/FrontComponentHostCommunicationApiEffect';
import { FrontComponentUpdateContextEffect } from '@/front-component-renderer/remote/components/FrontComponentUpdateContextEffect';
import { type FrontComponentExecutionContext } from '@/front-component-renderer/types/FrontComponentExecutionContext';
import { type FrontComponentHostCommunicationApi } from '@/front-component-renderer/types/FrontComponentHostCommunicationApi';
import { type WorkerExports } from '@/front-component-renderer/types/WorkerExports';
import { type ThreadWebWorker } from '@quilted/threads';
import {
type RemoteReceiver,
@@ -70,7 +70,26 @@ export const FrontComponentRenderer = ({
{MemoizedFrontComponentWorkerEffect}
{isDefined(error) && (
<FrontComponentErrorEffect error={error} onError={onError} />
<>
<FrontComponentErrorEffect error={error} onError={onError} />
<div
style={{
padding: '12px 16px',
backgroundColor: '#fef2f2',
border: '1px solid #fecaca',
borderRadius: '6px',
color: '#991b1b',
fontFamily: 'monospace',
fontSize: '13px',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
maxHeight: '200px',
overflow: 'auto',
}}
>
<strong>FrontComponent error:</strong> {error.message}
</div>
</>
)}
{isDefined(thread) && (
@@ -12,7 +12,7 @@ import {
RemoteFragmentRenderer,
createRemoteComponentRenderer,
} from '@remote-dom/react/host';
import { type SerializedEventData } from '../../../sdk/front-component-common/SerializedEventData';
import { type SerializedEventData } from '../../../sdk/front-component-api/constants/SerializedEventData';
import {
AnimatedButton,
AnimatedLightIconButton,
@@ -114,7 +114,7 @@ export type {
HtmlThProperties,
TwentyUiButtonProperties,
} from './remote/generated/remote-elements';
export { createRemoteWorker } from './remote/worker/createRemoteWorker';
export { createRemoteWorker } from './remote/worker/utils/createRemoteWorker';
export type { FrontComponentExecutionContext } from './types/FrontComponentExecutionContext';
export type { FrontComponentHostCommunicationApi } from './types/FrontComponentHostCommunicationApi';
export type { HostToWorkerRenderContext } from './types/HostToWorkerRenderContext';
@@ -1,5 +1,5 @@
import { type FrontComponentHostCommunicationApi } from '@/front-component/types/FrontComponentHostCommunicationApi';
import { type WorkerExports } from '@/front-component/types/WorkerExports';
import { type FrontComponentHostCommunicationApi } from '@/front-component-renderer/types/FrontComponentHostCommunicationApi';
import { type WorkerExports } from '@/front-component-renderer/types/WorkerExports';
import { type ThreadWebWorker } from '@quilted/threads';
import { useEffect } from 'react';
@@ -1,6 +1,6 @@
import { type FrontComponentExecutionContext } from '@/front-component/types/FrontComponentExecutionContext';
import { type FrontComponentHostCommunicationApi } from '@/front-component/types/FrontComponentHostCommunicationApi';
import { type WorkerExports } from '@/front-component/types/WorkerExports';
import { type FrontComponentExecutionContext } from '@/front-component-renderer/types/FrontComponentExecutionContext';
import { type FrontComponentHostCommunicationApi } from '@/front-component-renderer/types/FrontComponentHostCommunicationApi';
import { type WorkerExports } from '@/front-component-renderer/types/WorkerExports';
import { type ThreadWebWorker } from '@quilted/threads';
import { useEffect } from 'react';
@@ -3,7 +3,7 @@ import { RemoteReceiver } from '@remote-dom/core/receivers';
import { useEffect, useRef } from 'react';
import { type FrontComponentHostCommunicationApi } from '../../types/FrontComponentHostCommunicationApi';
import { type WorkerExports } from '../../types/WorkerExports';
import { createRemoteWorker } from '../worker/createRemoteWorker';
import { createRemoteWorker } from '../worker/utils/createRemoteWorker';
type FrontComponentWorkerEffectProps = {
componentUrl: string;
@@ -41,7 +41,12 @@ export const FrontComponentWorkerEffect = ({
const worker = createRemoteWorker();
worker.onerror = (event: ErrorEvent) => {
setError(event.error);
const workerError =
event.error ??
new Error(event.message || 'Unknown worker error');
console.error('[FrontComponentRenderer] Worker error:', workerError);
setError(workerError);
};
// Expose host functions to the worker via stable refs to avoid recreating threads
@@ -13,7 +13,7 @@ import {
RemoteFragmentElement,
type RemoteEvent,
} from '@remote-dom/core/elements';
import { type SerializedEventData } from '../../../sdk/front-component-common/SerializedEventData';
import { type SerializedEventData } from '../../../sdk/front-component-api/constants/SerializedEventData';
export type HtmlCommonProperties = {
id?: string;
@@ -85,6 +85,12 @@ const render: WorkerExports['render'] = async (
const reactRoot = createRoot(root);
reactRoot.render(componentModule.default);
} catch (importError) {
console.error(
'[FrontComponentWorker] Failed to load or render component:',
importError,
);
throw importError;
} finally {
URL.revokeObjectURL(importUrl);
}
@@ -1,5 +1,5 @@
// @ts-expect-error - Vite worker inline import
import RemoteWorker from './remote-worker?worker&inline';
import RemoteWorker from '../remote-worker?worker&inline';
export const createRemoteWorker = (): Worker => {
return new RemoteWorker();
@@ -1,51 +0,0 @@
import * as esbuild from 'esbuild';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createFrontComponentBuildOptions } from '../../../cli/utilities/build/common/front-component-build/utils/create-front-component-build-options';
const dirname = path.dirname(fileURLToPath(import.meta.url));
const mocksDir = path.resolve(dirname, '../mocks');
const outputDir = path.resolve(dirname, '../built');
const sdkRoot = path.resolve(dirname, '../../../..');
const STORY_COMPONENTS = [
'static.front-component',
'interactive.front-component',
'lifecycle.front-component',
];
export const buildMockComponents = async (): Promise<void> => {
fs.mkdirSync(outputDir, { recursive: true });
const entryPoints: Record<string, string> = {};
for (const name of STORY_COMPONENTS) {
const filePath = path.join(mocksDir, `${name}.tsx`);
if (!fs.existsSync(filePath)) {
throw new Error(
`Story component source file not found: ${filePath}\n` +
`Ensure the file exists in ${mocksDir} and the name in STORY_COMPONENTS is correct.`,
);
}
entryPoints[name] = filePath;
}
const buildOptions = createFrontComponentBuildOptions({
entryPoints,
outdir: outputDir,
tsconfigPath: path.join(sdkRoot, 'tsconfig.json'),
});
await esbuild.build(buildOptions);
console.log(
`Built ${STORY_COMPONENTS.length} story components to ${outputDir}`,
);
};
buildMockComponents().catch((error) => {
console.error('Failed to build mock components:', error);
process.exit(1);
});
@@ -1,3 +0,0 @@
export const getBuiltComponentPath = (componentName: string): string => {
return `/built/${componentName}.mjs`;
};
-1
View File
@@ -1 +0,0 @@
export * from './sdk';
@@ -1,4 +1,4 @@
import { type PropertySchema } from '@/front-component/types/PropertySchema';
import { type PropertySchema } from '@/front-component-renderer/types/PropertySchema';
export const HTML_COMMON_PROPERTIES: Record<string, PropertySchema> = {
id: { type: 'string', optional: true },
@@ -3,3 +3,11 @@ export { navigate, setNavigate } from './functions/navigate';
export { useFrontComponentExecutionContext } from './hooks/useFrontComponentExecutionContext';
export { useUserId } from './hooks/useUserId';
export type { FrontComponentExecutionContext } from './types/FrontComponentExecutionContext';
export type { AllowedHtmlElement } from './constants/AllowedHtmlElements';
export { ALLOWED_HTML_ELEMENTS } from './constants/AllowedHtmlElements';
export { COMMON_HTML_EVENTS } from './constants/CommonHtmlEvents';
export { EVENT_TO_REACT } from './constants/EventToReact';
export { HTML_COMMON_PROPERTIES } from './constants/HtmlCommonProperties';
export { HTML_TAG_TO_REMOTE_COMPONENT } from './constants/HtmlTagToRemoteComponent';
export type { SerializedEventData } from './constants/SerializedEventData';
@@ -1,16 +0,0 @@
/*
* _____ _
*|_ _|_ _____ _ __ | |_ _ _
* | | \ \ /\ / / _ \ '_ \| __| | | | Auto-generated file
* | | \ V V / __/ | | | |_| |_| | Any edits to this will be overridden
* |_| \_/\_/ \___|_| |_|\__|\__, |
* |___/
*/
export type { AllowedHtmlElement } from './AllowedHtmlElements';
export { ALLOWED_HTML_ELEMENTS } from './AllowedHtmlElements';
export { COMMON_HTML_EVENTS } from './CommonHtmlEvents';
export { EVENT_TO_REACT } from './EventToReact';
export { HTML_COMMON_PROPERTIES } from './HtmlCommonProperties';
export { HTML_TAG_TO_REMOTE_COMPONENT } from './HtmlTagToRemoteComponent';
export type { SerializedEventData } from './SerializedEventData';
+6 -6
View File
@@ -57,9 +57,9 @@ export { useUserId } from './front-component-api';
export type { FrontComponentExecutionContext } from './front-component-api';
// Front Component Common exports
export type { AllowedHtmlElement } from './front-component-common';
export { ALLOWED_HTML_ELEMENTS } from './front-component-common';
export { COMMON_HTML_EVENTS } from './front-component-common';
export { EVENT_TO_REACT } from './front-component-common';
export { HTML_COMMON_PROPERTIES } from './front-component-common';
export { HTML_TAG_TO_REMOTE_COMPONENT } from './front-component-common';
export type { AllowedHtmlElement } from './front-component-api';
export { ALLOWED_HTML_ELEMENTS } from './front-component-api';
export { COMMON_HTML_EVENTS } from './front-component-api';
export { EVENT_TO_REACT } from './front-component-api';
export { HTML_COMMON_PROPERTIES } from './front-component-api';
export { HTML_TAG_TO_REMOTE_COMPONENT } from './front-component-api';
+6 -4
View File
@@ -26,12 +26,14 @@
"**/__mocks__/**/*",
"**/__tests__/**/*",
"vite.config.ts",
"vite.config.node.ts",
"vite.config.browser.ts",
"jest.config.mjs"
],
"exclude": [
"src/front-component/remote/mock/**/*",
"src/front-component/host/generated/host-component-registry.ts",
"src/front-component/remote/generated/remote-components.ts",
"src/front-component/remote/generated/remote-elements.ts"
"src/front-component-renderer/remote/mock/**/*",
"src/front-component-renderer/host/generated/host-component-registry.ts",
"src/front-component-renderer/remote/generated/remote-components.ts",
"src/front-component-renderer/remote/generated/remote-elements.ts"
]
}
@@ -1,38 +1,38 @@
import path from 'path';
import { PackageJson } from 'type-fest';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import packageJson from './package.json';
import { type PackageJson } from 'type-fest';
const entries = [
'src/index.ts',
'src/cli/cli.ts',
'src/ui/index.ts',
'src/front-component/index.ts',
];
import packageJson from './package.json';
const entries = ['src/ui/index.ts', 'src/front-component-renderer/index.ts'];
const entryFileNames = (chunk: any, extension: 'cjs' | 'mjs') => {
if (!chunk.isEntry) {
throw new Error(
`Should never occurs, encountered a non entry chunk ${chunk.facadeModuleId}`,
`Should never occur, encountered a non-entry chunk ${chunk.facadeModuleId}`,
);
}
// Find which entry this chunk corresponds to
const entry = entries.find((e) => chunk.facadeModuleId?.endsWith(e));
if (!entry || entry === 'src/index.ts' || entry === 'src/cli/cli.ts') {
const entry = entries.find((entryPath) =>
chunk.facadeModuleId?.endsWith(entryPath),
);
if (!entry) {
return `${chunk.name}.${extension}`;
}
// Remove 'src/' prefix and '/index.ts' suffix to get the module path
const modulePath = entry.replace('src/', '').replace('/index.ts', '');
return `${modulePath}/index.${extension}`;
};
export default defineConfig(() => {
return {
root: __dirname,
cacheDir: '../../node_modules/.vite/packages/twenty-sdk',
cacheDir: '../../node_modules/.vite/packages/twenty-sdk-browser',
resolve: {
alias: {
'@/': path.resolve(__dirname, 'src') + '/',
@@ -61,11 +61,11 @@ export default defineConfig(() => {
],
},
build: {
emptyOutDir: false,
outDir: 'dist',
lib: { entry: entries, name: 'twenty-sdk' },
rollupOptions: {
onwarn: (warning, warn) => {
// Suppress "use client" directive warnings from framer-motion
if (
warning.code === 'MODULE_LEVEL_DIRECTIVE' &&
warning.message.includes('"use client"')
@@ -75,26 +75,6 @@ export default defineConfig(() => {
warn(warning);
},
external: (id: string) => {
if (/^node:/.test(id)) {
return true;
}
const builtins = [
'path',
'fs',
'fs/promises',
'url',
'crypto',
'stream',
'util',
'os',
'module',
];
if (builtins.includes(id)) {
return true;
}
const deps = Object.entries(
(packageJson as PackageJson).dependencies || {},
).filter(([_, version]) => !version?.startsWith('workspace:'));
@@ -119,16 +99,5 @@ export default defineConfig(() => {
},
},
logLevel: 'warn',
optimizeDeps: {
include: [
'@remote-dom/core/polyfill',
'@remote-dom/react/polyfill',
'@remote-dom/core/elements',
'@remote-dom/react',
'@remote-dom/react/host',
'react-dom/client',
'react/jsx-runtime',
],
},
};
});
+76
View File
@@ -0,0 +1,76 @@
import path from 'path';
import { type PackageJson } from 'type-fest';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import packageJson from './package.json';
export default defineConfig(() => {
return {
root: __dirname,
cacheDir: '../../node_modules/.vite/packages/twenty-sdk-node',
resolve: {
alias: {
'@/': path.resolve(__dirname, 'src') + '/',
},
},
plugins: [
tsconfigPaths({
root: __dirname,
}),
],
build: {
emptyOutDir: false,
outDir: 'dist',
lib: {
entry: ['src/sdk/index.ts', 'src/cli/cli.ts'],
name: 'twenty-sdk',
},
rollupOptions: {
external: (id: string) => {
if (/^node:/.test(id)) {
return true;
}
const builtins = [
'path',
'fs',
'fs/promises',
'url',
'crypto',
'stream',
'util',
'os',
'module',
];
if (builtins.includes(id)) {
return true;
}
const deps = Object.entries(
(packageJson as PackageJson).dependencies || {},
).filter(([_, version]) => !version?.startsWith('workspace:'));
return deps.some(
([dep]) => id === dep || id.startsWith(dep + '/'),
);
},
output: [
{
format: 'es' as const,
entryFileNames: '[name].mjs',
},
{
format: 'cjs' as const,
interop: 'auto' as const,
esModule: true,
exports: 'named' as const,
entryFileNames: '[name].cjs',
},
],
},
},
logLevel: 'warn' as const,
};
});
@@ -20,7 +20,7 @@ export default defineConfig({
},
projects: [
{
extends: './vite.config.ts',
extends: './vite.config.browser.ts',
plugins: [
storybookTest({
configDir: path.join(dirname, '.storybook'),
@@ -37,6 +37,7 @@ export default defineConfig({
},
setupFiles: ['./.storybook/vitest.setup.ts'],
testTimeout: 5 * MINUTES_IN_MS,
retry: 2,
},
},
],
+1125 -2
View File
File diff suppressed because it is too large Load Diff