Skip to content

Commit 0bb3660

Browse files
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 |
1 parent 2ccc293 commit 0bb3660

36 files changed

Lines changed: 586 additions & 128 deletions

File tree

packages/create-twenty-app/src/constants/template/.oxlintrc.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,23 @@
1515
}
1616
],
1717
"typescript/no-explicit-any": "off"
18-
}
18+
},
19+
"overrides": [
20+
{
21+
"files": ["**/*.logic-function.ts", "**/logic-functions/**/*.ts"],
22+
"rules": {
23+
"no-restricted-imports": [
24+
"error",
25+
{
26+
"patterns": [
27+
{
28+
"group": ["twenty-shared", "twenty-shared/*"],
29+
"message": "Logic functions must not import from twenty-shared directly. Import runtime types and helpers from `twenty-sdk/logic-function` instead so the logic-function bundle stays minimal."
30+
}
31+
]
32+
}
33+
]
34+
}
35+
}
36+
]
1937
}

packages/twenty-apps/community/github-connector/.oxlintrc.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,23 @@
1515
}
1616
],
1717
"typescript/no-explicit-any": "off"
18-
}
18+
},
19+
"overrides": [
20+
{
21+
"files": ["**/*.logic-function.ts", "**/logic-functions/**/*.ts"],
22+
"rules": {
23+
"no-restricted-imports": [
24+
"error",
25+
{
26+
"patterns": [
27+
{
28+
"group": ["twenty-shared", "twenty-shared/*"],
29+
"message": "Logic functions must not import from twenty-shared directly. Import runtime types and helpers from `twenty-sdk/logic-function` instead so the logic-function bundle stays minimal."
30+
}
31+
]
32+
}
33+
]
34+
}
35+
}
36+
]
1937
}

packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/contributor-stats.logic-function.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
1+
import { defineLogicFunction } from 'twenty-sdk/define';
2+
import { type RoutePayload } from 'twenty-sdk/logic-function';
23
import { getClient } from 'src/modules/shared/twenty-client';
34

