fix(twenty-front): fix tsconfig to properly typecheck all files with tsgo (#17380)

## Summary

This PR fixes the `tsconfig` setup in `twenty-front` so that `tsgo -p
tsconfig.json` properly type-checks all files.

### Root Cause

The previous setup used TypeScript project references with `files: []`
in the main `tsconfig.json`. When running `tsgo -p tsconfig.json`, this
checks nothing because `tsgo` requires the `-b` (build) flag for project
references, but the configs weren't set up for composite mode.

### Changes

**Simplified tsconfig architecture (4 files → 2):**
- `tsconfig.json` - All files (dev, tests, stories) for
typecheck/IDE/lint
- `tsconfig.build.json` - Production files only (excludes tests/stories)

**Removed redundant configs:**
- `tsconfig.dev.json`
- `tsconfig.spec.json` 
- `tsconfig.storybook.json`

**Updated references:**
- `jest.config.mjs` → uses `tsconfig.json`
- `eslint.config.mjs` → uses `tsconfig.json`
- `vite.config.ts` → uses `tsconfig.json` for dev

**Type fixes (pre-existing errors revealed by proper typechecking):**
- Made `applicationId` optional in `FieldMetadataItem` and
`ObjectMetadataItem`
- Added missing `navigationMenuItem` translation
- Added `objectLabelSingular` to Search GraphQL query
- Fixed `sortMorphItems.test.ts` mock data

## Test plan

- [ ] Run `npx nx typecheck twenty-front` - should pass
- [ ] Run `npx nx lint twenty-front` - should work
- [ ] Run `npx nx test twenty-front` - should work
- [ ] Run `npx nx build twenty-front` - should work
- [ ] Verify IDE type checking works correctly
This commit is contained in:
Félix Malfait 2026-01-23 11:22:23 +01:00 committed by GitHub
parent cb9fe604e4
commit 41dd9856e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 186 additions and 339 deletions

View file

@ -1,4 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"allowJs": false,
"esModuleInterop": false,
@ -8,19 +9,17 @@
"noImplicitAny": true,
"strictBindCallApply": false,
"noEmit": true,
"types": ["jest", "node"],
"paths": {
"@/*": ["./src/*"]
}
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.d.ts",
"**/__mocks__/**/*",
"vite.config.ts",
"jest.config.mjs"
]
}

View file

@ -1,16 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"]
},
"include": [
"**/__mocks__/**/*",
"vite.config.ts",
"jest.config.mjs",
"src/**/*.d.ts",
"src/**/*.spec.ts",
"src/**/*.spec.tsx",
"src/**/*.test.ts",
"src/**/*.test.tsx"
]
}

View file

