perf(sdk): split twenty-sdk barrel into per-purpose subpaths to cut logic-function bundle ~700x (#19834)

## Summary

Logic-function bundles produced by the twenty-sdk CLI were ~1.18 MB even
for a one-line handler. Root cause: the SDK shipped as a single bundled
barrel (`twenty-sdk` → `dist/index.mjs`) that co-mingled server-side
definition factories with the front-component runtime, validation (zod),
and React. With no `\"sideEffects\"` declaration on the SDK package,
esbuild had to assume every module-level statement could have side
effects and refused to drop unused code.

This PR restructures the SDK so consumers' bundlers can tree-shake at
the leaf level:

- **Reorganized SDK source.** All server-side definition factories now
  live under `src/sdk/define/` (agents, application, fields,
  logic-functions, objects, page-layouts, roles, skills, views,
  navigation-menu-items, etc.). All front-component runtime
  (components, hooks, host APIs, command primitives) lives under
  `src/sdk/front-component/`. The legacy bare `src/sdk/index.ts` is
  removed; the bare `twenty-sdk` entry no longer exists.

- **Split the build configs by purpose / runtime env.** Replaced
  `vite.config.sdk.ts` with two purpose-specific configs:
  - `vite.config.define.ts` — node target, externals from package
    `dependencies`, emits to `dist/define/**`
  - `vite.config.front-component.ts` — browser/React target, emits to
    `dist/front-component/**`
  Both use `preserveModules: true` so each leaf ships as its own `.mjs`.

- **\`\"sideEffects\": false\`** on `twenty-sdk` so esbuild can drop
  unreferenced re-exports.

- **\`package.json\` exports + \`typesVersions\`** updated: dropped the
bare \`.\` entry, added \`./front-component\`, and pointed \`./define\`
  at the new per-module dist layout.

- **Migrated every internal/example/community app** to the new subpath
  imports (`twenty-sdk/define`, `twenty-sdk/front-component`,
  `twenty-sdk/ui`).

- **Added \`bundle-investigation\` internal app** that reproduces the
  bundle bloat and demonstrates the fix.

- Cleaned up dead \`twenty-sdk/dist/sdk/...\` references in the
  front-component story builder, the call-recording app, and the SDK
  tsconfig.

## Bundle size impact

Measured with esbuild using the same options as the SDK CLI
(\`packages/twenty-apps/internal/bundle-investigation\`):

| Variant | Imports | Before | After |
| ----------------------- |
------------------------------------------------------- | ---------- |
--------- |
| \`01-bare\` | \`defineLogicFunction\` from \`twenty-sdk/define\` |
1177 KB | **1.6 KB** |
| \`02-with-sdk-client\` | + \`CoreApiClient\` from
\`twenty-client-sdk/core\` | 1177 KB | **1.9 KB** |
| \`03-fetch-issues\` | + GitHub GraphQL fetch + JWT signing + 2
mutations | 1181 KB | **5.8 KB** |
| \`05-via-define-subpath\` | same as \`01\`, via the public subpath |
1177 KB | **1.7 KB** |

That's a ~735× reduction on the bare baseline. Knock-on benefits for
Lambda warm + cold starts, S3 upload size, and \`/tmp\` disk usage in
warm containers.

## Test plan

- [x] \`npx nx run twenty-sdk:build\` succeeds
- [x] \`npx nx run twenty-sdk:typecheck\` passes
- [x] \`npx nx run twenty-sdk:test:unit\` passes (31 files / 257 tests)
- [x] \`npx nx run-many -t typecheck
--projects=twenty-front,twenty-server,twenty-front-component-renderer,twenty-sdk,twenty-shared,bundle-investigation\`
passes
- [x] \`node
packages/twenty-apps/internal/bundle-investigation/scripts/build-variants.mjs\`
produces the sizes above
- [ ] CI green

Made with [Cursor](https://cursor.com)
This commit is contained in:
Charles Bochet
2026-04-18 19:38:34 +02:00
committed by GitHub
parent fd495ee61b
commit eb1ca1b9ec
325 changed files with 691 additions and 786 deletions
@@ -1,4 +1,4 @@
import { FieldType } from '@/sdk';
import { FieldType } from '@/sdk/define';
import type { Manifest } from 'twenty-shared/application';
import {
FieldMetadataType,
@@ -1,4 +1,4 @@
import { FieldType } from '@/sdk';
import { FieldType } from '@/sdk/define';
import type { Manifest } from 'twenty-shared/application';
import { PermissionFlagType } from 'twenty-shared/constants';
import {
@@ -1,4 +1,5 @@
import { defineFrontComponent, numberOfSelectedRecords } from '@/sdk';
import { defineFrontComponent } from '@/sdk/define';
import { numberOfSelectedRecords } from '@/sdk/front-component';
const MyComponent = () => null;
@@ -1,10 +1,10 @@
import { defineFrontComponent } from '@/sdk/define';
import {
defineFrontComponent,
none,
numberOfSelectedRecords,
objectPermissions,
selectedRecords,
} from '@/sdk';
} from '@/sdk/front-component';
const MyComponent = () => null;
@@ -1,4 +1,5 @@
import { defineFrontComponent, someDefined, selectedRecords } from '@/sdk';
import { defineFrontComponent } from '@/sdk/define';
import { someDefined, selectedRecords } from '@/sdk/front-component';
const MyComponent = () => null;
@@ -1,4 +1,5 @@
import { defineFrontComponent, featureFlags, objectPermissions } from '@/sdk';
import { defineFrontComponent } from '@/sdk/define';
import { featureFlags, objectPermissions } from '@/sdk/front-component';
const MyComponent = () => null;
@@ -1,9 +1,9 @@
import { defineFrontComponent } from '@/sdk/define';
import {
defineFrontComponent,
favoriteRecordIds,
objectMetadataItem,
pageType,
} from '@/sdk';
} from '@/sdk/front-component';
const MyComponent = () => null;
@@ -1,4 +1,5 @@
import { defineFrontComponent, isInSidePanel, objectPermissions } from '@/sdk';
import { defineFrontComponent } from '@/sdk/define';
import { isInSidePanel, objectPermissions } from '@/sdk/front-component';
const MyComponent = () => null;
@@ -1,4 +1,5 @@
import { defineFrontComponent, pageType } from '@/sdk';
import { defineFrontComponent } from '@/sdk/define';
import { pageType } from '@/sdk/front-component';
const MyComponent = () => null;
@@ -1,9 +1,5 @@
import {
defineFrontComponent,
everyEquals,
pageType,
selectedRecords,
} from '@/sdk';
import { defineFrontComponent } from '@/sdk/define';
import { everyEquals, pageType, selectedRecords } from '@/sdk/front-component';
const MyComponent = () => null;
@@ -1,8 +1,5 @@
import {
defineFrontComponent,
pageType,
targetObjectWritePermissions,
} from '@/sdk';
import { defineFrontComponent } from '@/sdk/define';
import { pageType, targetObjectWritePermissions } from '@/sdk/front-component';
const MyComponent = () => null;
@@ -3,7 +3,7 @@ import { extractDefineEntity } from '@/cli/utilities/build/manifest/manifest-ext
describe('extractDefineEntity', () => {
it('should detect defineApplication in default export', () => {
const fileContent = `
import { defineApplication } from 'twenty-sdk';
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({ name: 'MyApp' });
`;
const result = extractDefineEntity(fileContent);
@@ -12,7 +12,7 @@ describe('extractDefineEntity', () => {
it('should detect defineField in default export', () => {
const fileContent = `
import { defineField } from 'twenty-sdk';
import { defineField } from 'twenty-sdk/define';
export default defineField({ name: 'myField' });
`;
const result = extractDefineEntity(fileContent);
@@ -21,7 +21,7 @@ describe('extractDefineEntity', () => {
it('should detect defineLogicFunction in default export', () => {
const fileContent = `
import { defineLogicFunction } from 'twenty-sdk';
import { defineLogicFunction } from 'twenty-sdk/define';
export default defineLogicFunction({ name: 'myFunction' });
`;
const result = extractDefineEntity(fileContent);
@@ -30,7 +30,7 @@ describe('extractDefineEntity', () => {
it('should detect defineObject in default export', () => {
const fileContent = `
import { defineObject } from 'twenty-sdk';
import { defineObject } from 'twenty-sdk/define';
export default defineObject({ name: 'myObject' });
`;
const result = extractDefineEntity(fileContent);
@@ -39,7 +39,7 @@ describe('extractDefineEntity', () => {
it('should detect defineRole in default export', () => {
const fileContent = `
import { defineRole } from 'twenty-sdk';
import { defineRole } from 'twenty-sdk/define';
export default defineRole({ name: 'myRole' });
`;
const result = extractDefineEntity(fileContent);
@@ -48,7 +48,7 @@ describe('extractDefineEntity', () => {
it('should detect defineFrontComponent in default export', () => {
const fileContent = `
import { defineFrontComponent } from 'twenty-sdk';
import { defineFrontComponent } from 'twenty-sdk/define';
export default defineFrontComponent({ name: 'myComponent' });
`;
const result = extractDefineEntity(fileContent);
@@ -57,7 +57,7 @@ describe('extractDefineEntity', () => {
it('should not detect non-target function in default export', () => {
const fileContent = `
import { someOtherFunction } from 'twenty-sdk';
import { someOtherFunction } from 'twenty-sdk/define';
export default someOtherFunction({ name: 'myFunction' });
`;
const result = extractDefineEntity(fileContent);
@@ -66,7 +66,7 @@ describe('extractDefineEntity', () => {
it('should handle files without default export', () => {
const fileContent = `
import { defineApplication } from 'twenty-sdk';
import { defineApplication } from 'twenty-sdk/define';
const app = defineApplication({ name: 'MyApp' });
`;
const result = extractDefineEntity(fileContent);
@@ -75,7 +75,7 @@ describe('extractDefineEntity', () => {
it('should handle files with non-call default export', () => {
const fileContent = `
import { defineApplication } from 'twenty-sdk';
import { defineApplication } from 'twenty-sdk/define';
export default { name: 'MyApp' };
`;
const result = extractDefineEntity(fileContent);
@@ -7,14 +7,11 @@ import {
} from '@/cli/utilities/build/manifest/manifest-extract-config';
import { extractManifestFromFile } from '@/cli/utilities/build/manifest/manifest-extract-config-from-file';
import { getDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields';
import {
type ApplicationConfig,
type FrontComponentConfig,
type LogicFunctionConfig,
} from '@/sdk';
import { type ObjectConfig } from '@/sdk/objects/object-config';
import { type PageLayoutConfig } from '@/sdk/page-layouts/page-layout-config';
import { type ViewConfig } from '@/sdk/views/view-config';
import { type ApplicationConfig, type LogicFunctionConfig } from '@/sdk/define';
import { type FrontComponentConfig } from '@/sdk/define/front-component/front-component-config';
import { type ObjectConfig } from '@/sdk/define/objects/object-config';
import { type PageLayoutConfig } from '@/sdk/define/page-layouts/page-layout-config';
import { type ViewConfig } from '@/sdk/define/views/view-config';
import { readFile } from 'node:fs/promises';
import { basename, extname, relative } from 'path';
import { glob } from 'tinyglobby';
@@ -40,10 +37,10 @@ import {
import { getInputSchemaFromSourceCode } from 'twenty-shared/logic-function';
import { assertUnreachable } from 'twenty-shared/utils';
import { addMissingFieldOptionIds } from '@/cli/utilities/build/manifest/utils/add-missing-field-option-ids';
import { type PostInstallLogicFunctionConfig } from '@/sdk/logic-functions/post-install-logic-function-config';
import { type PreInstallLogicFunctionConfig } from '@/sdk/logic-functions/pre-install-logic-function-config';
import { type PostInstallLogicFunctionConfig } from '@/sdk/define/logic-functions/post-install-logic-function-config';
import { type PreInstallLogicFunctionConfig } from '@/sdk/define/logic-functions/pre-install-logic-function-config';
import { fromRoleConfigToRoleManifest } from '@/cli/utilities/build/manifest/utils/from-role-config-to-role-manifest';
import { type RoleConfig } from '@/sdk/roles/role-config';
import { type RoleConfig } from '@/sdk/define/roles/role-config';
const loadSources = async (appPath: string): Promise<string[]> => {
return await glob(['**/*.ts', '**/*.tsx'], {
@@ -1,6 +1,6 @@
import { conditionalAvailabilityTransformPlugin } from '@/cli/utilities/build/common/conditional-availability/conditional-availability-transform-plugin';
import { pathExists, remove } from '@/cli/utilities/file/fs-utils';
import { type ValidationResult } from '@/sdk';
import { type ValidationResult } from '@/sdk/define';
import * as esbuild from 'esbuild';
import { createRequire } from 'module';
import { mkdtemp, writeFile } from 'node:fs/promises';
@@ -1,5 +1,5 @@
import { generateDefaultFieldUniversalIdentifier } from '@/cli/utilities/build/manifest/utils/generate-default-field-universal-identifier';
import { type ObjectConfig } from '@/sdk/objects/object-config';
import { type ObjectConfig } from '@/sdk/define/objects/object-config';
describe('generateDefaultFieldUniversalIdentifier', () => {
it('should generate a unique universal identifier', () => {
@@ -1,7 +1,7 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { getDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields';
import { getDefaultObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-object-fields';
import type { ObjectConfig } from '@/sdk/objects/object-config';
import type { ObjectConfig } from '@/sdk/define/objects/object-config';
import { getDefaultRelationObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-relation-object-fields';
import { type ObjectFieldManifest } from 'twenty-shared/application';
@@ -1,7 +1,7 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { getDefaultObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-object-fields';
import { generateDefaultFieldUniversalIdentifier } from '@/cli/utilities/build/manifest/utils/generate-default-field-universal-identifier';
import type { ObjectConfig } from '@/sdk/objects/object-config';
import type { ObjectConfig } from '@/sdk/define/objects/object-config';
const mockObjectConfig: ObjectConfig = {
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
@@ -1,4 +1,4 @@
import { type RoleConfig } from '@/sdk/roles/role-config';
import { type RoleConfig } from '@/sdk/define/roles/role-config';
import { type RoleManifest } from 'twenty-shared/application';
import { v5 as uuidv5 } from 'uuid';
@@ -1,4 +1,4 @@
import type { ObjectConfig } from '@/sdk/objects/object-config';
import type { ObjectConfig } from '@/sdk/define/objects/object-config';
import { v5 } from 'uuid';
const UNIVERSAL_IDENTIFIER_NAMESPACE = '142046f0-4d80-48b5-ad56-26ad410e895c';
@@ -1,4 +1,4 @@
import type { ObjectConfig } from '@/sdk/objects/object-config';
import type { ObjectConfig } from '@/sdk/define/objects/object-config';
import { getDefaultObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-object-fields';
import { getDefaultRelationObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-relation-object-fields';
import {
@@ -1,4 +1,4 @@
import type { ObjectConfig } from '@/sdk/objects/object-config';
import type { ObjectConfig } from '@/sdk/define/objects/object-config';
import { FieldMetadataType } from 'twenty-shared/types';
import { generateDefaultFieldUniversalIdentifier } from '@/cli/utilities/build/manifest/utils/generate-default-field-universal-identifier';
import { type ObjectFieldManifest } from 'twenty-shared/application';
@@ -1,6 +1,6 @@
import { generateDefaultFieldUniversalIdentifier } from '@/cli/utilities/build/manifest/utils/generate-default-field-universal-identifier';
import { RelationType } from '@/sdk';
import type { ObjectConfig } from '@/sdk/objects/object-config';
import { RelationType } from '@/sdk/define';
import type { ObjectConfig } from '@/sdk/define/objects/object-config';
import {
type FieldManifest,
type ObjectFieldManifest,
@@ -7,7 +7,9 @@ describe('getAgentBaseFile', () => {
universalIdentifier: '71e45a58-41da-4ae4-8b73-a543c0a9d3d4',
});
expect(result).toContain("import { defineAgent } from 'twenty-sdk'");
expect(result).toContain(
"import { defineAgent } from 'twenty-sdk/define';",
);
expect(result).toContain('export default defineAgent({');
expect(result).toContain(
'universalIdentifier: MY_AGENT_AGENT_UNIVERSAL_IDENTIFIER',
@@ -13,7 +13,7 @@ describe('getFieldBaseFile', () => {
});
expect(result).toContain(
"import { defineField, FieldType } from 'twenty-sdk'",
"import { defineField, FieldType } from 'twenty-sdk/define';",
);
expect(result).toContain('export default defineField({');
expect(result).toContain("name: 'email'");
@@ -8,7 +8,7 @@ describe('getFrontComponentBaseFile', () => {
});
expect(result).toContain(
"import { defineFrontComponent } from 'twenty-sdk'",
"import { defineFrontComponent } from 'twenty-sdk/define';",
);
expect(result).toContain('export default defineFrontComponent({');
@@ -8,7 +8,7 @@ describe('getFunctionBaseFile', () => {
});
expect(result).toContain(
"import { defineLogicFunction } from 'twenty-sdk'",
"import { defineLogicFunction } from 'twenty-sdk/define';",
);
expect(result).toContain('export default defineLogicFunction({');
@@ -8,7 +8,7 @@ describe('getNavigationMenuItemBaseFile', () => {
});
expect(result).toContain(
"import { defineNavigationMenuItem } from 'twenty-sdk'",
"import { defineNavigationMenuItem } from 'twenty-sdk/define';",
);
expect(result).toContain('export default defineNavigationMenuItem({');
expect(result).toContain(
@@ -13,7 +13,7 @@ describe('getNewObjectFileContent', () => {
});
expect(result).toContain(
"import { defineObject, FieldType } from 'twenty-sdk'",
"import { defineObject, FieldType } from 'twenty-sdk/define';",
);
expect(result).toContain('export default defineObject({');
expect(result).toContain('export const NAME_FIELD_UNIVERSAL_IDENTIFIER');
@@ -6,7 +6,9 @@ describe('getPageLayoutBaseFile', () => {
name: 'my-layout',
});
expect(result).toContain("import { definePageLayout } from 'twenty-sdk'");
expect(result).toContain(
"import { definePageLayout } from 'twenty-sdk/define';",
);
expect(result).toContain('export default definePageLayout({');
expect(result).toContain("name: 'my-layout'");
expect(result).toContain("title: 'Overview'");
@@ -7,7 +7,7 @@ describe('getRoleBaseFile', () => {
universalIdentifier: '71e45a58-41da-4ae4-8b73-a543c0a9d3d4',
});
expect(result).toContain("import { defineRole } from 'twenty-sdk'");
expect(result).toContain("import { defineRole } from 'twenty-sdk/define';");
expect(result).toContain('export default defineRole({');
expect(result).toContain(
@@ -7,7 +7,9 @@ describe('getSkillBaseFile', () => {
universalIdentifier: '71e45a58-41da-4ae4-8b73-a543c0a9d3d4',
});
expect(result).toContain("import { defineSkill } from 'twenty-sdk'");
expect(result).toContain(
"import { defineSkill } from 'twenty-sdk/define';",
);
expect(result).toContain('export default defineSkill({');
expect(result).toContain(
'universalIdentifier: MY_SKILL_SKILL_UNIVERSAL_IDENTIFIER',
@@ -8,7 +8,7 @@ describe('getViewBaseFile', () => {
objectUniversalIdentifier: 'obj-abc-123',
});
expect(result).toContain("import { defineView } from 'twenty-sdk'");
expect(result).toContain("import { defineView } from 'twenty-sdk/define';");
expect(result).toContain('export default defineView({');
expect(result).toContain(
"universalIdentifier: '71e45a58-41da-4ae4-8b73-a543c0a9d3d4'",
@@ -10,7 +10,7 @@ export const getAgentBaseFile = ({
}) => {
const kebabCaseName = kebabCase(name);
return `import { defineAgent } from 'twenty-sdk';
return `import { defineAgent } from 'twenty-sdk/define';
export const ${kebabCaseName.toUpperCase().replace(/-/g, '_')}_AGENT_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
@@ -18,7 +18,7 @@ export const getFieldBaseFile = ({
? `\n description: '${data.description}',`
: '';
return `import { defineField, FieldType } from 'twenty-sdk';
return `import { defineField, FieldType } from 'twenty-sdk/define';
export default defineField({
universalIdentifier: '${universalIdentifier}',
@@ -10,7 +10,7 @@ export const getFrontComponentBaseFile = ({
}) => {
const kebabCaseName = kebabCase(name);
return `import { defineFrontComponent } from 'twenty-sdk';
return `import { defineFrontComponent } from 'twenty-sdk/define';
// React component - implement your UI here
const Component = () => {
@@ -10,7 +10,7 @@ export const getLogicFunctionBaseFile = ({
}) => {
const kebabCaseName = kebabCase(name);
return `import { defineLogicFunction } from 'twenty-sdk';
return `import { defineLogicFunction } from 'twenty-sdk/define';
// Logic function handler - rename and implement your logic
const handler = async (params: {
@@ -34,7 +34,7 @@ export const getNavigationMenuItemBaseFile = ({
// viewUniversalIdentifier: '...',`;
}
return `import { defineNavigationMenuItem } from 'twenty-sdk';
return `import { defineNavigationMenuItem } from 'twenty-sdk/define';
export default defineNavigationMenuItem({
universalIdentifier: '${universalIdentifier}',
@@ -15,7 +15,7 @@ export const getObjectBaseFile = ({
universalIdentifier?: string;
nameFieldUniversalIdentifier?: string;
}) => {
return `import { defineObject, FieldType } from 'twenty-sdk';
return `import { defineObject, FieldType } from 'twenty-sdk/define';
export const NAME_FIELD_UNIVERSAL_IDENTIFIER =
'${nameFieldUniversalIdentifier}';
@@ -1,7 +1,7 @@
import { v4 as uuidv4 } from 'uuid';
export const getPageLayoutBaseFile = ({ name }: { name: string }) => {
return `import { definePageLayout } from 'twenty-sdk';
return `import { definePageLayout } from 'twenty-sdk/define';
export default definePageLayout({
universalIdentifier: '${uuidv4()}',
@@ -10,7 +10,7 @@ export const getRoleBaseFile = ({
}) => {
const kebabCaseName = kebabCase(name);
return `import { defineRole } from 'twenty-sdk';
return `import { defineRole } from 'twenty-sdk/define';
export const ${kebabCaseName.toUpperCase().replace(/-/g, '_')}_ROLE_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
@@ -10,7 +10,7 @@ export const getSkillBaseFile = ({
}) => {
const kebabCaseName = kebabCase(name);
return `import { defineSkill } from 'twenty-sdk';
return `import { defineSkill } from 'twenty-sdk/define';
export const ${kebabCaseName.toUpperCase().replace(/-/g, '_')}_SKILL_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
@@ -51,7 +51,7 @@ export const getViewBaseFile = ({
],`
: defaultFields;
return `import { defineView } from 'twenty-sdk';
return `import { defineView } from 'twenty-sdk/define';
export default defineView({
universalIdentifier: '${universalIdentifier}',