Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 4 additions & 17 deletions .cursor/rules/code-style.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,15 @@ const processUserData = (
```

## Comments

Only use comments if they are necessary to explain tricky or non-obvious code. This should be a rare occurrence.

```typescript
// ✅ Use short-form comments, NOT JSDoc blocks
// ✅ Explain business logic and non-obvious intentions (WHY, not WHAT)
// Apply 15% discount for premium users with orders > $100
const discount = isPremiumUser && orderTotal > 100 ? 0.15 : 0;

// TODO: Replace with proper authentication service
// TODO: Replace with proper authentication service
const isAuthenticated = localStorage.getItem('token') !== null;

// ✅ Multi-line comments use multiple // lines (NOT /** */ blocks)
Expand All @@ -97,21 +99,6 @@ const calculateTotalPrice = (basePrice: number): number => {
// Implementation
};

// ❌ AVOID obvious comments that just describe what code does
// Bad: Get all inline fields dynamically
const { inlineFieldMetadataItems } = useFieldListFieldMetadataItems({...});

// Bad: Define standard fields in display order
const standardFieldOrder = ['startsAt', 'endsAt', 'conferenceLink'];

// Bad: Split fields into standard and custom
const standardFields = standardFieldOrder.map(...)

// ✅ GOOD: Only comment if explaining non-obvious business logic
// Calendar events display standard fields first, then custom fields after participants
// to maintain consistency with the legacy UI behavior
const standardFields = standardFieldOrder.map(...)

// ❌ AVOID JSDoc blocks - use short comments instead
/**
* This style is NOT preferred in this codebase
Expand Down
13 changes: 13 additions & 0 deletions packages/create-twenty-app/src/utils/app-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,24 @@ const createDefaultFrontComponent = async ({
}) => {
const universalIdentifier = v4();

// Create assets directory and a sample logo
const assetsDir = join(appDirectory, 'assets');
await fs.ensureDir(assetsDir);

// Create a minimal 1x1 transparent PNG as a placeholder
const minimalPng = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
'base64',
);
await fs.writeFile(join(assetsDir, 'logo.png'), minimalPng);

const content = `import { defineFrontComponent } from 'twenty-sdk';
import logo from 'src/assets/logo.png';

export const HelloWorld = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<img src={logo} alt="Logo" style={{ width: '32px', height: '32px' }} />
<h1>Hello, World!</h1>
<p>This is your first front component.</p>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@
"universalIdentifier": "h0h1h2h3-h4h5-4000-8000-000000000001"
},
{
"assets": [
{
"builtAssetChecksum": "[checksum]",
"builtAssetPath": "[asset-path]",
"sourceAssetPath": "src/assets/test-logo.png"
}
],
"builtComponentPath": "front-components/src/components/test.front-component.mjs",
"builtComponentChecksum": "[checksum]",
"componentName": "TestComponent",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,15 @@ export const defineFrontComponentsTests = (appPath: string): void => {
'src/root.front-component.mjs.map',
]);
});

it('should have built assets in separate assets directory', async () => {
const assetsDir = join(appPath, '.twenty/output/assets');
const files = await fs.readdir(assetsDir);
const sortedFiles = files.map((f) => f.toString()).sort();

// Verify assets folder contains the expected asset (with hash in filename)
expect(sortedFiles.length).toBeGreaterThanOrEqual(1);
expect(sortedFiles.some((f) => f.includes('test-logo'))).toBe(true);
});
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

const { sources: _sources, ...sanitizedManifest } = manifest;

expect(normalizeManifestForComparison(sanitizedManifest)).toEqual(

Check failure on line 18 in packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts

View workflow job for this annotation

GitHub Actions / sdk-test (test:integration)

[twenty-sdk-integration] src/cli/__tests__/apps/rich-app/__integration__/app-build/app-build.integration.spec.ts > rich-app app:build > manifest > should build manifest matching expected JSON

AssertionError: expected { application: { …(6) }, …(6) } to deeply equal { application: { …(6) }, …(6) } - Expected + Received @@ -15,47 +15,41 @@ "universalIdentifier": "4ec0391d-18d5-411c-b2f3-266ddc1c3ef7", }, "frontComponents": [ { "assets": undefined, - "builtComponentChecksum": "[checksum]", + "builtComponentChecksum": null, "builtComponentPath": "front-components/src/root.front-component.mjs", "componentName": "RootComponent", "description": "A root-level front component", "name": "root-component", "sourceComponentPath": "src/root.front-component.tsx", "universalIdentifier": "a0a1a2a3-a4a5-4000-8000-000000000001", }, { "assets": undefined, - "builtComponentChecksum": "[checksum]", + "builtComponentChecksum": null, "builtComponentPath": "front-components/src/components/card.front-component.mjs", "componentName": "CardDisplay", "description": "A component using an external component file", "name": "card-component", "sourceComponentPath": "src/components/card.front-component.tsx", "universalIdentifier": "i0i1i2i3-i4i5-4000-8000-000000000001", }, { "assets": undefined, - "builtComponentChecksum": "[checksum]", + "builtComponentChecksum": null, "builtComponentPath": "front-components/src/components/greeting.front-component.mjs", "componentName": "GreetingComponent", "description": "A component that uses greeting utility", "name": "greeting-component", "sourceComponentPath": "src/components/greeting.front-component.tsx", "universalIdentifier": "h0h1h2h3-h4h5-4000-8000-000000000001", }, { - "assets": [ - { - "builtAssetChecksum": "[checksum]", - "builtAssetPath": "[asset-path]", - "sourceAssetPath": "src/assets/test-logo.png", - }, - ], - "builtComponentChecksum": "[checksum]", + "assets": undefined, + "builtComponentChecksum": null, "builtComponentPath": "front-components/src/components/test.front-component.mjs", "componentName": "TestComponent", "description": "A test front component", "name": "test-component", "sourceComponentPath": "src/components/test.front-component.tsx", ❯ src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts:18:65

Check failure on line 18 in packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts

View workflow job for this annotation

GitHub Actions / sdk-test (test:integration)

[twenty-sdk-integration] src/cli/__tests__/apps/rich-app/__integration__/app-build/app-build.integration.spec.ts > rich-app app:build > manifest > should build manifest matching expected JSON

AssertionError: expected { application: { …(6) }, …(6) } to deeply equal { application: { …(6) }, …(6) } - Expected + Received @@ -15,47 +15,41 @@ "universalIdentifier": "4ec0391d-18d5-411c-b2f3-266ddc1c3ef7", }, "frontComponents": [ { "assets": undefined, - "builtComponentChecksum": "[checksum]", + "builtComponentChecksum": null, "builtComponentPath": "front-components/src/root.front-component.mjs", "componentName": "RootComponent", "description": "A root-level front component", "name": "root-component", "sourceComponentPath": "src/root.front-component.tsx", "universalIdentifier": "a0a1a2a3-a4a5-4000-8000-000000000001", }, { "assets": undefined, - "builtComponentChecksum": "[checksum]", + "builtComponentChecksum": null, "builtComponentPath": "front-components/src/components/card.front-component.mjs", "componentName": "CardDisplay", "description": "A component using an external component file", "name": "card-component", "sourceComponentPath": "src/components/card.front-component.tsx", "universalIdentifier": "i0i1i2i3-i4i5-4000-8000-000000000001", }, { "assets": undefined, - "builtComponentChecksum": "[checksum]", + "builtComponentChecksum": null, "builtComponentPath": "front-components/src/components/greeting.front-component.mjs", "componentName": "GreetingComponent", "description": "A component that uses greeting utility", "name": "greeting-component", "sourceComponentPath": "src/components/greeting.front-component.tsx", "universalIdentifier": "h0h1h2h3-h4h5-4000-8000-000000000001", }, { - "assets": [ - { - "builtAssetChecksum": "[checksum]", - "builtAssetPath": "[asset-path]", - "sourceAssetPath": "src/assets/test-logo.png", - }, - ], - "builtComponentChecksum": "[checksum]", + "assets": undefined, + "builtComponentChecksum": null, "builtComponentPath": "front-components/src/components/test.front-component.mjs", "componentName": "TestComponent", "description": "A test front component", "name": "test-component", "sourceComponentPath": "src/components/test.front-component.tsx", ❯ src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts:18:65
normalizeManifestForComparison(expectedManifest),
);

Expand Down Expand Up @@ -48,5 +48,24 @@
expect(manifest?.roles).toHaveLength(2);
expect(manifest?.objectExtensions).toHaveLength(1);
});

it('should include assets for front components with static imports', async () => {
const manifest = await fs.readJson(manifestOutputPath);

// Find the test-component which imports a PNG asset
const testComponent = manifest.frontComponents?.find(
(component: { name: string }) => component.name === 'test-component',
);

expect(testComponent).toBeDefined();
expect(testComponent.assets).toBeDefined();

Check failure on line 61 in packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts

View workflow job for this annotation

GitHub Actions / sdk-test (test:integration)

[twenty-sdk-integration] src/cli/__tests__/apps/rich-app/__integration__/app-build/app-build.integration.spec.ts > rich-app app:build > manifest > should include assets for front components with static imports

AssertionError: expected undefined to be defined ❯ src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts:61:36

Check failure on line 61 in packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts

View workflow job for this annotation

GitHub Actions / sdk-test (test:integration)

[twenty-sdk-integration] src/cli/__tests__/apps/rich-app/__integration__/app-build/app-build.integration.spec.ts > rich-app app:build > manifest > should include assets for front components with static imports

AssertionError: expected undefined to be defined ❯ src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts:61:36
expect(testComponent.assets).toHaveLength(1);

const asset = testComponent.assets[0];
expect(asset.sourceAssetPath).toBe('src/assets/test-logo.png');
expect(asset.builtAssetPath).toMatch(/^assets\/.*\.png$/);
expect(asset.builtAssetChecksum).toBeDefined();
expect(typeof asset.builtAssetChecksum).toBe('string');
});
});
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { defineFrontComponent } from '@/application/front-components/define-front-component';
import testLogo from '../assets/test-logo.png';

export const TestComponent = () => {
return (
<div style={{ padding: '20px' }}>
<h1>Test Component</h1>
<img src={testLogo} alt="Test Logo" />
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type ApplicationManifest } from 'twenty-shared/application';

// Replace dynamic checksum values with a placeholder for consistent comparisons
// Replace dynamic checksum values and asset paths with placeholders for consistent comparisons
export const normalizeManifestForComparison = <
T extends Partial<ApplicationManifest>,
>(
Expand All @@ -16,5 +16,11 @@ export const normalizeManifestForComparison = <
builtComponentChecksum: component.builtComponentChecksum
? '[checksum]'
: null,
assets: component.assets?.map((asset) => ({
...asset,
builtAssetChecksum: asset.builtAssetChecksum ? '[checksum]' : null,
// Asset paths include a hash, so normalize them
builtAssetPath: asset.builtAssetPath ? '[asset-path]' : null,
})),
})),
});
62 changes: 58 additions & 4 deletions packages/twenty-sdk/src/cli/commands/app/app-build.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { type ApiResponse } from '@/cli/utilities/api/types/api-response.types';
import { AssetsWatcher, type BuiltAsset } from '@/cli/utilities/build/assets/assets-watcher';
import { createLogger } from '@/cli/utilities/build/common/logger';
import { FrontComponentsWatcher } from '@/cli/utilities/build/front-components/front-component-watcher';
import { FunctionsWatcher } from '@/cli/utilities/build/functions/function-watcher';
import {
runManifestBuild,
updateManifestAssets,
updateManifestChecksum,
type ManifestBuildResult,
} from '@/cli/utilities/build/manifest/manifest-build';
Expand All @@ -18,6 +20,7 @@ export type AppBuildOptions = {
};

export class AppBuildCommand {
private assetsBuilder: AssetsWatcher | null = null;
private functionsBuilder: FunctionsWatcher | null = null;
private frontComponentsBuilder: FrontComponentsWatcher | null = null;

Expand Down Expand Up @@ -48,6 +51,8 @@ export class AppBuildCommand {
return null;
}

// Build assets first so they're available for functions and front components
await this.buildAssets();
await this.buildFunctions(buildResult);
await this.buildFrontComponents(buildResult);
await writeManifestToOutput(this.appPath, buildResult.manifest);
Expand All @@ -56,19 +61,41 @@ export class AppBuildCommand {
return buildResult;
}

private async buildAssets(): Promise<void> {
this.assetsBuilder = new AssetsWatcher({
appPath: this.appPath,
watch: false,
});

await this.assetsBuilder.start();
}

private async buildFunctions(buildResult: ManifestBuildResult): Promise<void> {
const assetsWatcher = this.assetsBuilder;

this.functionsBuilder = new FunctionsWatcher({
appPath: this.appPath,
sourcePaths: buildResult.filePaths.functions,
watch: false,
onFileBuilt: (builtPath, checksum) => {
onFileBuilt: (builtPath: string, checksum: string, sourceAssetPaths: string[]) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

record's arg

if (buildResult.manifest) {
const updatedManifest = updateManifestChecksum({
let updatedManifest = updateManifestChecksum({
manifest: buildResult.manifest,
entityType: 'function',
builtPath,
checksum,
});

if (updatedManifest && sourceAssetPaths.length > 0 && assetsWatcher) {
const builtAssets = this.resolveBuiltAssets(assetsWatcher, sourceAssetPaths);
updatedManifest = updateManifestAssets({
manifest: updatedManifest,
entityType: 'function',
builtPath,
assets: builtAssets,
});
}

if (updatedManifest) {
buildResult.manifest = updatedManifest;
}
Expand All @@ -80,18 +107,31 @@ export class AppBuildCommand {
}

private async buildFrontComponents(buildResult: ManifestBuildResult): Promise<void> {
const assetsWatcher = this.assetsBuilder;

this.frontComponentsBuilder = new FrontComponentsWatcher({
appPath: this.appPath,
sourcePaths: buildResult.filePaths.frontComponents,
watch: false,
onFileBuilt: (builtPath, checksum) => {
onFileBuilt: (builtPath: string, checksum: string, sourceAssetPaths: string[]) => {
if (buildResult.manifest) {
const updatedManifest = updateManifestChecksum({
let updatedManifest = updateManifestChecksum({
manifest: buildResult.manifest,
entityType: 'frontComponent',
builtPath,
checksum,
});

if (updatedManifest && sourceAssetPaths.length > 0 && assetsWatcher) {
const builtAssets = this.resolveBuiltAssets(assetsWatcher, sourceAssetPaths);
updatedManifest = updateManifestAssets({
manifest: updatedManifest,
entityType: 'frontComponent',
builtPath,
assets: builtAssets,
});
}

if (updatedManifest) {
buildResult.manifest = updatedManifest;
}
Expand All @@ -102,7 +142,21 @@ export class AppBuildCommand {
await this.frontComponentsBuilder.start();
}

private resolveBuiltAssets(assetsWatcher: AssetsWatcher, sourceAssetPaths: string[]): BuiltAsset[] {
const builtAssets: BuiltAsset[] = [];

for (const sourceAssetPath of sourceAssetPaths) {
const builtAsset = assetsWatcher.getBuiltAsset(sourceAssetPath);
if (builtAsset) {
builtAssets.push(builtAsset);
}
}

return builtAssets;
}

private async cleanup(): Promise<void> {
await this.assetsBuilder?.close();
await this.functionsBuilder?.close();
await this.frontComponentsBuilder?.close();
await manifestExtractFromFileServer.closeViteServer();
Expand Down
Loading
Loading