@ -230,7 +230,7 @@ npm run test -- --watch
npx twenty-cli app dev
# Type checking
npx tsc --noEmit
npx tsgo --noEmit
```
## Testing

View file

@ -1,2 +1,8 @@
dist
.react-email/
# Build artifacts
src/**/*.js
src/**/*.js.map
vite.config.js
vite.config.js.map

View file

@ -23,7 +23,7 @@ export default [
languageOptions: {
parser: typescriptParser,
parserOptions: {
project: [path.resolve(__dirname, 'tsconfig.*.json')],
project: [path.resolve(__dirname, 'tsconfig.json')],
ecmaFeatures: {
jsx: true,
},

View file

@ -1,23 +1,20 @@
{
"type": "module",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"types": ["vite/client"],
"types": ["vite/client", "node"],
"paths": {
"@/*": ["./src/*"],
"src/*": ["./src/*"]
}
},
"files": [],
"include": ["vite.config.ts"],
"references": [
{
"path": "./tsconfig.lib.json"
}
],
"extends": "../../tsconfig.base.json"
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"vite.config.ts"
]
}

View file

@ -4,21 +4,13 @@
"outDir": "../../.cache/tsc",
"composite": true,
"declaration": true,
"types": [
"node",
"@nx/react/typings/image.d.ts",
"vite/client"
]
"types": ["node", "@nx/react/typings/image.d.ts", "vite/client"]
},
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"],
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
"**/*.test.tsx"
]
}

View file

@ -10,6 +10,13 @@ export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/packages/twenty-emails',
resolve: {
alias: {
'@/': path.resolve(__dirname, 'src') + '/',
'src/': path.resolve(__dirname, 'src') + '/',
},
},
plugins: [
react({
plugins: [['@lingui/swc-plugin', {}]],

View file

@ -4,7 +4,7 @@ export default {
silent: false,
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/packages/twenty-eslint-rules',

View file

@ -4,16 +4,8 @@
"outDir": "../../.cache/tsc",
"esModuleInterop": true,
"moduleResolution": "node16",
"module": "node16"
"module": "node16",
"types": ["jest", "node"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lint.json"
},
{
"path": "./tsconfig.spec.json"
}
]
"include": ["**/*.ts", "jest.config.mjs"]
}

View file

@ -1,8 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["node"]
},
"exclude": ["**/*.spec.ts"],
"include": ["**/*.ts"]
}

View file

@ -1,7 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"]
},
"include": ["jest.config.mjs", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"]
}

View file

@ -49,11 +49,7 @@ const config = [
languageOptions: {
parser: typescriptParser,
parserOptions: {
project: [
path.resolve(__dirname, 'tsconfig.dev.json'),
path.resolve(__dirname, 'tsconfig.storybook.json'),
path.resolve(__dirname, 'tsconfig.spec.json'),
],
project: [path.resolve(__dirname, 'tsconfig.json')],
ecmaFeatures: {
jsx: true,
},

View file

@ -6,7 +6,7 @@ import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const tsConfigPath = resolve(__dirname, './tsconfig.spec.json');
const tsConfigPath = resolve(__dirname, './tsconfig.json');
const tsConfig = JSON.parse(readFileSync(tsConfigPath, 'utf8'));
// eslint-disable-next-line no-undef
@ -55,7 +55,7 @@ const jestConfig = {
'<rootDir>/__mocks__/imageMockFront.js',
'\\.css$': '<rootDir>/__mocks__/styleMock.js',
...pathsToModuleNameMapper(tsConfig.compilerOptions.paths, {
prefix: '<rootDir>/../../',
prefix: '<rootDir>/',
}),
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],

File diff suppressed because one or more lines are too long

View file

@ -21,6 +21,7 @@ export const SEARCH_QUERY = gql`
node {
recordId
objectNameSingular
objectLabelSingular
label
imageUrl
tsRankCD

View file

@ -44,6 +44,7 @@ export const useMetadataErrorHandler = () => {
viewFilterGroup: t`view filter group`,
commandMenuItem: t`command menu item`,
frontComponent: t`front component`,
navigationMenuItem: t`navigation menu item`,
} as const satisfies Record<AllMetadataName, string>;
const handleMetadataError = (

View file

@ -17,9 +17,15 @@ export type FieldMetadataItemOption = PartialFieldMetadataItemOption & {
export type FieldMetadataItem = Omit<
Field,
'__typename' | 'defaultValue' | 'options' | 'relation' | 'morphRelations'
| '__typename'
| 'applicationId'
| 'defaultValue'
| 'options'
| 'relation'
| 'morphRelations'
> & {
__typename?: string;
applicationId?: string;
defaultValue?: any;
options?: FieldMetadataItemOption[] | null;
relation?: FieldMetadataItemRelation | null;

View file

@ -6,6 +6,7 @@ import { type FieldMetadataItem } from './FieldMetadataItem';
export type ObjectMetadataItem = Omit<
GeneratedObject,
| '__typename'
| 'applicationId'
| 'fields'
| 'indexMetadatas'
| 'labelIdentifierFieldMetadataId'
@ -13,6 +14,7 @@ export type ObjectMetadataItem = Omit<
| 'indexMetadataList'
> & {
__typename?: string;
applicationId?: string;
fields: FieldMetadataItem[];
readableFields: FieldMetadataItem[];
updatableFields: FieldMetadataItem[];

View file

@ -17,7 +17,7 @@ import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { isCompositeFilterableFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFilterableFieldType';
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
@ -117,7 +117,7 @@ export const AdvancedFilterFieldSelectMenu = ({
recordFilterId,
});
if (isCompositeFieldType(filterType)) {
if (isCompositeFilterableFieldType(filterType)) {
setObjectFilterDropdownSubMenuFieldType(filterType);
setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id);

View file

@ -54,6 +54,13 @@ export const computeDraftValueFromString = <FieldValue>({
if (isFieldAddress(fieldDefinition)) {
return {
addressStreet1: value,
addressStreet2: null,
addressCity: null,
addressState: null,
addressPostcode: null,
addressCountry: null,
addressLat: null,
addressLng: null,
} as FieldInputDraftValue<FieldValue>;
}

View file

@ -48,7 +48,7 @@ export const computeEmptyDraftValue = <FieldValue>({
addressState: '',
addressCountry: '',
addressPostcode: '',
} as FieldInputDraftValue<FieldValue>;
} as unknown as FieldInputDraftValue<FieldValue>;
}
if (isFieldCurrency(fieldDefinition)) {

View file

@ -16,6 +16,7 @@ const createSearchRecord = (recordId: string): SearchRecord => ({
recordId,
label: `Record ${recordId}`,
objectNameSingular: 'person',
objectLabelSingular: 'Person',
tsRank: 0,
tsRankCD: 0,
});

View file

@ -14,7 +14,7 @@ import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { isCompositeFilterableFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFilterableFieldType';
import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems';
import { RECORD_LEVEL_PERMISSION_PREDICATE_FIELD_TYPES } from '@/settings/roles/role-permissions/object-level-permissions/record-level-permissions/constants/RecordLevelPermissionPredicateFieldTypes';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
@ -99,7 +99,7 @@ export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectF
selectedFieldMetadataItem.type,
);
if (isCompositeFieldType(filterType)) {
if (isCompositeFilterableFieldType(filterType)) {
setObjectFilterDropdownSubMenuFieldType(filterType);
setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id);
setObjectFilterDropdownIsSelectingCompositeField(true);

View file

@ -155,7 +155,7 @@ export const Dialog = ({
{children}
{buttons.map(({ accent, onClick, role, title: key, variant }) => (
<StyledDialogButton
onClick={(event) => {
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
onClose?.();
onClick?.(event);
}}

View file

@ -272,7 +272,9 @@ export const WorkflowEditActionIfElseBody = ({
title={t`Add route`}
variant="secondary"
size="small"
onClick={(event) => handleAddRoute(event)}
onClick={(event: React.MouseEvent<HTMLButtonElement>) =>
handleAddRoute(event)
}
/>
</>
)}

View file

@ -3,8 +3,16 @@
"compilerOptions": {
"types": ["node"]
},
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.d.ts",
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"**/__mocks__/**/*",
"**/__tests__/**/*",
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.stories.ts",
@ -12,12 +20,5 @@
"**/*.test.ts",
"**/*.test.tsx",
"src/testing/**/*"
],
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.d.ts",
"src/**/*.ts",
"src/**/*.tsx"
]
}

View file

@ -1,21 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["node"]
},
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.d.ts",
"src/**/*.ts",
"src/**/*.tsx",
"lingui.config.ts",
"jest.config.mjs",
"vite.config.ts",
"vitest.config.ts",
"setupTests.ts"
],
"exclude": [
"src/testing/**/*"
]
}

View file

@ -24,29 +24,29 @@
"@/*": ["./src/modules/*"],
"~/*": ["./src/*"]
},
"types": ["node", "jest"],
"plugins": [
{
"name": "@styled/typescript-styled-plugin",
"lint": {
"validProperties": ["container-type"],
"validProperties": ["container-type"]
}
}
]
},
"files": [],
"references": [
{
"path": "./tsconfig.dev.json"
},
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./tsconfig.storybook.json"
}
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.d.ts",
"src/**/*.ts",
"src/**/*.tsx",
".storybook/*.ts",
".storybook/*.tsx",
"lingui.config.ts",
"jest.config.mjs",
"vite.config.ts",
"vitest.config.ts",
"setupTests.ts"
],
"extends": "../../tsconfig.base.json"
}

View file

@ -1,26 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"],
"baseUrl": "../..",
"paths": {
"@/*": ["./packages/twenty-front/src/modules/*"],
"~/*": ["./packages/twenty-front/src/*"]
}
},
"include": [
"**/__mocks__/**/*",
"**/__tests__/**/*",
"jest.config.mjs",
"setupTests.ts",
"src/**/*.d.ts",
"src/**/*.spec.ts",
"src/**/*.spec.tsx",
"src/**/*.test.ts",
"src/**/*.test.tsx",
"src/testing/**/*",
"tsup.config.ts",
"tsup.ui.index.tsx",
"vite.config.ts"
]
}

View file

@ -1,14 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDecoratorMetadata": true
},
"include": [
".storybook/*.ts",
".storybook/*.tsx",
"src/**/*.d.ts",
"src/**/*.stories.mdx",
"src/**/*.stories.ts",
"src/**/*.stories.tsx"
]
}

View file

@ -39,7 +39,7 @@ export default defineConfig(({ command, mode }) => {
const tsConfigPath = isBuildCommand
? path.resolve(__dirname, './tsconfig.build.json')
: path.resolve(__dirname, './tsconfig.dev.json');
: path.resolve(__dirname, './tsconfig.json');
const CHUNK_SIZE_WARNING_LIMIT = 1024 * 1024; // 1MB
// Please don't increase this limit for main index chunk

View file

@ -1,9 +1,12 @@
import { type ApplicationManifest } from 'twenty-shared/application';
// Loose type for JSON manifest imports where enum values are inferred as strings
type JsonManifestInput = {
functions?: Array<{ builtHandlerChecksum?: string | null; [key: string]: unknown }>;
frontComponents?: Array<{ builtComponentChecksum?: string | null; [key: string]: unknown }>;
[key: string]: unknown;
};
// Replace dynamic checksum values with a placeholder for consistent comparisons
export const normalizeManifestForComparison = <
T extends Partial<ApplicationManifest>,
>(
export const normalizeManifestForComparison = <T extends JsonManifestInput>(
manifest: T,
): T => ({
...manifest,

View file

@ -33,7 +33,9 @@ export const processEsbuildResult = async ({
for (const outputFile of outputFiles) {
const absoluteOutputFile = path.resolve(outputFile);
const relativePath = path.relative(outputDir, absoluteOutputFile);
const builtPath = `${builtDir}/${relativePath}`;
// Normalize path separators to forward slashes for consistent matching
const normalizedRelativePath = relativePath.split(path.sep).join('/');
const builtPath = `${builtDir}/${normalizedRelativePath}`;
const content = await fs.readFile(absoluteOutputFile);
const checksum = crypto.createHash('md5').update(content).digest('hex');

View file

@ -1,6 +1,9 @@
import { glob } from 'fast-glob';
import path from 'path';
import { type Application } from 'twenty-shared/application';
import {
type Application,
type ApplicationVariables,
} from 'twenty-shared/application';
import { createLogger } from '../../common/logger';
import { manifestExtractFromFileServer } from '../manifest-extract-from-file-server';
import { type ValidationError } from '../manifest.types';
@ -82,7 +85,7 @@ export class ApplicationEntityBuilder
if (application?.applicationVariables) {
for (const [name, variable] of Object.entries(
application.applicationVariables,
)) {
) as [string, ApplicationVariables[string]][]) {
if (variable.universalIdentifier) {
const locations = seen.get(variable.universalIdentifier) ?? [];
locations.push(`application.variables.${name}`);

View file

@ -1,4 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"allowJs": false,
"esModuleInterop": false,
@ -9,19 +10,19 @@
"noImplicitAny": true,
"strictBindCallApply": false,
"noEmit": true,
"types": ["jest", "node"],
"paths": {
"@/*": ["./src/*"]
}
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json",
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.d.ts",
"**/__mocks__/**/*",
"**/__tests__/**/*",
"vite.config.ts",
"scripts/generateBarrels.ts",
"jest.config.mjs"
]
}

View file

@ -1,19 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"]
},
"include": [
"**/__mocks__/**/*",
"**/__tests__/**/*",
"vite.config.ts",
"scripts/generateBarrels.ts",
"jest.config.mjs",
"src/**/*.d.ts",
"src/**/*.spec.ts",
"src/**/*.spec.tsx",
"src/**/*.test.ts",
"src/**/*.test.tsx",
"src/**/*.e2e-spec.ts"
]
}

View file

@ -120,7 +120,7 @@ export default [
languageOptions: {
parser: typescriptParser,
parserOptions: {
project: [path.resolve(__dirname, 'tsconfig.*.json')],
project: [path.resolve(__dirname, 'tsconfig.json')],
},
},
plugins: {

View file

@ -1,4 +1,4 @@
import { type AllMetadataName } from '@/metadata/all-metadata-name.type';
import { type AllMetadataName } from './all-metadata-name.type';
export type FailedMetadataValidationError = {
code: string;

View file

@ -1,7 +1,7 @@
import { SOURCE_LOCALE } from '@/translations/constants/SourceLocale';
export const APP_LOCALES = {
[SOURCE_LOCALE]: SOURCE_LOCALE,
en: SOURCE_LOCALE,
'pseudo-en': 'pseudo-en',
'af-ZA': 'af-ZA',
'ar-SA': 'ar-SA',
@ -33,3 +33,5 @@ export const APP_LOCALES = {
'zh-CN': 'zh-CN',
'zh-TW': 'zh-TW',
} as const;
export type AppLocale = keyof typeof APP_LOCALES;

View file

@ -7,5 +7,6 @@
* |___/
*/
export type { AppLocale } from './constants/AppLocales';
export { APP_LOCALES } from './constants/AppLocales';
export { SOURCE_LOCALE } from './constants/SourceLocale';

View file

@ -306,7 +306,7 @@ describe('isMatchingCurrencyFilter', () => {
currencyCode: 'USD',
},
}),
).toThrowError('Unexpected filter for currency : {}');
).toThrow('Unexpected filter for currency : {}');
});
});
@ -320,7 +320,7 @@ describe('isMatchingCurrencyFilter', () => {
currencyFilter,
value: { amountMicros: 10 },
}),
).toThrowError(
).toThrow(
'Unexpected operand for currency amount micros filter : {"unexpected":10}',
);
});

View file

@ -1,5 +1,4 @@
import { APP_LOCALES } from '@/translations';
import { APP_LOCALES, type AppLocale } from '@/translations/constants/AppLocales';
export const isValidLocale = (
value: string | null,
): value is keyof typeof APP_LOCALES => value !== null && value in APP_LOCALES;
export const isValidLocale = (value: string | null): value is AppLocale =>
value !== null && value in APP_LOCALES;

View file

@ -1,13 +1,14 @@
import { APP_LOCALES, SOURCE_LOCALE } from '@/translations';
import {
APP_LOCALES,
type AppLocale,
} from '@/translations/constants/AppLocales';
import { SOURCE_LOCALE } from '@/translations/constants/SourceLocale';
/**
* Maps language codes to full locale keys in APP_LOCALES
* Example: 'fr' -> 'fr-FR', 'en' -> 'en'
*/
// Maps language codes to full locale keys in APP_LOCALES
// Example: 'fr' -> 'fr-FR', 'en' -> 'en'
const languageToLocaleMap = Object.keys(APP_LOCALES).reduce<
Record<string, string>
>((map, locale) => {
// Extract the language code (part before the hyphen or the whole code if no hyphen)
const language = locale.split('-')[0].toLowerCase();
// Only add to the map if not already added or if the current locale is the source locale
@ -20,19 +21,14 @@ const languageToLocaleMap = Object.keys(APP_LOCALES).reduce<
return map;
}, {});
/**
* Normalizes a locale string to match our supported formats
*/
export const normalizeLocale = (
value: string | null,
): keyof typeof APP_LOCALES => {
export const normalizeLocale = (value: string | null): AppLocale => {
if (value === null) {
return SOURCE_LOCALE;
}
// Direct match in our supported locales
if (value in APP_LOCALES) {
return value as keyof typeof APP_LOCALES;
return value as AppLocale;
}
// Try case-insensitive match (e.g., 'fr-fr' -> 'fr-FR')
@ -40,13 +36,13 @@ export const normalizeLocale = (
(locale) => locale.toLowerCase() === value.toLowerCase(),
);
if (caseInsensitiveMatch) {
return caseInsensitiveMatch as keyof typeof APP_LOCALES;
return caseInsensitiveMatch as AppLocale;
}
// Try matching just the language part (e.g., 'fr' -> 'fr-FR')
const languageCode = value?.trim() ? value.split('-')[0].toLowerCase() : '';
if (languageToLocaleMap[languageCode]) {
return languageToLocaleMap[languageCode] as keyof typeof APP_LOCALES;
return languageToLocaleMap[languageCode] as AppLocale;
}
return SOURCE_LOCALE;

View file

@ -1,4 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"allowJs": false,
"esModuleInterop": false,
@ -8,19 +9,15 @@
"noImplicitAny": true,
"strictBindCallApply": false,
"noEmit": true,
"types": ["jest", "node"],
"paths": {
"@/*": ["./src/*"]
}
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"**/__mocks__/**/*",
"jest.config.mjs"
]
}

View file

@ -4,19 +4,15 @@
"declaration": true,
"declarationMap": true,
"noEmit": false,
"baseUrl": ".",
"outDir": "../../.cache/tsc",
"types": ["node"]
},
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"],
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
"**/__mocks__/**/*"
]
}

