mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
## 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)
97 lines
2.7 KiB
TypeScript
97 lines
2.7 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { defineFrontComponent } from 'twenty-sdk/define';
|
|
import { useRecordId, updateProgress, enqueueSnackbar, unmountFrontComponent } from 'twenty-sdk/front-component';
|
|
import { CoreApiClient } from 'twenty-client-sdk/core';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
|
|
|
const SendPostCardsEffect = () => {
|
|
const recordId = useRecordId();
|
|
|
|
useEffect(() => {
|
|
const send = async () => {
|
|
try {
|
|
await updateProgress(0.1);
|
|
const client = new CoreApiClient();
|
|
|
|
let idsToSend: string[] = [];
|
|
|
|
if (isDefined(recordId)) {
|
|
idsToSend = [recordId];
|
|
} else {
|
|
const { postCards } = await client.query({
|
|
postCards: {
|
|
__args: {
|
|
filter: { status: { eq: 'DRAFT' } },
|
|
},
|
|
edges: { node: { id: true } },
|
|
},
|
|
});
|
|
|
|
idsToSend =
|
|
postCards?.edges?.map(
|
|
(edge: { node: { id: string; status: true } }) => edge.node.id,
|
|
) ?? [];
|
|
}
|
|
|
|
if (idsToSend.length === 0) {
|
|
await updateProgress(1);
|
|
await unmountFrontComponent();
|
|
return;
|
|
}
|
|
|
|
await updateProgress(0.3);
|
|
|
|
for (let i = 0; i < idsToSend.length; i++) {
|
|
await client.mutation({
|
|
updatePostCard: {
|
|
__args: {
|
|
id: idsToSend[i],
|
|
data: { status: 'SENT' },
|
|
},
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
await updateProgress(0.3 + (0.7 * (i + 1)) / idsToSend.length);
|
|
}
|
|
|
|
const count = idsToSend.length;
|
|
|
|
await enqueueSnackbar({
|
|
message: `${count} postcard${count > 1 ? 's' : ''} sent`,
|
|
variant: 'success',
|
|
});
|
|
|
|
await unmountFrontComponent();
|
|
} catch (error) {
|
|
const message =
|
|
error instanceof Error ? error.message : 'Failed to send postcards';
|
|
|
|
await enqueueSnackbar({ message, variant: 'error' });
|
|
await unmountFrontComponent();
|
|
}
|
|
};
|
|
|
|
send();
|
|
}, [recordId]);
|
|
|
|
return null;
|
|
};
|
|
|
|
export default defineFrontComponent({
|
|
universalIdentifier: 'a3b7c2d1-4e5f-4a8b-9c0d-1e2f3a4b5c6d',
|
|
name: 'Send Post Cards',
|
|
description: 'Sets postcard status to Sent',
|
|
isHeadless: true,
|
|
component: SendPostCardsEffect,
|
|
command: {
|
|
universalIdentifier: 'bd75de13-87a1-4f7a-94e2-92e19e97523c',
|
|
label: 'Send post cards',
|
|
shortLabel: 'Send',
|
|
icon: 'IconSend',
|
|
isPinned: true,
|
|
availabilityType: 'RECORD_SELECTION',
|
|
availabilityObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
|
},
|
|
});
|