chore(twenty-sdk): shrink logic-function bundles via stubbing (#20033)

## Summary

Logic-function bundles produced by the SDK CLI were ~1.2 MB each (source
maps ~3.1 MB) because esbuild was inlining `twenty-sdk/define` and its
transitive dependencies (zod + locales, twenty-shared, etc.). Those
`define*` factories are pure build-time metadata used only by the
manifest extractor — the Lambda runtime only ever invokes
`default.config.handler`, so the factories are dead weight at runtime.

This PR shrinks the bundles to ~9.5 KB each (~99% reduction) without
changing runtime behaviour.

## What changes

- **Stub `twenty-sdk/define` at user-app build time.** New esbuild
plugin
(`packages/twenty-sdk/src/cli/utilities/build/common/plugins/stub-twenty-sdk-define.plugin.ts`)
intercepts every import of `twenty-sdk/define` during user-app builds
and replaces it with a tiny virtual module:
- Factory functions (`defineLogicFunction`,
`definePostInstallLogicFunction`, …) become `(config) => ({ success:
true, config, errors: [] })`.
  - Enums and helpers become `Proxy`-based no-ops.
- Wired into both the one-shot build (`build-application.ts`) and the
watcher (`esbuild-watcher.ts`), for logic functions and front
components.
- **New runtime barrel `twenty-sdk/logic-function`.** Re-exports only
the types logic-function authors need (`InstallPayload`, `RoutePayload`,
`CronPayload`, `DatabaseEventPayload`, `LogicFunctionConfig`,
`InputJsonSchema`, …). Compiled `.mjs` is 36 bytes. Wired into Vite,
Rollup `.d.ts` bundling, `package.json#exports`, and `typesVersions`.
- **Lint enforcement.** Added an oxlint `no-restricted-imports` rule
that forbids `twenty-shared` / `twenty-shared/*` imports from
`**/*.logic-function.ts` and `**/logic-functions/**/*.ts`, with a help
message pointing at the new barrel. Applied to the `create-twenty-app`
template and to `github-connector`, `hello-world`, `postcard`.
- **Migrated existing sources.** All logic-function files across
`community/{github-connector, apollo-enrich}`, `examples/{hello-world,
postcard}`, and `internal/{twenty-for-twenty, self-hosting, exa}` now
import types from `twenty-sdk/logic-function` instead of
`twenty-sdk/define` or `twenty-shared/*`. Renamed leftover
`InstallLogicFunctionPayload` references to `InstallPayload`.

## Why this is safe

- `define*` exports from `twenty-sdk/define` are metadata factories
whose call expressions are statically inspected by the manifest
extractor (`manifest-extract-config.ts`). They're never evaluated at
runtime — the Lambda executor only walks `default.config.handler`
(`logic-function-drivers/constants/executor/index.mjs`).
- The stub keeps the same call shape (`{ success, config, errors }`), so
any logic-function module that re-exports
`defineX(config).config.handler` still resolves to the user's handler at
runtime.
- Front-component bundles are unaffected by the stub because the
pre-existing JSX transform plugin
(`jsx-transform-to-remote-dom-worker-format-plugin.ts`) unwraps
`defineFrontComponent(...)` earlier in the pipeline. That's intentional
— front-component bloat is React/Preact, not in scope here.

## Measurements (github-connector)

| Asset | Before | After |
|---|---|---|
| `*.logic-function.mjs` | ~1.2 MB | ~9.5 KB |
| `*.logic-function.mjs.map` | ~3.1 MB | ~22 KB |
This commit is contained in:
Charles Bochet
2026-04-24 17:51:35 +02:00
committed by GitHub
parent 2ccc293f99
commit 0bb3660844
36 changed files with 586 additions and 128 deletions
@@ -1,4 +1,5 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { defineLogicFunction } from 'twenty-sdk/define';
import { type RoutePayload } from 'twenty-sdk/logic-function';
import { getClient } from 'src/modules/shared/twenty-client';
export type StatsPeriod = 'week' | 'month' | '3months' | 'year';
@@ -56,7 +57,11 @@ const PERIOD_CONFIG: Record<
{ granularity: Granularity; rangeMs: number; bucketCount: number }
> = {
week: { granularity: 'day', rangeMs: 7 * 24 * 3600 * 1000, bucketCount: 7 },
month: { granularity: 'day', rangeMs: 30 * 24 * 3600 * 1000, bucketCount: 30 },
month: {
granularity: 'day',
rangeMs: 30 * 24 * 3600 * 1000,
bucketCount: 30,
},
'3months': {
granularity: 'week',
rangeMs: 13 * 7 * 24 * 3600 * 1000,
@@ -98,7 +103,10 @@ const bucketStartFor = (d: Date, granularity: Granularity): Date => {
};
const formatBucketLabel = (start: Date, granularity: Granularity): string => {
const month = start.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' });
const month = start.toLocaleString('en-US', {
month: 'short',
timeZone: 'UTC',
});
if (granularity === 'month') {
return `${month} ${String(start.getUTCFullYear()).slice(2)}`;
}
@@ -1,4 +1,5 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { defineLogicFunction } from 'twenty-sdk/define';
import { type RoutePayload } from 'twenty-sdk/logic-function';
import { countAcrossRepos } from 'src/modules/github/connector/count-across-repos';
import { countContributors } from 'src/modules/github/contributor/graphql/github/count-contributors';
@@ -7,11 +8,7 @@ type CountContributorsPayload = {
};
const handler = async (event: RoutePayload<CountContributorsPayload>) =>
countAcrossRepos(
event.body?.repos,
countContributors,
'count-contributors',
);
countAcrossRepos(event.body?.repos, countContributors, 'count-contributors');
export default defineLogicFunction({
universalIdentifier: 'fe0a6f00-0d63-4cb9-9b3c-1d8186181830',
@@ -1,4 +1,5 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { defineLogicFunction } from 'twenty-sdk/define';
import { type RoutePayload } from 'twenty-sdk/logic-function';
import {
fetchContributors,
type GqlContributor,
@@ -41,7 +42,11 @@ const handler = async (event: RoutePayload<FetchContributorsPayload>) => {
name: c.login,
githubId: c.databaseId ?? 0,
avatarUrl: c.avatarUrl
? { primaryLinkLabel: c.login, primaryLinkUrl: c.avatarUrl, secondaryLinks: null }
? {
primaryLinkLabel: c.login,
primaryLinkUrl: c.avatarUrl,
secondaryLinks: null,
}
: null,
}));
@@ -1,4 +1,5 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { defineLogicFunction } from 'twenty-sdk/define';
import { type RoutePayload } from 'twenty-sdk/logic-function';
import {
searchContributors,
type ContributorSearchResult,
@@ -1,4 +1,5 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { defineLogicFunction } from 'twenty-sdk/define';
import { type RoutePayload } from 'twenty-sdk/logic-function';
import { isBotLogin } from 'src/modules/github/contributor/utils/is-bot-login';
import { getClient } from 'src/modules/shared/twenty-client';
@@ -107,7 +108,10 @@ const tally = (
}
}
return Array.from(counts.values())
.sort((a, b) => b.count - a.count || (a.ghLogin ?? '').localeCompare(b.ghLogin ?? ''))
.sort(
(a, b) =>
b.count - a.count || (a.ghLogin ?? '').localeCompare(b.ghLogin ?? ''),
)
.slice(0, limit);
};
@@ -148,78 +152,82 @@ const handler = async (
const client = getClient();
const authoredResult: { items: PrNode[]; truncated: boolean } =
kind === 'reviewers' ? { items: [], truncated: false } : await paginateUntil<PrNode>(
async (cursor) => {
const res = await client.query({
pullRequests: {
__args: {
orderBy: [{ githubCreatedAt: 'DescNullsLast' }],
first: PAGE_SIZE,
after: cursor,
},
edges: {
node: {
githubCreatedAt: true,
author: {
id: true,
name: true,
ghLogin: true,
avatarUrl: { primaryLinkUrl: true },
kind === 'reviewers'
? { items: [], truncated: false }
: await paginateUntil<PrNode>(
async (cursor) => {
const res = await client.query({
pullRequests: {
__args: {
orderBy: [{ githubCreatedAt: 'DescNullsLast' }],
first: PAGE_SIZE,
after: cursor,
},
edges: {
node: {
githubCreatedAt: true,
author: {
id: true,
name: true,
ghLogin: true,
avatarUrl: { primaryLinkUrl: true },
},
},
},
pageInfo: { hasNextPage: true, endCursor: true },
},
},
});
return (
(res.pullRequests as Connection<PrNode>) ?? {
edges: [],
pageInfo: { hasNextPage: false, endCursor: null },
}
);
},
pageInfo: { hasNextPage: true, endCursor: true },
},
});
return (
(res.pullRequests as Connection<PrNode>) ?? {
edges: [],
pageInfo: { hasNextPage: false, endCursor: null },
}
);
},
(n) => {
if (!n.githubCreatedAt) return false;
return new Date(n.githubCreatedAt).getTime() < sinceMs;
},
);
(n) => {
if (!n.githubCreatedAt) return false;
return new Date(n.githubCreatedAt).getTime() < sinceMs;
},
);
const reviewedResult: { items: ReviewNode[]; truncated: boolean } =
kind === 'authors' ? { items: [], truncated: false } : await paginateUntil<ReviewNode>(
async (cursor) => {
const res = await client.query({
pullRequestReviews: {
__args: {
orderBy: [{ firstSubmittedAt: 'DescNullsLast' }],
first: PAGE_SIZE,
after: cursor,
},
edges: {
node: {
firstSubmittedAt: true,
reviewer: {
id: true,
name: true,
ghLogin: true,
avatarUrl: { primaryLinkUrl: true },
kind === 'authors'
? { items: [], truncated: false }
: await paginateUntil<ReviewNode>(
async (cursor) => {
const res = await client.query({
pullRequestReviews: {
__args: {
orderBy: [{ firstSubmittedAt: 'DescNullsLast' }],
first: PAGE_SIZE,
after: cursor,
},
edges: {
node: {
firstSubmittedAt: true,
reviewer: {
id: true,
name: true,
ghLogin: true,
avatarUrl: { primaryLinkUrl: true },
},
},
},
pageInfo: { hasNextPage: true, endCursor: true },
},
},
});
return (
(res.pullRequestReviews as Connection<ReviewNode>) ?? {
edges: [],
pageInfo: { hasNextPage: false, endCursor: null },
}
);
},
pageInfo: { hasNextPage: true, endCursor: true },
},
});
return (
(res.pullRequestReviews as Connection<ReviewNode>) ?? {
edges: [],
pageInfo: { hasNextPage: false, endCursor: null },
}
);
},
(n) => {
if (!n.firstSubmittedAt) return false;
return new Date(n.firstSubmittedAt).getTime() < sinceMs;
},
);
(n) => {
if (!n.firstSubmittedAt) return false;
return new Date(n.firstSubmittedAt).getTime() < sinceMs;
},
);
const topAuthors = tally(
authoredResult.items.map((pr) => ({ contributor: pr.author })),
@@ -1,4 +1,5 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { defineLogicFunction } from 'twenty-sdk/define';
import { type RoutePayload } from 'twenty-sdk/logic-function';
import { countAcrossRepos } from 'src/modules/github/connector/count-across-repos';
import { countIssues } from 'src/modules/github/issue/graphql/github/count-issues';
@@ -1,4 +1,5 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { defineLogicFunction } from 'twenty-sdk/define';
import { type RoutePayload } from 'twenty-sdk/logic-function';
import {
fetchIssues,
type GqlIssue,
@@ -65,9 +66,8 @@ const handler = async (event: RoutePayload<FetchIssuesPayload>) => {
authorId: issue.author ? (idByLogin.get(issue.author.login) ?? null) : null,
}));
await timed(
`fetch-issues:upsertIssues ${tag} (${issueData.length})`,
() => batchUpsertIssues(issueData),
await timed(`fetch-issues:upsertIssues ${tag} (${issueData.length})`, () =>
batchUpsertIssues(issueData),
);
const totalMs = Date.now() - handlerStart;
@@ -1,4 +1,5 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { defineLogicFunction } from 'twenty-sdk/define';
import { type RoutePayload } from 'twenty-sdk/logic-function';
import type { GitHubWebhookPayload } from 'src/modules/github/connector/webhook-payload';
import type { ProjectV2Item } from 'src/modules/github/project-item/types/project-v2-item';
import { fetchProjectItemByNodeId } from 'src/modules/github/project-item/graphql/github/fetch-project-item-by-node-id';
@@ -43,7 +44,9 @@ async function handlePullRequestEvent(payload: GitHubWebhookPayload) {
const idByLogin = await upsertContributorsByLogin([pr.user, pr.merged_by]);
const authorId = idByLogin.get(pr.user.login) ?? null;
const mergerId = pr.merged_by ? (idByLogin.get(pr.merged_by.login) ?? null) : null;
const mergerId = pr.merged_by
? (idByLogin.get(pr.merged_by.login) ?? null)
: null;
const canonical = pullRequestFromWebhook(pr, repository.full_name);
@@ -81,7 +84,9 @@ async function handlePullRequestReviewEvent(payload: GitHubWebhookPayload) {
{
...prCanonical,
authorId: idByLogin.get(pr.user.login) ?? null,
mergerId: pr.merged_by ? (idByLogin.get(pr.merged_by.login) ?? null) : null,
mergerId: pr.merged_by
? (idByLogin.get(pr.merged_by.login) ?? null)
: null,
},
]);
@@ -175,7 +180,8 @@ async function handleProjectV2ItemEvent(
return { skipped: true, reason: 'delete not implemented' };
}
const node = testProjectItem ?? (await fetchProjectItemByNodeId(item.node_id));
const node =
testProjectItem ?? (await fetchProjectItemByNodeId(item.node_id));
if (!node) {
return { skipped: true, reason: 'project item not found on GitHub' };
}
@@ -1,4 +1,5 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { defineLogicFunction } from 'twenty-sdk/define';
import { type RoutePayload } from 'twenty-sdk/logic-function';
import {
getGithubProjects,
type GithubProject,
@@ -1,4 +1,5 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { defineLogicFunction } from 'twenty-sdk/define';
import { type RoutePayload } from 'twenty-sdk/logic-function';
import { fetchProjectItems } from 'src/modules/github/project-item/graphql/github/fetch-project-items';
import type { ProjectV2Item } from 'src/modules/github/project-item/types/project-v2-item';
import { batchUpsertProjectItems } from 'src/modules/github/project-item/graphql/mutations/batch-upsert';
@@ -1,4 +1,5 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { defineLogicFunction } from 'twenty-sdk/define';
import { type RoutePayload } from 'twenty-sdk/logic-function';
import { getClient } from 'src/modules/shared/twenty-client';
import { timed } from 'src/modules/shared/timing';
import { batchUpsertConsolidatedReviews } from 'src/modules/github/pull-request-review/graphql/mutations/batch-upsert';
@@ -128,9 +129,8 @@ const handler = async (_event: RoutePayload<unknown>) => {
let upsertedCount = 0;
if (rows.length > 0) {
const recs = await timed(
`recompute-reviews:upsert (${rows.length})`,
() => batchUpsertConsolidatedReviews(rows),
const recs = await timed(`recompute-reviews:upsert (${rows.length})`, () =>
batchUpsertConsolidatedReviews(rows),
);
upsertedCount = recs.length;
}
@@ -1,4 +1,5 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { defineLogicFunction } from 'twenty-sdk/define';
import { type RoutePayload } from 'twenty-sdk/logic-function';
import { countAcrossRepos } from 'src/modules/github/connector/count-across-repos';
import { countPullRequests } from 'src/modules/github/pull-request/graphql/github/count-pull-requests';
@@ -1,4 +1,5 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { defineLogicFunction } from 'twenty-sdk/define';
import { type RoutePayload } from 'twenty-sdk/logic-function';
import {
fetchPullRequests,
type GqlPullRequest,
@@ -51,7 +52,13 @@ const handler = async (event: RoutePayload<FetchPrsPayload>) => {
if (prs.length === 0) {
console.log(`[fetch-prs] empty page for ${tag}`);
return { prCount: 0, reviewCount: 0, totalCount, hasMore: false, endCursor: null };
return {
prCount: 0,
reviewCount: 0,
totalCount,
hasMore: false,
endCursor: null,
};
}
const allUsers = prs.flatMap((pr) => [