45
export type StatsPeriod = 'week' | 'month' | '3months' | 'year';
@@ -56,7 +57,11 @@ const PERIOD_CONFIG: Record<
5657
{ granularity: Granularity; rangeMs: number; bucketCount: number }
5758
> = {
5859
week: { granularity: 'day', rangeMs: 7 * 24 * 3600 * 1000, bucketCount: 7 },
59-
month: { granularity: 'day', rangeMs: 30 * 24 * 3600 * 1000, bucketCount: 30 },
60+
month: {
61+
granularity: 'day',
62+
rangeMs: 30 * 24 * 3600 * 1000,
63+
bucketCount: 30,
64+
},
6065
'3months': {
6166
granularity: 'week',
6267
rangeMs: 13 * 7 * 24 * 3600 * 1000,
@@ -98,7 +103,10 @@ const bucketStartFor = (d: Date, granularity: Granularity): Date => {
98103
};
99104

100105
const formatBucketLabel = (start: Date, granularity: Granularity): string => {
101-
const month = start.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' });
106+
const month = start.toLocaleString('en-US', {
107+
month: 'short',
108+
timeZone: 'UTC',
109+
});
102110
if (granularity === 'month') {
103111
return `${month} ${String(start.getUTCFullYear()).slice(2)}`;
104112
}

packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/count-contributors.logic-function.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
1+
import { defineLogicFunction } from 'twenty-sdk/define';
2+
import { type RoutePayload } from 'twenty-sdk/logic-function';
23
import { countAcrossRepos } from 'src/modules/github/connector/count-across-repos';
34
import { countContributors } from 'src/modules/github/contributor/graphql/github/count-contributors';
45

@@ -7,11 +8,7 @@ type CountContributorsPayload = {
78
};
89

910
const handler = async (event: RoutePayload<CountContributorsPayload>) =>
10-
countAcrossRepos(
11-
event.body?.repos,
12-
countContributors,
13-
'count-contributors',
14-
);
11+
countAcrossRepos(event.body?.repos, countContributors, 'count-contributors');
1512

1613
export default defineLogicFunction({
1714
universalIdentifier: 'fe0a6f00-0d63-4cb9-9b3c-1d8186181830',

packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/fetch-contributors.logic-function.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
1+
import { defineLogicFunction } from 'twenty-sdk/define';
2+
import { type RoutePayload } from 'twenty-sdk/logic-function';
23
import {
34
fetchContributors,
45
type GqlContributor,
@@ -41,7 +42,11 @@ const handler = async (event: RoutePayload<FetchContributorsPayload>) => {
4142
name: c.login,
4243
githubId: c.databaseId ?? 0,
4344
avatarUrl: c.avatarUrl
44-
? { primaryLinkLabel: c.login, primaryLinkUrl: c.avatarUrl, secondaryLinks: null }
45+
? {
46+
primaryLinkLabel: c.login,
47+
primaryLinkUrl: c.avatarUrl,
48+
secondaryLinks: null,
49+
}
4550
: null,
4651
}));
4752

packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/search-contributors.logic-function.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
1+
import { defineLogicFunction } from 'twenty-sdk/define';
2+
import { type RoutePayload } from 'twenty-sdk/logic-function';
23
import {
34
searchContributors,
45
type ContributorSearchResult,

packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/top-contributors.logic-function.ts

Lines changed: 76 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
1+
import { defineLogicFunction } from 'twenty-sdk/define';
2+
import { type RoutePayload } from 'twenty-sdk/logic-function';
23
import { isBotLogin } from 'src/modules/github/contributor/utils/is-bot-login';
34
import { getClient } from 'src/modules/shared/twenty-client';
45

@@ -107,7 +108,10 @@ const tally = (
107108
}
108109
}
109110
return Array.from(counts.values())
110-
.sort((a, b) => b.count - a.count || (a.ghLogin ?? '').localeCompare(b.ghLogin ?? ''))
111+
.sort(
112+
(a, b) =>
113+
b.count - a.count || (a.ghLogin ?? '').localeCompare(b.ghLogin ?? ''),
114+
)
111115
.slice(0, limit);
112116
};
113117

@@ -148,78 +152,82 @@ const handler = async (
148152
const client = getClient();
149153

150154
const authoredResult: { items: PrNode[]; truncated: boolean } =
151-
kind === 'reviewers' ? { items: [], truncated: false } : await paginateUntil<PrNode>(
152-
async (cursor) => {
153-
const res = await client.query({
154-
pullRequests: {
155-
__args: {
156-
orderBy: [{ githubCreatedAt: 'DescNullsLast' }],
157-
first: PAGE_SIZE,
158-
after: cursor,
159-
},
160-
edges: {
161-
node: {
162-
githubCreatedAt: true,
163-
author: {
164-
id: true,
165-
name: true,
166-
ghLogin: true,
167-
avatarUrl: { primaryLinkUrl: true },
155+
kind === 'reviewers'
156+
? { items: [], truncated: false }
157+
: await paginateUntil<PrNode>(
158+
async (cursor) => {
159+
const res = await client.query({
160+
pullRequests: {
161+
__args: {
162+
orderBy: [{ githubCreatedAt: 'DescNullsLast' }],
163+
first: PAGE_SIZE,
164+
after: cursor,
165+
},
166+
edges: {
167+
node: {
168+
githubCreatedAt: true,
169+
author: {
170+
id: true,
171+
name: true,
172+
ghLogin: true,
173+
avatarUrl: { primaryLinkUrl: true },
174+
},
175+
},
176+
},
177+
pageInfo: { hasNextPage: true, endCursor: true },
168178
},
169-
},
179+
});
180+
return (
181+
(res.pullRequests as Connection<PrNode>) ?? {
182+
edges: [],
183+
pageInfo: { hasNextPage: false, endCursor: null },
184+
}
185+
);
170186
},
171-
pageInfo: { hasNextPage: true, endCursor: true },
172-
},
173-
});
174-
return (
175-
(res.pullRequests as Connection<PrNode>) ?? {
176-
edges: [],
177-
pageInfo: { hasNextPage: false, endCursor: null },
178-
}
179-
);
180-
},
181-
(n) => {
182-
if (!n.githubCreatedAt) return false;
183-
return new Date(n.githubCreatedAt).getTime() < sinceMs;
184-
},
185-
);
187+
(n) => {
188+
if (!n.githubCreatedAt) return false;
189+
return new Date(n.githubCreatedAt).getTime() < sinceMs;
190+
},
191+
);
186192

187193
const reviewedResult: { items: ReviewNode[]; truncated: boolean } =
188-
kind === 'authors' ? { items: [], truncated: false } : await paginateUntil<ReviewNode>(
189-
async (cursor) => {
190-
const res = await client.query({
191-
pullRequestReviews: {
192-
__args: {
193-
orderBy: [{ firstSubmittedAt: 'DescNullsLast' }],
194-
first: PAGE_SIZE,
195-
after: cursor,
196-
},
197-
edges: {
198-
node: {
199-
firstSubmittedAt: true,
200-
reviewer: {
201-
id: true,
202-
name: true,
203-
ghLogin: true,
204-
avatarUrl: { primaryLinkUrl: true },
194+
kind === 'authors'
195+
? { items: [], truncated: false }
196+
: await paginateUntil<ReviewNode>(
197+
async (cursor) => {
198+
const res = await client.query({
199+
pullRequestReviews: {
200+
__args: {
201+
orderBy: [{ firstSubmittedAt: 'DescNullsLast' }],
202+
first: PAGE_SIZE,
203+
after: cursor,
204+
},
205+
edges: {
206+
node: {
207+
firstSubmittedAt: true,
208+
reviewer: {
209+
id: true,
210+
name: true,
211+
ghLogin: true,
212+
avatarUrl: { primaryLinkUrl: true },
213+
},
214+
},
215+
},
216+
pageInfo: { hasNextPage: true, endCursor: true },
205217
},
206-
},
218+
});
219+
return (
220+
(res.pullRequestReviews as Connection<ReviewNode>) ?? {
221+
edges: [],
222+
pageInfo: { hasNextPage: false, endCursor: null },
223+
}
224+
);
207225
},
208-
pageInfo: { hasNextPage: true, endCursor: true },
209-
},
210-
});
211-
return (
212-
(res.pullRequestReviews as Connection<ReviewNode>) ?? {
213-
edges: [],
214-
pageInfo: { hasNextPage: false, endCursor: null },
215-
}
216-
);
217-
},
218-
(n) => {
219-
if (!n.firstSubmittedAt) return false;
220-
return new Date(n.firstSubmittedAt).getTime() < sinceMs;
221-
},
222-
);
226+
(n) => {
227+
if (!n.firstSubmittedAt) return false;
228+
return new Date(n.firstSubmittedAt).getTime() < sinceMs;
229+
},
230+
);
223231

224232
const topAuthors = tally(
225233
authoredResult.items.map((pr) => ({ contributor: pr.author })),

packages/twenty-apps/community/github-connector/src/modules/github/issue/logic-functions/count-issues.logic-function.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
1+
import { defineLogicFunction } from 'twenty-sdk/define';
2+
import { type RoutePayload } from 'twenty-sdk/logic-function';
23
import { countAcrossRepos } from 'src/modules/github/connector/count-across-repos';
34
import { countIssues } from 'src/modules/github/issue/graphql/github/count-issues';
45

packages/twenty-apps/community/github-connector/src/modules/github/issue/logic-functions/fetch-issues.logic-function.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
1+
import { defineLogicFunction } from 'twenty-sdk/define';
2+
import { type RoutePayload } from 'twenty-sdk/logic-function';
23
import {
34
fetchIssues,
45
type GqlIssue,
@@ -65,9 +66,8 @@ const handler = async (event: RoutePayload<FetchIssuesPayload>) => {
6566
authorId: issue.author ? (idByLogin.get(issue.author.login) ?? null) : null,
6667
}));
6768

68-
await timed(
69-
`fetch-issues:upsertIssues ${tag} (${issueData.length})`,
70-
() => batchUpsertIssues(issueData),
69+
await timed(`fetch-issues:upsertIssues ${tag} (${issueData.length})`, () =>
70+
batchUpsertIssues(issueData),
7171
);
7272

7373
const totalMs = Date.now() - handlerStart;

packages/twenty-apps/community/github-connector/src/modules/github/logic-functions/handle-webhook.logic-function.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
1+
import { defineLogicFunction } from 'twenty-sdk/define';
2+
import { type RoutePayload } from 'twenty-sdk/logic-function';
23
import type { GitHubWebhookPayload } from 'src/modules/github/connector/webhook-payload';
34
import type { ProjectV2Item } from 'src/modules/github/project-item/types/project-v2-item';
45
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) {
4344

4445
const idByLogin = await upsertContributorsByLogin([pr.user, pr.merged_by]);
4546
const authorId = idByLogin.get(pr.user.login) ?? null;
46-
const mergerId = pr.merged_by ? (idByLogin.get(pr.merged_by.login) ?? null) : null;
47+
const mergerId = pr.merged_by
48+
? (idByLogin.get(pr.merged_by.login) ?? null)
49+
: null;
4750

4851
const canonical = pullRequestFromWebhook(pr, repository.full_name);
4952

@@ -81,7 +84,9 @@ async function handlePullRequestReviewEvent(payload: GitHubWebhookPayload) {
8184
{
8285
...prCanonical,
8386
authorId: idByLogin.get(pr.user.login) ?? null,
84-
mergerId: pr.merged_by ? (idByLogin.get(pr.merged_by.login) ?? null) : null,
87+
mergerId: pr.merged_by
88+
? (idByLogin.get(pr.merged_by.login) ?? null)
89+
: null,
8590
},
8691
]);
8792

@@ -175,7 +180,8 @@ async function handleProjectV2ItemEvent(
175180
return { skipped: true, reason: 'delete not implemented' };
176181
}
177182

178-
const node = testProjectItem ?? (await fetchProjectItemByNodeId(item.node_id));
183+
const node =
184+
testProjectItem ?? (await fetchProjectItemByNodeId(item.node_id));
179185
if (!node) {
180186
return { skipped: true, reason: 'project item not found on GitHub' };
181187
}

0 commit comments

Comments
 (0)