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:
+11
-3
@@ -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)}`;
|
||||
}
|
||||
|
||||
+3
-6
@@ -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',
|
||||
|
||||
+7
-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 {
|
||||
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,
|
||||
}));
|
||||
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+76
-68
@@ -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 })),
|
||||
|
||||
+2
-1
@@ -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';
|
||||
|
||||
|
||||
+4
-4
@@ -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;
|
||||
|
||||
+10
-4
@@ -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' };
|
||||
}
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+2
-1
@@ -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';
|
||||
|
||||
+4
-4
@@ -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;
|
||||
}
|
||||
|
||||
+2
-1
@@ -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';
|
||||
|
||||
|
||||
+9
-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 {
|
||||
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) => [
|
||||
|
||||
Reference in New Issue
Block a user