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:
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
+35
-5
@@ -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);
|
||||
|
||||
+20
-10
@@ -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);
|
||||
};
|
||||
|
||||
+19
-3
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user