Files
twenty/packages/twenty-sdk/vite.config.ts
T
martmull 40eef5c464 Improve application ast (#17016)
# Summary

- Introduces a new, flexible folder structure for Twenty SDK
applications using file suffix-based entity detection
- Adds defineApp, defineFunction, defineObject, and defineRole helper
functions with built-in validation
- Refactors manifest loading to use jiti runtime evaluation for
TypeScript config files
- Separates validation logic into dedicated module with comprehensive
error reporting

  # New Application Folder Structure

Applications now use a convention-over-configuration approach where
entities are detected by their file suffix, allowing flexible
organization within the src/app/ folder.

  # Required Structure

    my-app/
    ├── package.json
    ├── yarn.lock
    └── src/
        ├── app/
│ └── application.config.ts # Required - main application configuration
└── utils/ # Optional - handler implementations & utilities

  # Entity Detection by File Suffix

    - *.object.ts - Custom object definitions
    - *.function.ts - Serverless function definitions
    - *.role.ts - Role definitions

  # Supported Folder Organizations

  ## Traditional (by type):
    src/app/
    ├── application.config.ts
    ├── objects/
    │   └── postCard.object.ts
    ├── functions/
    │   └── createPostCard.function.ts
    └── roles/
        └── admin.role.ts

  ## Feature-based:
    src/app/
    ├── application.config.ts
    └── post-card/
        ├── postCard.object.ts
        ├── createPostCard.function.ts
        └── postCardAdmin.role.ts

  ## Flat:
    src/app/
    ├── application.config.ts
    ├── postCard.object.ts
    ├── createPostCard.function.ts
    └── admin.role.ts

  # New Helper Functions

  ## defineApp(config)

    import { defineApp } from 'twenty-sdk';

    export default defineApp({
      universalIdentifier: '4ec0391d-...',
      displayName: 'My App',
      description: 'App description',
      icon: 'IconWorld',
    });

  ## defineObject(config)

    import { defineObject, FieldType } from 'twenty-sdk';

    export default defineObject({
      universalIdentifier: '54b589ca-...',
      nameSingular: 'postCard',
      namePlural: 'postCards',
      labelSingular: 'Post Card',
      labelPlural: 'Post Cards',
      icon: 'IconMail',
      fields: [
        {
          universalIdentifier: '58a0a314-...',
          type: FieldType.TEXT,
          name: 'content',
          label: 'Content',
        },
      ],
    });

  ## defineFunction(config)

    import { defineFunction } from 'twenty-sdk';
    import { myHandler } from '../utils/my-handler';

    export default defineFunction({
      universalIdentifier: 'e56d363b-...',
      name: 'My Function',
      handler: myHandler,
      triggers: [
        {
          universalIdentifier: 'c9f84c8d-...',
          type: 'route',
          path: '/my-route',
          httpMethod: 'POST',
        },
      ],
    });

  ## defineRole(config)

    import { defineRole, PermissionFlag } from 'twenty-sdk';

    export default defineRole({
      universalIdentifier: 'b648f87b-...',
      label: 'App User',
      objectPermissions: [
        {
          objectNameSingular: 'postCard',
          canReadObjectRecords: true,
        },
      ],
      permissionFlags: [PermissionFlag.UPLOAD_FILE],
    });

  # Test plan

    - Verify npx twenty app sync works with new folder structure
    - Verify npx twenty app dev works with new folder structure
    - Verify validation errors display correctly for invalid configs
- Verify all three folder organization styles work (traditional,
feature-based, flat)
    - Run existing E2E tests to ensure backward compatibility
2026-01-09 13:06:30 +00:00

116 lines
3.2 KiB
TypeScript

import * as fs from 'fs-extra';
import path from 'path';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import tsconfigPaths from 'vite-tsconfig-paths';
import packageJson from './package.json';
const moduleEntries = Object.keys((packageJson as any).exports || {})
.filter((key) => key !== '.' && !key.startsWith('./src/'))
.map((module) => `src/${module.replace(/^\.\//, '')}/index.ts`);
const entries = ['src/index.ts', 'src/cli/cli.ts', ...moduleEntries];
const entryFileNames = (chunk: any, extension: 'cjs' | 'mjs') => {
if (!chunk.isEntry) {
throw new Error(
`Should never occurs, encountered a non entry chunk ${chunk.facadeModuleId}`,
);
}
const splitFaceModuleId = chunk.facadeModuleId?.split('/');
if (splitFaceModuleId === undefined) {
throw new Error(
`Should never occurs splitFaceModuleId is undefined ${chunk.facadeModuleId}`,
);
}
const moduleDirectory = splitFaceModuleId[splitFaceModuleId?.length - 2];
if (moduleDirectory === 'src') {
return `${chunk.name}.${extension}`;
}
return `${moduleDirectory}.${extension}`;
};
const copySharedDist = () => {
return {
name: 'copy-twenty-shared-dist',
closeBundle: async () => {
const sharedDist = path.resolve(__dirname, '../twenty-shared/dist');
const vendorDist = path.resolve(__dirname, 'dist/vendor/twenty-shared');
await fs.remove(vendorDist);
await fs.ensureDir(path.dirname(vendorDist));
await fs.copy(sharedDist, vendorDist);
},
};
};
export default defineConfig(() => {
const tsConfigPath = path.resolve(__dirname, './tsconfig.lib.json');
return {
root: __dirname,
cacheDir: '../../node_modules/.vite/packages/twenty-sdk',
plugins: [
tsconfigPaths({
root: __dirname,
}),
copySharedDist(),
dts({
entryRoot: './src',
tsconfigPath: tsConfigPath,
exclude: ['vite.config.ts'],
beforeWriteFile: (filePath, content) => {
const fromDir = path.dirname(filePath);
const vendorDir = path.resolve(process.cwd(), 'dist/vendor');
let rel = path
.relative(fromDir, vendorDir)
.split(path.sep)
.join(path.posix.sep);
if (!rel.startsWith('.')) rel = `./${rel}`;
return {
filePath,
content: content.replace(
/(from\s+["'])twenty-shared(\/[^"']*)?(["'])/g,
`$1${rel}/twenty-shared$2$3`,
),
};
},
}),
],
build: {
outDir: 'dist',
lib: { entry: entries, name: 'twenty-sdk' },
rollupOptions: {
external: [
...Object.keys((packageJson as any).dependencies || {}),
'path',
'fs',
'url',
'crypto',
'stream',
'util',
'os',
],
output: [
{
format: 'es',
entryFileNames: (chunk) => entryFileNames(chunk, 'mjs'),
},
{
format: 'cjs',
interop: 'auto',
esModule: true,
exports: 'named',
entryFileNames: (chunk) => entryFileNames(chunk, 'cjs'),
},
],
},
},
logLevel: 'warn',
};
});