Move twenty-client-sdk to dev dep (#21611)

# Introduction
The `twenty-client-sdk` is always provided and injected at runtime by
the twenty-server instance
Which mean that even if in your app locally you're using
twenty-client-sdk `1.0` installing this app on twenty instance `2.0`
will result in injecting another `twenty-client-sdk`

That's the expected behavior and tradeof

The twenty-app devdep should only be used to guide local devxp following
typesafety and so on

A user can still locally generated its own twenty-client-sdk and publish
it if necessary

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21611?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Paul Rastoin
2026-06-15 16:46:22 +02:00
committed by GitHub
parent 09694b2f3b
commit fdab89ae02
11 changed files with 125 additions and 34 deletions
@@ -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",
@@ -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,
);
});
@@ -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(
@@ -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"
@@ -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"
@@ -38,3 +38,24 @@ my-twenty-app/
<Note>
**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.
</Note>
## 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.
@@ -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).
+17
View File
@@ -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
@@ -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);
@@ -6,9 +6,21 @@ type PackageJsonDependencies = {
dependencies?: Record<string, string>;
};
// 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<string, string> = {
'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<string[]> => {
@@ -20,11 +32,9 @@ export const validatePackageJsonDependencies = async (
const packageJson = await readJson<PackageJsonDependencies>(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);
};
@@ -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<void> {
@@ -312,13 +317,24 @@ export class LogicFunctionResourceService {
) as { dependencies?: Record<string, string> };
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,