Files
twenty/packages/twenty-front-component-renderer/scripts/front-component-stories/build-source-examples.ts
T
Charles BochetandGitHub eb1ca1b9ec 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)
2026-04-18 19:38:34 +02:00

182 lines
4.8 KiB
TypeScript

import * as esbuild from 'esbuild';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { getFrontComponentBuildPlugins } from 'twenty-sdk/front-component-renderer/build';
const dirname = path.dirname(fileURLToPath(import.meta.url));
const exampleSourcesDir = path.resolve(
dirname,
'../../src/__stories__/example-sources',
);
const exampleSourcesBuiltDir = path.resolve(
dirname,
'../../src/__stories__/example-sources-built',
);
const exampleSourcesBuiltPreactDir = path.resolve(
dirname,
'../../src/__stories__/example-sources-built-preact',
);
const rootNodeModules = path.resolve(dirname, '../../../../node_modules');
const twentyUiIndividualIndex = path.resolve(
dirname,
'../../../twenty-ui/dist/individual/individual-entry.js',
);
const sdkDefineIndex = path.resolve(
dirname,
'../../../twenty-sdk/dist/define/index.mjs',
);
const sdkFrontComponentIndex = path.resolve(
dirname,
'../../../twenty-sdk/dist/front-component/index.mjs',
);
const twentySharedIndividualDir = path.resolve(
dirname,
'../../../twenty-shared/dist/individual',
);
const TWENTY_SHARED_SUBMODULES = [
'ai',
'application',
'constants',
'database-events',
'metadata',
'testing',
'translations',
'types',
'utils',
'workflow',
'workspace',
];
const twentySharedAliases = Object.fromEntries(
TWENTY_SHARED_SUBMODULES.map((submodule) => [
`twenty-shared/${submodule}`,
path.join(twentySharedIndividualDir, submodule, 'index.js'),
]),
);
const storyAlias = {
react: path.join(rootNodeModules, 'react'),
'react-dom': path.join(rootNodeModules, 'react-dom'),
'twenty-sdk/define': sdkDefineIndex,
'twenty-sdk/front-component': sdkFrontComponentIndex,
'twenty-sdk/ui': twentyUiIndividualIndex,
...twentySharedAliases,
};
const STORY_COMPONENTS = [
'static.front-component',
'interactive.front-component',
'lifecycle.front-component',
'chakra-example.front-component',
'tailwind-example.front-component',
'emotion-example.front-component',
'styled-components-example.front-component',
'shadcn-example.front-component',
'mui-example.front-component',
'twenty-ui-example.front-component',
'sdk-context-example.front-component',
'form-events.front-component',
'keyboard-events.front-component',
'host-api-calls.front-component',
];
const resolveEntryPoints = (): Record<string, string> => {
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;
}
return entryPoints;
};
type BundleSizeEntry = {
name: string;
reactBytes: number;
preactBytes: number;
};
const collectBundleSizes = (): BundleSizeEntry[] =>
STORY_COMPONENTS.map((name) => {
const reactFile = path.join(exampleSourcesBuiltDir, `${name}.mjs`);
const preactFile = path.join(exampleSourcesBuiltPreactDir, `${name}.mjs`);
return {
name,
reactBytes: fs.existsSync(reactFile) ? fs.statSync(reactFile).size : 0,
preactBytes: fs.existsSync(preactFile) ? fs.statSync(preactFile).size : 0,
};
});
const buildSourceExamples = async (): Promise<void> => {
const entryPoints = resolveEntryPoints();
const tsconfigPath = path.join(dirname, '../../tsconfig.json');
const commonOptions: esbuild.BuildOptions = {
entryPoints,
bundle: true,
splitting: false,
format: 'esm',
outExtension: { '.js': '.mjs' },
tsconfig: tsconfigPath,
jsx: 'automatic',
sourcemap: true,
metafile: true,
logLevel: 'silent',
minify: true,
alias: storyAlias,
};
fs.mkdirSync(exampleSourcesBuiltDir, { recursive: true });
await esbuild.build({
...commonOptions,
outdir: exampleSourcesBuiltDir,
plugins: getFrontComponentBuildPlugins(),
});
console.log(
`Built ${STORY_COMPONENTS.length} React story components to ${exampleSourcesBuiltDir}`,
);
fs.mkdirSync(exampleSourcesBuiltPreactDir, { recursive: true });
await esbuild.build({
...commonOptions,
outdir: exampleSourcesBuiltPreactDir,
plugins: getFrontComponentBuildPlugins({ usePreact: true }),
});
console.log(
`Built ${STORY_COMPONENTS.length} Preact story components to ${exampleSourcesBuiltPreactDir}`,
);
const sizes = collectBundleSizes();
const manifestPath = path.join(exampleSourcesBuiltDir, 'bundle-sizes.json');
fs.writeFileSync(manifestPath, JSON.stringify(sizes, null, 2));
console.log(`Wrote bundle size manifest to ${manifestPath}`);
};
buildSourceExamples().catch((error) => {
console.error('Failed to build mock components:', error);
process.exit(1);
});