Files
twenty/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts
T
Paul Rastoin fdab89ae02 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. -->
2026-06-15 14:46:22 +00:00

269 lines
8.2 KiB
TypeScript

import { copyBaseApplicationProject } from '@/utils/app-template';
import * as fs from 'fs-extra';
import { tmpdir } from 'os';
import createTwentyAppPackageJson from 'package.json';
import { join } from 'path';
jest.mock('fs-extra', () => {
const actual = jest.requireActual('fs-extra');
return {
...actual,
copy: jest.fn().mockResolvedValue(undefined),
};
});
const UNIVERSAL_IDENTIFIERS_PATH = join(
'src',
'constants',
'universal-identifiers.ts',
);
const YARNRC_PATH = 'yarnrc.yml';
// Template content matching template/src/constants/universal-identifiers.ts
const TEMPLATE_UNIVERSAL_IDENTIFIERS = `export const APP_DISPLAY_NAME = 'DISPLAY-NAME-TO-BE-GENERATED';
export const APP_DESCRIPTION = 'DESCRIPTION-TO-BE-GENERATED';
export const APPLICATION_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
`;
// Template package.json matching template/package.json
const TEMPLATE_PACKAGE_JSON = {
name: 'template-app',
version: '0.1.0',
license: 'MIT',
scripts: { twenty: 'twenty' },
dependencies: {},
devDependencies: {
'twenty-client-sdk': '0.0.0',
'twenty-sdk': '0.0.0',
},
};
describe('copyBaseApplicationProject', () => {
let testAppDirectory: string;
beforeEach(async () => {
testAppDirectory = join(
tmpdir(),
`test-twenty-app-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
await fs.ensureDir(testAppDirectory);
// Seed the files that generateUniversalIdentifiers and updatePackageJson
// expect to find (since fs.copy is mocked and won't actually create them)
await fs.ensureDir(join(testAppDirectory, 'src', 'constants'));
await fs.writeFile(
join(testAppDirectory, UNIVERSAL_IDENTIFIERS_PATH),
TEMPLATE_UNIVERSAL_IDENTIFIERS,
);
await fs.writeJson(
join(testAppDirectory, 'package.json'),
TEMPLATE_PACKAGE_JSON,
);
jest.clearAllMocks();
});
afterEach(async () => {
if (testAppDirectory && (await fs.pathExists(testAppDirectory))) {
await fs.remove(testAppDirectory);
}
});
it('should call fs.copy to copy base application template', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
});
// Two fs.copy calls: (1) the template directory, (2) AGENTS.md → CLAUDE.md
expect(fs.copy).toHaveBeenCalledTimes(2);
expect(fs.copy).toHaveBeenCalledWith(
expect.stringContaining('template'),
testAppDirectory,
);
expect(fs.copy).toHaveBeenCalledWith(
join(testAppDirectory, 'AGENTS.md'),
join(testAppDirectory, 'CLAUDE.md'),
);
});
it('should replace placeholders in universal-identifiers.ts with real values', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
});
const content = await fs.readFile(
join(testAppDirectory, UNIVERSAL_IDENTIFIERS_PATH),
'utf8',
);
expect(content).toContain("APP_DISPLAY_NAME = 'My Test App'");
expect(content).toContain("APP_DESCRIPTION = 'A test application'");
expect(content).not.toContain('DISPLAY-NAME-TO-BE-GENERATED');
expect(content).not.toContain('DESCRIPTION-TO-BE-GENERATED');
expect(content).not.toContain('UUID-TO-BE-GENERATED');
// Both UUIDs should be valid v4 format
const uuidMatches = content.match(
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g,
);
expect(uuidMatches).toHaveLength(2);
});
it('should generate different UUIDs for each identifier', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
});
const content = await fs.readFile(
join(testAppDirectory, UNIVERSAL_IDENTIFIERS_PATH),
'utf8',
);
const uuidMatches = content.match(
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g,
);
expect(uuidMatches).toHaveLength(2);
expect(uuidMatches![0]).not.toBe(uuidMatches![1]);
});
it('should update package.json with app name and SDK versions', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
});
const packageJson = await fs.readJson(
join(testAppDirectory, 'package.json'),
);
expect(packageJson.name).toBe('my-test-app');
expect(packageJson.devDependencies['twenty-sdk']).toBe(
createTwentyAppPackageJson.version,
);
expect(packageJson.devDependencies['twenty-client-sdk']).toBe(
createTwentyAppPackageJson.version,
);
});
it('should create an empty public directory in the scaffolded project', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
});
const publicDirectoryPath = join(testAppDirectory, 'public');
expect(await fs.pathExists(publicDirectoryPath)).toBe(true);
const publicDirectoryStats = await fs.stat(publicDirectoryPath);
expect(publicDirectoryStats.isDirectory()).toBe(true);
const publicDirectoryContents = await fs.readdir(publicDirectoryPath);
expect(publicDirectoryContents).toHaveLength(0);
});
it('should rename yarnrc.yml to .yarnrc.yml in the scaffolded project', async () => {
await fs.writeFile(
join(testAppDirectory, YARNRC_PATH),
'nodeLinker: node-modules',
);
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
});
expect(await fs.pathExists(join(testAppDirectory, YARNRC_PATH))).toBe(
false,
);
expect(await fs.pathExists(join(testAppDirectory, '.yarnrc.yml'))).toBe(
true,
);
});
it('should handle empty description', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: '',
appDirectory: testAppDirectory,
});
const content = await fs.readFile(
join(testAppDirectory, UNIVERSAL_IDENTIFIERS_PATH),
'utf8',
);
expect(content).toContain("APP_DESCRIPTION = ''");
});
it('should generate unique UUIDs across different scaffolds', async () => {
const firstAppDir = join(testAppDirectory, 'app1');
await fs.ensureDir(join(firstAppDir, 'src', 'constants'));
await fs.writeFile(
join(firstAppDir, UNIVERSAL_IDENTIFIERS_PATH),
TEMPLATE_UNIVERSAL_IDENTIFIERS,
);
await fs.writeJson(
join(firstAppDir, 'package.json'),
TEMPLATE_PACKAGE_JSON,
);
await copyBaseApplicationProject({
appName: 'app-one',
appDisplayName: 'App One',
appDescription: 'First app',
appDirectory: firstAppDir,
});
const secondAppDir = join(testAppDirectory, 'app2');
await fs.ensureDir(join(secondAppDir, 'src', 'constants'));
await fs.writeFile(
join(secondAppDir, UNIVERSAL_IDENTIFIERS_PATH),
TEMPLATE_UNIVERSAL_IDENTIFIERS,
);
await fs.writeJson(
join(secondAppDir, 'package.json'),
TEMPLATE_PACKAGE_JSON,
);
await copyBaseApplicationProject({
appName: 'app-two',
appDisplayName: 'App Two',
appDescription: 'Second app',
appDirectory: secondAppDir,
});
const firstConstants = await fs.readFile(
join(firstAppDir, UNIVERSAL_IDENTIFIERS_PATH),
'utf8',
);
const secondConstants = await fs.readFile(
join(secondAppDir, UNIVERSAL_IDENTIFIERS_PATH),
'utf8',
);
const uuidRegex =
/APPLICATION_UNIVERSAL_IDENTIFIER = '([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/;
const firstUuid = firstConstants.match(uuidRegex)?.[1];
const secondUuid = secondConstants.match(uuidRegex)?.[1];
expect(firstUuid).toBeDefined();
expect(secondUuid).toBeDefined();
expect(firstUuid).not.toBe(secondUuid);
});
});