View file

@ -1,15 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"]
},
"include": [
"**/__mocks__/**/*",
"jest.config.mjs",
"src/**/*.d.ts",
"src/**/*.spec.ts",
"src/**/*.spec.tsx",
"src/**/*.test.ts",
"src/**/*.test.tsx",
]
}

View file

@ -41,6 +41,11 @@ export default defineConfig(() => {
return {
root: __dirname,
cacheDir: '../../node_modules/.vite/packages/twenty-shared',
resolve: {
alias: {
'@/': path.resolve(__dirname, 'src') + '/',
},
},
plugins: [
tsconfigPaths({
root: __dirname

View file

@ -30,7 +30,7 @@ const config: StorybookConfig = {
plugins.push(
checker({
typescript: {
tsconfigPath: path.resolve(dirname, '../tsconfig.dev.json'),
tsconfigPath: path.resolve(dirname, '../tsconfig.json'),
},
}),
);

View file

@ -23,7 +23,7 @@ export default [
languageOptions: {
parser: typescriptParser,
parserOptions: {
project: [path.resolve(__dirname, 'tsconfig.*.json')],
project: [path.resolve(__dirname, 'tsconfig.json')],
ecmaFeatures: {
jsx: true,
},

View file

@ -9,9 +9,8 @@ const StyledButtonGroupContainer = styled.div`
display: flex;
`;
export type ButtonGroupProps = Pick<
ButtonProps,
'variant' | 'size' | 'accent'
export type ButtonGroupProps = Partial<
Pick<ButtonProps, 'variant' | 'size' | 'accent'>
> & {
className?: string;
children: ReactNode[];

View file

@ -1,15 +0,0 @@
{
"extends": "./tsconfig.json",
"include": [
"jest.config.mjs",
"setupTests.ts",
"src/*.d.ts",
"src/**/*.d.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts",
"src/**/*.stories.tsx",
"src/**/*.tsx",
"src/**/*.ts",
"vite.config.ts"
]
}

View file

@ -9,27 +9,21 @@
"moduleResolution": "bundler",
"esModuleInterop": true,
"noEmit": true,
"types": ["node"],
"types": ["node", "jest"],
"outDir": "../../.cache/tsc",
"paths": {
"@ui/*": ["./src/*"],
"@assets/*": ["./src/assets/*"]
}
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.dev.json"
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./tsconfig.storybook.json"
}
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.d.ts",
".storybook/*.ts",
".storybook/*.tsx",
"jest.config.mjs",
"setupTests.ts",
"vite.config.ts"
]
}
}

View file

@ -3,9 +3,9 @@
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"baseUrl": ".",
"noEmit": false
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.tsx"],
"exclude": [
"**/*.spec.ts",
"**/*.spec.tsx",
@ -13,6 +13,5 @@
"**/*.stories.tsx",
"**/*.test.ts",
"**/*.test.tsx"
],
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.tsx"]
]
}

View file

@ -1,11 +0,0 @@
{
"extends": "./tsconfig.json",
"include": [
"jest.config.mjs",
"setupTests.ts",
"src/**/*.d.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts",
"vite.config.ts"
]
}

View file

@ -1,15 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDecoratorMetadata": true
},
"include": [
".storybook/*.ts",
".storybook/*.tsx",
"src/*.d.ts",
"src/**/*.d.ts",
"src/**/*.stories.mdx",
"src/**/*.stories.ts",
"src/**/*.stories.tsx"
]
}

View file

@ -41,7 +41,7 @@ export default defineConfig(({ command }) => {
const tsConfigPath = isBuildCommand
? path.resolve(__dirname, './tsconfig.lib.json')
: path.resolve(__dirname, './tsconfig.dev.json');
: path.resolve(__dirname, './tsconfig.json');
const checkersConfig: Checkers = {
typescript: {
@ -55,6 +55,12 @@ export default defineConfig(({ command }) => {
};
return {
resolve: {
alias: {
'@ui/': path.resolve(__dirname, 'src') + '/',
'@assets/': path.resolve(__dirname, 'src/assets') + '/',
},
},
css: {
modules: {
localsConvention: 'camelCaseOnly',