0bb3660844
## 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 |
138 lines
3.3 KiB
TypeScript
138 lines
3.3 KiB
TypeScript
import { defineLogicFunction } from 'twenty-sdk/define';
|
|
import { type RoutePayload } from 'twenty-sdk/logic-function';
|
|
import { CoreApiClient } from 'twenty-client-sdk/core';
|
|
import { type TelemetryEvent } from 'src/logic-functions/types/telemetry-event.type';
|
|
|
|
export const main = async (
|
|
params: RoutePayload<TelemetryEvent>,
|
|
): Promise<{
|
|
success: boolean;
|
|
message: string;
|
|
error?: string;
|
|
}> => {
|
|
try {
|
|
const {
|
|
action,
|
|
workspaceId,
|
|
userWorkspaceId,
|
|
userId,
|
|
userEmail,
|
|
userFirstName,
|
|
userLastName,
|
|
locale,
|
|
serverUrl,
|
|
serverId,
|
|
} = params.body || {};
|
|
|
|
if (action !== 'user_signup') {
|
|
return {
|
|
success: true,
|
|
message: `Event type '${action}' ignored`,
|
|
};
|
|
}
|
|
|
|
if (!userEmail) {
|
|
return {
|
|
success: true,
|
|
message: 'No email found in telemetry event',
|
|
};
|
|
}
|
|
|
|
if (
|
|
userEmail.toLowerCase().includes('example') ||
|
|
userEmail.toLowerCase().includes('test')
|
|
) {
|
|
return {
|
|
success: true,
|
|
message: `Email '${userEmail}' ignored (contains test/example data)`,
|
|
};
|
|
}
|
|
|
|
const client = new CoreApiClient();
|
|
|
|
let existingSelfHostingUserId: string | undefined = undefined;
|
|
try {
|
|
const { selfHostingUser: existingSelfHostingUser } = await client.query({
|
|
selfHostingUser: {
|
|
__args: {
|
|
filter: {
|
|
email: { primaryEmail: { eq: userEmail } },
|
|
},
|
|
},
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
existingSelfHostingUserId = existingSelfHostingUser?.id;
|
|
} catch {
|
|
//
|
|
}
|
|
|
|
if (existingSelfHostingUserId) {
|
|
await client.mutation({
|
|
updateSelfHostingUser: {
|
|
__args: {
|
|
id: existingSelfHostingUserId,
|
|
data: {
|
|
name: { firstName: userFirstName, lastName: userLastName },
|
|
email: { primaryEmail: userEmail, additionalEmails: null },
|
|
userWorkspaceId,
|
|
userId,
|
|
locale,
|
|
serverUrl,
|
|
serverId,
|
|
},
|
|
},
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
message: `Self hosting user ${existingSelfHostingUserId} updated`,
|
|
};
|
|
}
|
|
|
|
const { createSelfHostingUser } = await client.mutation({
|
|
createSelfHostingUser: {
|
|
__args: {
|
|
data: {
|
|
name: { firstName: userFirstName, lastName: userLastName },
|
|
email: { primaryEmail: userEmail, additionalEmails: null },
|
|
workspaceId,
|
|
userWorkspaceId,
|
|
userId,
|
|
locale,
|
|
serverUrl,
|
|
serverId,
|
|
},
|
|
},
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
message: `Self hosting user ${createSelfHostingUser?.id} created`,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
message: 'Failed to process telemetry event',
|
|
error: error instanceof Error ? error.message : String(error),
|
|
};
|
|
}
|
|
};
|
|
|
|
export default defineLogicFunction({
|
|
universalIdentifier: '10104201-622b-4a5e-9f27-8f2af19b2a3c',
|
|
name: 'telemetry-webhook',
|
|
timeoutSeconds: 10,
|
|
handler: main,
|
|
httpRouteTriggerSettings: {
|
|
path: '/webhook/telemetry',
|
|
httpMethod: 'POST',
|
|
isAuthRequired: false,
|
|
},
|
|
});
|