[FRONT COMPONENTS] Expose twenty UI in twenty sdk (#17645)

- An app will declare its `twenty-sdk` version in the manifest.
- `twenty-sdk` will be served by a CDN to be imported in front component
host.
- When we render the host we will load twenty-ui through the correct
version of the sdk

We will proceed this way because twenty-ui is not mature enough yet to
be deployed as its own package. So instead, since the sdk is already
deployed as its own package, we will serve the ui with it. It allows us
to have versioning to handle breaking changes in twenty-ui.
This commit is contained in:
Raphaël Bosi
2026-02-04 11:43:08 +01:00
committed by GitHub
parent f6b210dd0d
commit 9b8eacb7f7
6 changed files with 189 additions and 125 deletions
+17 -7
View File
@@ -5,12 +5,13 @@
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"bin": {
"twenty": "dist/cli.cjs"
"twenty": "dist/cli/index.cjs"
},
"files": [
"dist",
"front-component",
"front-component-constants"
"front-component-constants",
"ui"
],
"scripts": {
"build": "npx rimraf dist && npx vite build"
@@ -31,13 +32,18 @@
},
"./front-component": {
"types": "./dist/front-component/index.d.ts",
"import": "./dist/front-component.mjs",
"require": "./dist/front-component.cjs"
"import": "./dist/front-component/index.mjs",
"require": "./dist/front-component/index.cjs"
},
"./front-component-constants": {
"types": "./dist/front-component-constants/index.d.ts",
"import": "./dist/front-component-constants.mjs",
"require": "./dist/front-component-constants.cjs"
"import": "./dist/front-component-constants/index.mjs",
"require": "./dist/front-component-constants/index.cjs"
},
"./ui": {
"types": "./dist/ui/index.d.ts",
"import": "./dist/ui/index.mjs",
"require": "./dist/ui/index.cjs"
}
},
"license": "AGPL-3.0",
@@ -58,7 +64,7 @@
"fs-extra": "^11.2.0",
"graphql": "^16.8.1",
"graphql-sse": "^2.5.4",
"ink": "^6.6.0",
"ink": "^5.1.1",
"inquirer": "^10.0.0",
"jsonc-parser": "^3.2.0",
"lodash.camelcase": "^4.3.0",
@@ -83,6 +89,7 @@
"@types/react-dom": "18.2.22",
"ts-morph": "^25.0.0",
"tsx": "^4.7.0",
"twenty-ui": "workspace:*",
"vite-plugin-dts": "^4.5.4",
"wait-on": "^7.2.0"
},
@@ -97,6 +104,9 @@
],
"front-component-constants": [
"dist/front-component-constants/index.d.ts"
],
"ui": [
"dist/ui/index.d.ts"
]
}
}
+11 -29
View File
@@ -3,30 +3,23 @@
"$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": {
"dependsOn": [
"generateBarrels",
"^build"
],
"dependsOn": ["generateBarrels", "^build"],
"outputs": [
"{projectRoot}/dist",
"{projectRoot}/front-component/package.json",
"{projectRoot}/front-component/dist",
"{projectRoot}/front-component-constants/package.json",
"{projectRoot}/front-component-constants/dist"
"{projectRoot}/front-component-constants/dist",
"{projectRoot}/ui/package.json",
"{projectRoot}/ui/dist"
]
},
"dev": {
"executor": "nx:run-commands",
"dependsOn": [
"generateBarrels",
"^build"
],
"dependsOn": ["generateBarrels", "^build"],
"options": {
"cwd": "packages/twenty-sdk",
"command": "npx rimraf dist && npx vite build --watch"
@@ -34,9 +27,7 @@
},
"start": {
"executor": "nx:run-commands",
"dependsOn": [
"build"
],
"dependsOn": ["build"],
"options": {
"cwd": "packages/twenty-sdk",
"command": "node dist/cli.cjs"
@@ -45,10 +36,7 @@
"generateBarrels": {
"executor": "nx:run-commands",
"cache": true,
"inputs": [
"production",
"{projectRoot}/scripts/generateBarrels.ts"
],
"inputs": ["production", "{projectRoot}/scripts/generateBarrels.ts"],
"outputs": [
"{projectRoot}/src/index.ts",
"{projectRoot}/src/*/index.ts",
@@ -61,16 +49,12 @@
"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": {}
@@ -78,9 +62,7 @@
},
"test": {
"executor": "@nx/vitest:test",
"outputs": [
"{workspaceRoot}/coverage/{projectRoot}"
],
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"config": "{projectRoot}/vitest.config.ts"
},
+39 -3
View File
@@ -35,6 +35,21 @@ const EXCLUDED_DIRECTORIES = [
] as const;
const ROOT_DIRECTORIES = ['sdk'];
// Modules that re-export from external packages instead of local files
const EXTERNAL_REEXPORT_MODULES: Record<string, string> = {
ui: `export * from 'twenty-ui';
export * from 'twenty-ui/accessibility';
export * from 'twenty-ui/components';
export * from 'twenty-ui/display';
export * from 'twenty-ui/feedback';
export * from 'twenty-ui/input';
export * from 'twenty-ui/json-visualizer';
export * from 'twenty-ui/layout';
export * from 'twenty-ui/navigation';
export * from 'twenty-ui/theme';
export * from 'twenty-ui/utilities';`,
};
const prettierConfigFile = prettier.resolveConfigFile();
if (prettierConfigFile == null) {
throw new Error('Prettier config file not found');
@@ -223,8 +238,8 @@ const generateModulePackageExports = (moduleDirectories: string[]) => {
...acc,
[`./${moduleName}`]: {
types: `./dist/${moduleName}/index.d.ts`,
import: `./dist/${moduleName}.mjs`,
require: `./dist/${moduleName}.cjs`,
import: `./dist/${moduleName}/index.mjs`,
require: `./dist/${moduleName}/index.cjs`,
},
};
}, {});
@@ -490,11 +505,17 @@ const main = () => {
ROOT_DIRECTORIES.includes(getLastPathFolder(dir)),
);
// Separate modules that re-export from external packages
const externalReexportModuleNames = Object.keys(EXTERNAL_REEXPORT_MODULES);
const localModuleDirectories = moduleDirectories.filter(
(dir) => !externalReexportModuleNames.includes(getLastPathFolder(dir)),
);
const otherBarrelDirectories = moduleDirectories.filter(
(dir) => !ROOT_DIRECTORIES.includes(getLastPathFolder(dir)),
);
const exportsByBarrel = retrieveExportsByBarrel(moduleDirectories);
const exportsByBarrel = retrieveExportsByBarrel(localModuleDirectories);
const moduleIndexFiles = generateModuleIndexFiles(exportsByBarrel);
const packageJsonConfig = computePackageJsonFilesAndExportsConfig(
@@ -508,6 +529,21 @@ const main = () => {
writeInPackageJson(packageJsonConfig);
moduleIndexFiles.forEach(createTypeScriptFile);
// Generate index files for modules that re-export from external packages
for (const [moduleName, content] of Object.entries(
EXTERNAL_REEXPORT_MODULES,
)) {
const moduleDirectory = path.join(SRC_PATH, moduleName);
if (!fs.existsSync(moduleDirectory)) {
fs.mkdirSync(moduleDirectory, { recursive: true });
}
createTypeScriptFile({
path: moduleDirectory,
filename: INDEX_FILENAME,
content,
});
}
// Ensure top-level src/index.ts re-exports the root directories barrel so consumers can `import * from "twenty-sdk"`
// We intentionally keep this file minimal: it delegates to the generated src/<rootDirectory>/index.ts
if (rootDirectory) {
+20
View File
@@ -0,0 +1,20 @@
/*
* _____ _
*|_ _|_ _____ _ __ | |_ _ _
* | | \ \ /\ / / _ \ '_ \| __| | | | Auto-generated file
* | | \ V V / __/ | | | |_| |_| | Any edits to this will be overridden
* |_| \_/\_/ \___|_| |_|\__|\__, |
* |___/
*/
export * from 'twenty-ui';
export * from 'twenty-ui/accessibility';
export * from 'twenty-ui/components';
export * from 'twenty-ui/display';
export * from 'twenty-ui/feedback';
export * from 'twenty-ui/input';
export * from 'twenty-ui/json-visualizer';
export * from 'twenty-ui/layout';
export * from 'twenty-ui/navigation';
export * from 'twenty-ui/theme';
export * from 'twenty-ui/utilities';
+14 -2
View File
@@ -31,7 +31,7 @@ const entryFileNames = (chunk: any, extension: 'cjs' | 'mjs') => {
if (moduleDirectory === 'src') {
return `${chunk.name}.${extension}`;
}
return `${moduleDirectory}.${extension}`;
return `${moduleDirectory}/index.${extension}`;
};
const copyTwentyPackagesInVendor = (packages: string[]) => {
@@ -115,8 +115,20 @@ export default defineConfig(() => {
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"')
) {
return;
}
warn(warning);
},
external: [
...Object.keys((packageJson as any).dependencies || {}),
...Object.keys((packageJson as any).dependencies || {}).filter(
(dep) => dep !== 'twenty-ui',
),
'path',
'fs',
'fs/promises',
+88 -84
View File
@@ -147,6 +147,16 @@ __metadata:
languageName: node
linkType: hard
"@alcalzone/ansi-tokenize@npm:^0.1.3":
version: 0.1.3
resolution: "@alcalzone/ansi-tokenize@npm:0.1.3"
dependencies:
ansi-styles: "npm:^6.2.1"
is-fullwidth-code-point: "npm:^4.0.0"
checksum: 10c0/b88c5708271bb64ce132fc80dac8d5b87fc1699bf3abfdf10ecae40dbb56ab82460818f5746ecdd9870a00be9854e039d5cb3a121b808d0ff6f5bc7d0146cb38
languageName: node
linkType: hard
"@alcalzone/ansi-tokenize@npm:^0.2.1":
version: 0.2.2
resolution: "@alcalzone/ansi-tokenize@npm:0.2.2"
@@ -27580,15 +27590,6 @@ __metadata:
languageName: node
linkType: hard
"ansi-escapes@npm:^7.2.0":
version: 7.2.0
resolution: "ansi-escapes@npm:7.2.0"
dependencies:
environment: "npm:^1.0.0"
checksum: 10c0/b562fd995761fa12f33be316950ee58fda489e125d331bcd9131434969a2eb55dc14e9405f214dcf4697c9d67c576ba0baf6e8f3d52058bf9222c97560b220cb
languageName: node
linkType: hard
"ansi-regex@npm:^2.0.0":
version: 2.1.1
resolution: "ansi-regex@npm:2.1.1"
@@ -30661,16 +30662,6 @@ __metadata:
languageName: node
linkType: hard
"cli-truncate@npm:^5.1.1":
version: 5.1.1
resolution: "cli-truncate@npm:5.1.1"
dependencies:
slice-ansi: "npm:^7.1.0"
string-width: "npm:^8.0.0"
checksum: 10c0/3842920829a62f3e041ce39199050c42706c3c9c756a4efc8b86d464e102d1fa031d8b1b9b2e3bb36e1017c763558275472d031bdc884c1eff22a2f20e4f6b0a
languageName: node
linkType: hard
"cli-ux@npm:^4.9.0":
version: 4.9.3
resolution: "cli-ux@npm:4.9.3"
@@ -34223,6 +34214,18 @@ __metadata:
languageName: node
linkType: hard
"es-toolkit@npm:^1.22.0":
version: 1.44.0
resolution: "es-toolkit@npm:1.44.0"
dependenciesMeta:
"@trivago/prettier-plugin-sort-imports@4.3.0":
unplugged: true
prettier-plugin-sort-re-exports@0.0.1:
unplugged: true
checksum: 10c0/b80ff52ddc85ba26914cda57c9d4e46379ccc38c60dc097ef0d065cc0b20f95a16cf8d537969eea600b51c6687b5900a6cce67489db16d5ccc14d47597a29c34
languageName: node
linkType: hard
"es-toolkit@npm:^1.39.10":
version: 1.41.0
resolution: "es-toolkit@npm:1.41.0"
@@ -37335,7 +37338,7 @@ __metadata:
languageName: node
linkType: hard
"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.0, get-east-asian-width@npm:^1.3.1":
"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.1":
version: 1.4.0
resolution: "get-east-asian-width@npm:1.4.0"
checksum: 10c0/4e481d418e5a32061c36fbb90d1b225a254cc5b2df5f0b25da215dcd335a3c111f0c2023ffda43140727a9cafb62dac41d022da82c08f31083ee89f714ee3b83
@@ -39666,6 +39669,47 @@ __metadata:
languageName: node
linkType: hard
"ink@npm:^5.1.1":
version: 5.2.1
resolution: "ink@npm:5.2.1"
dependencies:
"@alcalzone/ansi-tokenize": "npm:^0.1.3"
ansi-escapes: "npm:^7.0.0"
ansi-styles: "npm:^6.2.1"
auto-bind: "npm:^5.0.1"
chalk: "npm:^5.3.0"
cli-boxes: "npm:^3.0.0"
cli-cursor: "npm:^4.0.0"
cli-truncate: "npm:^4.0.0"
code-excerpt: "npm:^4.0.0"
es-toolkit: "npm:^1.22.0"
indent-string: "npm:^5.0.0"
is-in-ci: "npm:^1.0.0"
patch-console: "npm:^2.0.0"
react-reconciler: "npm:^0.29.0"
scheduler: "npm:^0.23.0"
signal-exit: "npm:^3.0.7"
slice-ansi: "npm:^7.1.0"
stack-utils: "npm:^2.0.6"
string-width: "npm:^7.2.0"
type-fest: "npm:^4.27.0"
widest-line: "npm:^5.0.0"
wrap-ansi: "npm:^9.0.0"
ws: "npm:^8.18.0"
yoga-layout: "npm:~3.2.1"
peerDependencies:
"@types/react": ">=18.0.0"
react: ">=18.0.0"
react-devtools-core: ^4.19.1
peerDependenciesMeta:
"@types/react":
optional: true
react-devtools-core:
optional: true
checksum: 10c0/0e2607712783353fbe99438ca4220db3d5874c7ae1a07e2b232f7539489b13a9db929b3046eedeccf318a3ffa222a572287dbe2307c848b8e7f6ec4bba39e550
languageName: node
linkType: hard
"ink@npm:^6.0.1":
version: 6.4.0
resolution: "ink@npm:6.4.0"
@@ -39706,46 +39750,6 @@ __metadata:
languageName: node
linkType: hard
"ink@npm:^6.6.0":
version: 6.6.0
resolution: "ink@npm:6.6.0"
dependencies:
"@alcalzone/ansi-tokenize": "npm:^0.2.1"
ansi-escapes: "npm:^7.2.0"
ansi-styles: "npm:^6.2.1"
auto-bind: "npm:^5.0.1"
chalk: "npm:^5.6.0"
cli-boxes: "npm:^3.0.0"
cli-cursor: "npm:^4.0.0"
cli-truncate: "npm:^5.1.1"
code-excerpt: "npm:^4.0.0"
es-toolkit: "npm:^1.39.10"
indent-string: "npm:^5.0.0"
is-in-ci: "npm:^2.0.0"
patch-console: "npm:^2.0.0"
react-reconciler: "npm:^0.33.0"
signal-exit: "npm:^3.0.7"
slice-ansi: "npm:^7.1.0"
stack-utils: "npm:^2.0.6"
string-width: "npm:^8.1.0"
type-fest: "npm:^4.27.0"
widest-line: "npm:^5.0.0"
wrap-ansi: "npm:^9.0.0"
ws: "npm:^8.18.0"
yoga-layout: "npm:~3.2.1"
peerDependencies:
"@types/react": ">=19.0.0"
react: ">=19.0.0"
react-devtools-core: ^6.1.2
peerDependenciesMeta:
"@types/react":
optional: true
react-devtools-core:
optional: true
checksum: 10c0/60fe53f122f025c2ee849fb79aba13923fc2ca0ff9ff9af05725742ba613ccad5f1b7e7aba36f6b4a64a9e1c2eb8caa1c678d47993f1281d71408df12b4b82b6
languageName: node
linkType: hard
"inline-source-map@npm:~0.6.0":
version: 0.6.3
resolution: "inline-source-map@npm:0.6.3"
@@ -40354,6 +40358,15 @@ __metadata:
languageName: node
linkType: hard
"is-in-ci@npm:^1.0.0":
version: 1.0.0
resolution: "is-in-ci@npm:1.0.0"
bin:
is-in-ci: cli.js
checksum: 10c0/98f9cec4c35aece4cf731abf35ebf28359a9b0324fac810da05b842515d9ccb33b8999c1d9a679f0362e1a4df3292065c38d7f86327b1387fa667bc0150f4898
languageName: node
linkType: hard
"is-in-ci@npm:^2.0.0":
version: 2.0.0
resolution: "is-in-ci@npm:2.0.0"
@@ -51804,6 +51817,18 @@ __metadata:
languageName: node
linkType: hard
"react-reconciler@npm:^0.29.0":
version: 0.29.2
resolution: "react-reconciler@npm:0.29.2"
dependencies:
loose-envify: "npm:^1.1.0"
scheduler: "npm:^0.23.2"
peerDependencies:
react: ^18.3.1
checksum: 10c0/94f48ddc348a974256cf13c859f5a94efdb0cd72e04c51b1a4d5c72a8b960ccd35df2196057ee6a4cbcb26145e12b01e3f9ba3b183fddb901414db36a07cbf43
languageName: node
linkType: hard
"react-reconciler@npm:^0.32.0":
version: 0.32.0
resolution: "react-reconciler@npm:0.32.0"
@@ -51815,17 +51840,6 @@ __metadata:
languageName: node
linkType: hard
"react-reconciler@npm:^0.33.0":
version: 0.33.0
resolution: "react-reconciler@npm:0.33.0"
dependencies:
scheduler: "npm:^0.27.0"
peerDependencies:
react: ^19.2.0
checksum: 10c0/3f7b27ea8d0ff4c8bf0e402a285e1af9b7d0e6f4c1a70a28f4384938bc1130bc82a90a31df0b79ef5e380e2e55e2598bd90b4dbf802b1203d735ba0355817d3a
languageName: node
linkType: hard
"react-redux@npm:^8.1.3":
version: 8.1.3
resolution: "react-redux@npm:8.1.3"
@@ -53922,7 +53936,7 @@ __metadata:
languageName: node
linkType: hard
"scheduler@npm:^0.23.2":
"scheduler@npm:^0.23.0, scheduler@npm:^0.23.2":
version: 0.23.2
resolution: "scheduler@npm:0.23.2"
dependencies:
@@ -55670,16 +55684,6 @@ __metadata:
languageName: node
linkType: hard
"string-width@npm:^8.0.0, string-width@npm:^8.1.0":
version: 8.1.0
resolution: "string-width@npm:8.1.0"
dependencies:
get-east-asian-width: "npm:^1.3.0"
strip-ansi: "npm:^7.1.0"
checksum: 10c0/749b5d0dab2532b4b6b801064230f4da850f57b3891287023117ab63a464ad79dd208f42f793458f48f3ad121fe2e1f01dd525ff27ead957ed9f205e27406593
languageName: node
linkType: hard
"string.prototype.includes@npm:^2.0.1":
version: 2.0.1
resolution: "string.prototype.includes@npm:2.0.1"
@@ -57696,7 +57700,7 @@ __metadata:
fs-extra: "npm:^11.2.0"
graphql: "npm:^16.8.1"
graphql-sse: "npm:^2.5.4"
ink: "npm:^6.6.0"
ink: "npm:^5.1.1"
inquirer: "npm:^10.0.0"
jsonc-parser: "npm:^3.2.0"
lodash.camelcase: "npm:^4.3.0"
@@ -57714,7 +57718,7 @@ __metadata:
wait-on: "npm:^7.2.0"
zod: "npm:^4.1.11"
bin:
twenty: dist/cli.cjs
twenty: dist/cli/index.cjs
languageName: unknown
linkType: soft