diff --git a/packages/create-twenty-app/src/constants/template/package.json b/packages/create-twenty-app/src/constants/template/package.json index dbcdf5482e..a41cd81902 100644 --- a/packages/create-twenty-app/src/constants/template/package.json +++ b/packages/create-twenty-app/src/constants/template/package.json @@ -16,15 +16,14 @@ "test": "vitest run", "test:watch": "vitest" }, - "dependencies": { - "twenty-client-sdk": "TO-BE-GENERATED" - }, + "dependencies": {}, "devDependencies": { "@types/node": "^24.7.2", "@types/react": "^19.0.0", "oxlint": "^0.16.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "twenty-client-sdk": "TO-BE-GENERATED", "twenty-sdk": "TO-BE-GENERATED", "typescript": "^5.9.3", "vite-tsconfig-paths": "^4.2.1", diff --git a/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts b/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts index 3882ec76df..3f15a09254 100644 --- a/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts +++ b/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts @@ -32,10 +32,9 @@ const TEMPLATE_PACKAGE_JSON = { version: '0.1.0', license: 'MIT', scripts: { twenty: 'twenty' }, - dependencies: { - 'twenty-client-sdk': '0.0.0', - }, + dependencies: {}, devDependencies: { + 'twenty-client-sdk': '0.0.0', 'twenty-sdk': '0.0.0', }, }; @@ -152,7 +151,7 @@ describe('copyBaseApplicationProject', () => { expect(packageJson.devDependencies['twenty-sdk']).toBe( createTwentyAppPackageJson.version, ); - expect(packageJson.dependencies['twenty-client-sdk']).toBe( + expect(packageJson.devDependencies['twenty-client-sdk']).toBe( createTwentyAppPackageJson.version, ); }); diff --git a/packages/create-twenty-app/src/utils/app-template.ts b/packages/create-twenty-app/src/utils/app-template.ts index caccd952cf..ca2801f97d 100644 --- a/packages/create-twenty-app/src/utils/app-template.ts +++ b/packages/create-twenty-app/src/utils/app-template.ts @@ -122,7 +122,7 @@ const updatePackageJson = async ({ packageJson.name = appName; packageJson.devDependencies['twenty-sdk'] = createTwentyAppPackageJson.version; - packageJson.dependencies['twenty-client-sdk'] = + packageJson.devDependencies['twenty-client-sdk'] = createTwentyAppPackageJson.version; await fs.writeFile( diff --git a/packages/twenty-apps/examples/hello-world/package.json b/packages/twenty-apps/examples/hello-world/package.json index 07c3fbc6e2..91c729e7d7 100644 --- a/packages/twenty-apps/examples/hello-world/package.json +++ b/packages/twenty-apps/examples/hello-world/package.json @@ -15,15 +15,14 @@ "test": "vitest run", "test:watch": "vitest" }, - "dependencies": { - "twenty-client-sdk": "2.13.0", - "twenty-sdk": "2.13.0" - }, + "dependencies": {}, "devDependencies": { "@types/node": "^24.7.2", "@types/react": "^18.2.0", "oxlint": "^0.16.0", "react": "^18.2.0", + "twenty-client-sdk": "2.13.0", + "twenty-sdk": "2.13.0", "typescript": "^5.9.3", "vite-tsconfig-paths": "^4.2.1", "vitest": "^4.0.0" diff --git a/packages/twenty-apps/examples/postcard/package.json b/packages/twenty-apps/examples/postcard/package.json index 1f1d99fc28..a19048c702 100644 --- a/packages/twenty-apps/examples/postcard/package.json +++ b/packages/twenty-apps/examples/postcard/package.json @@ -20,10 +20,7 @@ "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui" }, - "dependencies": { - "twenty-client-sdk": "2.13.0", - "twenty-sdk": "2.13.0" - }, + "dependencies": {}, "devDependencies": { "@playwright/test": "^1.60.0", "@types/node": "^24.7.2", @@ -32,6 +29,8 @@ "playwright": "^1.60.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "twenty-client-sdk": "2.13.0", + "twenty-sdk": "2.13.0", "typescript": "^5.9.3", "vite-tsconfig-paths": "^4.2.1", "vitest": "^4.0.0" diff --git a/packages/twenty-docs/developers/extend/apps/getting-started/project-structure.mdx b/packages/twenty-docs/developers/extend/apps/getting-started/project-structure.mdx index 0638365d70..c4587a3a3e 100644 --- a/packages/twenty-docs/developers/extend/apps/getting-started/project-structure.mdx +++ b/packages/twenty-docs/developers/extend/apps/getting-started/project-structure.mdx @@ -38,3 +38,24 @@ my-twenty-app/ **File organization is up to you.** The folders above are conventions — the SDK detects entities via AST analysis on `export default defineEntity(...)` calls regardless of where the file lives. + +## Dependencies + +Both Twenty SDK packages belong under `devDependencies`, not `dependencies`: + +```json filename="package.json" +{ + "dependencies": {}, + "devDependencies": { + "twenty-client-sdk": "^2.13.0", + "twenty-sdk": "^2.13.0" + } +} +``` + +- **`twenty-sdk`** ships the `twenty` CLI and the build/scaffolding tooling. It only runs at development and build time and is never imported by your published app's runtime. +- **`twenty-client-sdk`** _is_ imported by your app code (`CoreApiClient`, `MetadataApiClient`, `RestApiClient`), but Twenty provides it at runtime — logic functions get it from a generated SDK layer, and front components resolve it from server-served modules. Your installed copy is only used for typechecking and the deploy-time build, so it never needs to ship in the deployed bundle. + +Keeping either package under `dependencies` pulls it into the installed app's runtime bundle, where it is dead weight. `twenty build` emits a warning when either is still listed under `dependencies`. + +Add your app's own runtime dependencies (libraries your logic functions actually import at runtime) under `dependencies` as usual. diff --git a/packages/twenty-docs/developers/extend/apps/getting-started/troubleshooting.mdx b/packages/twenty-docs/developers/extend/apps/getting-started/troubleshooting.mdx index 88f244ab4e..09cdeb43b3 100644 --- a/packages/twenty-docs/developers/extend/apps/getting-started/troubleshooting.mdx +++ b/packages/twenty-docs/developers/extend/apps/getting-started/troubleshooting.mdx @@ -9,5 +9,6 @@ icon: "wrench" - **Yarn 4 missing** — Run `corepack enable`. - **Dependencies broken** — `rm -rf node_modules && yarn install`. - **`twenty-sdk` errors after upgrading to v2.8.0** — It moved from `dependencies` to `devDependencies` in v2.8.0. See [Project Structure → Dependencies](/developers/extend/apps/getting-started/project-structure#dependencies). +- **`twenty build` warns about `twenty-client-sdk` under `dependencies`** — It is provided at runtime by Twenty, so it should be moved to `devDependencies` alongside `twenty-sdk`. See [Project Structure → Dependencies](/developers/extend/apps/getting-started/project-structure#dependencies). Stuck? Ask on the [Twenty Discord](https://discord.com/channels/1130383047699738754/1130386664812982322). diff --git a/packages/twenty-sdk/CHANGELOG.md b/packages/twenty-sdk/CHANGELOG.md index 4fff109564..445984e6a4 100644 --- a/packages/twenty-sdk/CHANGELOG.md +++ b/packages/twenty-sdk/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to the [Twenty SDK](https://www.npmjs.com/package/twenty-sdk The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- **`twenty-client-sdk` should now be a dev dependency too.** Although app code imports it (`CoreApiClient`, `MetadataApiClient`, `RestApiClient`), Twenty provides it at runtime — logic functions get it from a generated SDK layer and front components resolve it from server-served modules — so the installed copy is only needed for typechecking and the deploy-time build. Newly scaffolded apps now place it under `devDependencies`. Moving it is recommended (not required: the server already strips it from the deployed runtime), and keeps the installed app leaner: + + ```diff + "dependencies": { + - "twenty-client-sdk": "^2.13.0" + }, + "devDependencies": { + + "twenty-client-sdk": "^2.13.0" + } + ``` + + `twenty build` now also emits a warning when `twenty-client-sdk` is still listed under `dependencies`. + ## [2.8.0] ### Breaking Changes diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/validate-package-json-dependencies.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/validate-package-json-dependencies.spec.ts index 666db22dd4..889c4a049c 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/validate-package-json-dependencies.spec.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/validate-package-json-dependencies.spec.ts @@ -29,7 +29,7 @@ describe('validatePackageJsonDependencies', () => { it('should warn when twenty-sdk is listed under dependencies', async () => { appPath = await writeTempPackageJson({ - dependencies: { 'twenty-sdk': '^2.8.0', 'twenty-client-sdk': '^2.8.0' }, + dependencies: { 'twenty-sdk': '^2.8.0' }, }); const warnings = await validatePackageJsonDependencies(appPath); @@ -39,10 +39,40 @@ describe('validatePackageJsonDependencies', () => { expect(warnings[0]).toContain('devDependencies'); }); - it('should not warn when twenty-sdk is listed under devDependencies', async () => { + it('should warn when twenty-client-sdk is listed under dependencies', async () => { appPath = await writeTempPackageJson({ dependencies: { 'twenty-client-sdk': '^2.8.0' }, - devDependencies: { 'twenty-sdk': '^2.8.0' }, + }); + + const warnings = await validatePackageJsonDependencies(appPath); + + expect(warnings).toHaveLength(1); + expect(warnings[0]).toContain('twenty-client-sdk'); + expect(warnings[0]).toContain('devDependencies'); + }); + + it('should warn for both SDK packages when both are listed under dependencies', async () => { + appPath = await writeTempPackageJson({ + dependencies: { 'twenty-sdk': '^2.8.0', 'twenty-client-sdk': '^2.8.0' }, + }); + + const warnings = await validatePackageJsonDependencies(appPath); + + expect(warnings).toHaveLength(2); + expect(warnings.some((warning) => warning.includes('twenty-sdk'))).toBe( + true, + ); + expect( + warnings.some((warning) => warning.includes('twenty-client-sdk')), + ).toBe(true); + }); + + it('should not warn when both SDK packages are listed under devDependencies', async () => { + appPath = await writeTempPackageJson({ + devDependencies: { + 'twenty-sdk': '^2.8.0', + 'twenty-client-sdk': '^2.8.0', + }, }); const warnings = await validatePackageJsonDependencies(appPath); @@ -50,9 +80,9 @@ describe('validatePackageJsonDependencies', () => { expect(warnings).toEqual([]); }); - it('should not warn when twenty-sdk is absent from dependencies', async () => { + it('should not warn when the SDK packages are absent from dependencies', async () => { appPath = await writeTempPackageJson({ - dependencies: { 'twenty-client-sdk': '^2.8.0' }, + dependencies: { 'some-other-package': '^1.0.0' }, }); const warnings = await validatePackageJsonDependencies(appPath); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/validate-package-json-dependencies.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/validate-package-json-dependencies.ts index f819d1ecf4..ffa981cafe 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/validate-package-json-dependencies.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/validate-package-json-dependencies.ts @@ -6,9 +6,21 @@ type PackageJsonDependencies = { dependencies?: Record; }; -// twenty-sdk ships the CLI and build/scaffolding tooling used only at dev and -// build time — it is never imported by the published app's runtime. Keeping it -// in "dependencies" ships dead weight, so it must live in "devDependencies". +// Neither Twenty SDK package needs to be resolved by the published app at +// runtime, so both must live in "devDependencies": +// - twenty-sdk ships the CLI and build/scaffolding tooling used only at dev and +// build time, and is never imported by the published app's runtime. +// - twenty-client-sdk is imported by app code but is provided at runtime by +// Twenty's injected SDK (Lambda SDK layer / server-served modules), so the +// app's installed copy is only needed for typecheck/build. +// Keeping either under "dependencies" pulls it into the Lambda deps layer. +const BUILD_TIME_DEPENDENCY_WARNINGS: Record = { + 'twenty-sdk': + '"twenty-sdk" is listed under "dependencies" in package.json. It is a build-time only tool and should be moved to "devDependencies".', + 'twenty-client-sdk': + '"twenty-client-sdk" is listed under "dependencies" in package.json. It is provided at runtime by Twenty\'s injected SDK and should be moved to "devDependencies".', +}; + export const validatePackageJsonDependencies = async ( appPath: string, ): Promise => { @@ -20,11 +32,9 @@ export const validatePackageJsonDependencies = async ( const packageJson = await readJson(packageJsonPath); - if (isDefined(packageJson.dependencies?.['twenty-sdk'])) { - return [ - '"twenty-sdk" is listed under "dependencies" in package.json. It is a build-time only tool and should be moved to "devDependencies".', - ]; - } - - return []; + return Object.entries(BUILD_TIME_DEPENDENCY_WARNINGS) + .filter(([packageName]) => + isDefined(packageJson.dependencies?.[packageName]), + ) + .map(([, warning]) => warning); }; diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service.ts index 130de5f27d..fbdbfb9bc6 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service.ts @@ -304,6 +304,11 @@ export class LogicFunctionResourceService { ); } + // twenty-sdk is build-time only and twenty-client-sdk is injected at runtime + // by Twenty (Lambda SDK layer / server-served modules), so neither needs to + // be resolved by the Lambda yarn install. Apps should already declare them as + // devDependencies (skipped by `yarn workspaces focus --production`); this is a + // safety net for apps that still list them under "dependencies". private async removeBuildTimeSdkFromDependencies( packageJsonPath: string, ): Promise { @@ -312,13 +317,24 @@ export class LogicFunctionResourceService { ) as { dependencies?: Record }; const dependencies = packageJson.dependencies; - const sdkVersionRange = dependencies?.['twenty-sdk']; - if (!isDefined(dependencies) || !isDefined(sdkVersionRange)) { + if (!isDefined(dependencies)) { return; } - delete dependencies['twenty-sdk']; + const packagesToRemove = ['twenty-sdk', 'twenty-client-sdk']; + + const packagesToRemoveFromDependencies = packagesToRemove.filter( + (packageName) => isDefined(dependencies[packageName]), + ); + + if (packagesToRemoveFromDependencies.length === 0) { + return; + } + + for (const packageName of packagesToRemoveFromDependencies) { + delete dependencies[packageName]; + } await fs.writeFile( packageJsonPath,