2023-12-10 17:10:54 +00:00
|
|
|
{
|
2025-02-27 14:18:07 +00:00
|
|
|
"private": true,
|
2024-01-05 13:59:58 +00:00
|
|
|
"dependencies": {
|
2026-03-13 13:59:46 +00:00
|
|
|
"@apollo/client": "^4.0.0",
|
2024-01-05 13:59:58 +00:00
|
|
|
"@floating-ui/react": "^0.24.3",
|
2024-06-12 14:31:07 +00:00
|
|
|
"@linaria/core": "^6.2.0",
|
|
|
|
|
"@linaria/react": "^6.2.1",
|
2025-10-29 18:08:51 +00:00
|
|
|
"@radix-ui/colors": "^3.0.0",
|
2024-01-05 13:59:58 +00:00
|
|
|
"@sniptt/guards": "^0.2.0",
|
2025-03-05 11:02:31 +00:00
|
|
|
"@tabler/icons-react": "^3.31.0",
|
Complete linaria migration (#18361)
## Summary
Completes the migration of the frontend styling system from **Emotion**
(`@emotion/styled`, `@emotion/react`) to **Linaria** (`@linaria/react`,
`@linaria/core`), a zero-runtime CSS-in-JS library where styles are
extracted at build time.
This is the final step of the migration — all ~494 files across
`twenty-front`, `twenty-ui`, `twenty-website`, and `twenty-sdk` are now
fully converted.
## Changes
### Styling Migration (across ~480 component files)
- Replaced all `@emotion/styled` imports with `@linaria/react`
- Converted runtime theme access patterns (`({ theme }) => theme.x.y`)
to build-time `themeCssVariables` CSS custom properties
- Replaced `useTheme()` hook (from Emotion) with
`useContext(ThemeContext)` where runtime theme values are still needed
(e.g., passing colors to non-CSS props like icon components)
- Removed `@emotion/react` `css` helper usages in favor of Linaria
template literals
### Dependency & Configuration Changes
- **Removed**: `@emotion/react`, `@emotion/styled` from root
`package.json`
- **Added**: `@wyw-in-js/babel-preset`, `next-with-linaria` (for
twenty-website SSR support)
- Updated Nx generator defaults from `@emotion/styled` to
`@linaria/react` in `nx.json`
- Simplified `vite.config.ts` (removed Emotion-specific configuration)
- Updated `twenty-website/next.config.js` to use `next-with-linaria` for
SSR Linaria support
### Storybook & Testing
- Removed `ThemeProvider` from Emotion in Storybook previews
(`twenty-front`, `twenty-sdk`)
- Now relies solely on `ThemeContextProvider` for theme injection
### Documentation
- Removed the temporary `docs/emotion-to-linaria-migration-plan.md`
(migration complete)
- Updated `CLAUDE.md` and `README.md` to reflect Linaria as the styling
stack
- Updated frontend style guide docs across all locales
## How it works
Linaria extracts styles at build time via the `@wyw-in-js/vite` plugin.
All expressions in `styled` template literals must be **statically
evaluable** — no runtime theme objects or closures over component state.
- **Static styles** use `themeCssVariables` which map to CSS custom
properties (`var(--theme-color-x)`)
- **Runtime theme access** (for non-CSS use cases like icon `color`
props) uses `useContext(ThemeContext)` instead of Emotion's `useTheme()`
2026-03-03 23:50:06 +00:00
|
|
|
"@wyw-in-js/babel-preset": "^1.0.6",
|
2025-08-12 10:43:35 +00:00
|
|
|
"@wyw-in-js/vite": "^0.7.0",
|
2024-07-17 15:53:01 +00:00
|
|
|
"archiver": "^7.0.1",
|
2024-01-05 13:59:58 +00:00
|
|
|
"danger-plugin-todos": "^1.3.1",
|
|
|
|
|
"date-fns": "^2.30.0",
|
2024-02-24 11:50:32 +00:00
|
|
|
"date-fns-tz": "^2.0.0",
|
2024-01-05 13:59:58 +00:00
|
|
|
"deep-equal": "^2.2.2",
|
|
|
|
|
"file-type": "16.5.4",
|
2025-01-16 09:03:05 +00:00
|
|
|
"framer-motion": "^11.18.0",
|
2025-05-23 16:33:18 +00:00
|
|
|
"fuse.js": "^7.1.0",
|
2024-01-05 13:59:58 +00:00
|
|
|
"googleapis": "105",
|
|
|
|
|
"hex-rgb": "^5.0.0",
|
2025-08-05 12:34:20 +00:00
|
|
|
"immer": "^10.1.1",
|
Start Jotai Migration (#17893)
## Recoil → Jotai progressive migration: infrastructure +
ChipFieldDisplay
### Benchmark
In the beginning, there was no hope:
<img width="1180" height="948" alt="image"
src="https://github.com/user-attachments/assets/f8635991-52e6-4958-8240-6ba7214132b2"
/>
Then the hope was reborn
<img width="2070" height="948" alt="image"
src="https://github.com/user-attachments/assets/be1182b9-1c8d-4fdc-ab4c-1484ad74449d"
/>
### Approach
We introduce a **V2 state management layer** backed by Jotai that
mirrors the existing Recoil API, enabling component-by-component
migration without a big-bang rewrite.
#### V2 API (Jotai-backed, Recoil-ergonomic)
- `createStateV2` / `createFamilyStateV2` — drop-in replacements for
`createState` / `createFamilyState`, returning wrapper types over Jotai
atoms
- `useRecoilValueV2`, `useRecoilStateV2`, `useFamilyRecoilValueV2`, etc.
— thin wrappers around Jotai's `useAtomValue` / `useAtom` / `useSetAtom`
- A shared `jotaiStore` (via `createStore()`) passed to a
`<JotaiProvider>` wrapping `<RecoilRoot>`, also accessible imperatively
for dual-writes
#### Dual-write bridge for progressive migration
For state shared between migrated and non-migrated components, we use
**dual-write**: writers update both the Recoil atom and the Jotai V2
atom (via `jotaiStore.set()`). This avoids sync components or extra
subscriptions.
Write sites updated: `useUpsertRecordsInStore`, `useSetRecordTableData`,
`ListenRecordUpdatesEffect`, `RecordShowEffect`,
`useLoadRecordIndexStates`, `useUpdateObjectViewOptions`.
#### First migration: ChipFieldDisplay render path
- `useChipFieldDisplay` → reads `recordStoreFamilyStateV2` via
`useFamilyRecoilValueV2` (was `useRecoilValue(recordStoreFamilyState)`)
- `RecordChip` → reads `recordIndexOpenRecordInStateV2` via
`useRecoilValueV2` (was `useRecoilValue(recordIndexOpenRecordInState)`)
- `Avatar` (twenty-ui) and event handlers (`useOpenRecordInCommandMenu`)
left on Recoil — not on the render path / in a different package
#### Pattern for migrating additional state
1. Create V2 atom: `createStateV2` or `createFamilyStateV2`
2. Add `jotaiStore.set(v2Atom, value)` at each write site
3. Switch readers to `useRecoilValueV2(v2Atom)`
4. Once all readers are migrated, remove the Recoil atom and dual-writes
#### Why not jotai-recoil-adapter?
Evaluated
[jotai-recoil-adapter](https://github.com/clockelliptic/jotai-recoil-adapter)
— not production-ready (21 open issues, no React 19, forces providerless
mode, missing types). We built a purpose-built thin layer instead.
2026-02-12 15:05:38 +00:00
|
|
|
"jotai": "^2.17.1",
|
2024-01-05 13:59:58 +00:00
|
|
|
"libphonenumber-js": "^1.10.26",
|
|
|
|
|
"lodash.camelcase": "^4.3.0",
|
2024-06-27 14:37:34 +00:00
|
|
|
"lodash.chunk": "^4.2.0",
|
2024-05-03 17:19:21 +00:00
|
|
|
"lodash.compact": "^3.0.1",
|
2025-01-02 12:43:27 +00:00
|
|
|
"lodash.escaperegexp": "^4.1.2",
|
2024-03-20 13:21:58 +00:00
|
|
|
"lodash.groupby": "^4.6.0",
|
2024-05-03 17:19:21 +00:00
|
|
|
"lodash.identity": "^3.0.0",
|
2024-01-05 13:59:58 +00:00
|
|
|
"lodash.isempty": "^4.4.0",
|
|
|
|
|
"lodash.isequal": "^4.5.0",
|
|
|
|
|
"lodash.isobject": "^3.0.2",
|
|
|
|
|
"lodash.kebabcase": "^4.1.1",
|
2024-03-08 09:22:23 +00:00
|
|
|
"lodash.mapvalues": "^4.6.0",
|
2024-01-05 13:59:58 +00:00
|
|
|
"lodash.merge": "^4.6.2",
|
2024-02-13 21:16:21 +00:00
|
|
|
"lodash.omit": "^4.5.0",
|
2024-05-03 17:19:21 +00:00
|
|
|
"lodash.pickby": "^4.6.0",
|
2024-01-05 13:59:58 +00:00
|
|
|
"lodash.snakecase": "^4.1.1",
|
|
|
|
|
"lodash.upperfirst": "^4.3.1",
|
|
|
|
|
"microdiff": "^1.3.2",
|
Complete linaria migration (#18361)
## Summary
Completes the migration of the frontend styling system from **Emotion**
(`@emotion/styled`, `@emotion/react`) to **Linaria** (`@linaria/react`,
`@linaria/core`), a zero-runtime CSS-in-JS library where styles are
extracted at build time.
This is the final step of the migration — all ~494 files across
`twenty-front`, `twenty-ui`, `twenty-website`, and `twenty-sdk` are now
fully converted.
## Changes
### Styling Migration (across ~480 component files)
- Replaced all `@emotion/styled` imports with `@linaria/react`
- Converted runtime theme access patterns (`({ theme }) => theme.x.y`)
to build-time `themeCssVariables` CSS custom properties
- Replaced `useTheme()` hook (from Emotion) with
`useContext(ThemeContext)` where runtime theme values are still needed
(e.g., passing colors to non-CSS props like icon components)
- Removed `@emotion/react` `css` helper usages in favor of Linaria
template literals
### Dependency & Configuration Changes
- **Removed**: `@emotion/react`, `@emotion/styled` from root
`package.json`
- **Added**: `@wyw-in-js/babel-preset`, `next-with-linaria` (for
twenty-website SSR support)
- Updated Nx generator defaults from `@emotion/styled` to
`@linaria/react` in `nx.json`
- Simplified `vite.config.ts` (removed Emotion-specific configuration)
- Updated `twenty-website/next.config.js` to use `next-with-linaria` for
SSR Linaria support
### Storybook & Testing
- Removed `ThemeProvider` from Emotion in Storybook previews
(`twenty-front`, `twenty-sdk`)
- Now relies solely on `ThemeContextProvider` for theme injection
### Documentation
- Removed the temporary `docs/emotion-to-linaria-migration-plan.md`
(migration complete)
- Updated `CLAUDE.md` and `README.md` to reflect Linaria as the styling
stack
- Updated frontend style guide docs across all locales
## How it works
Linaria extracts styles at build time via the `@wyw-in-js/vite` plugin.
All expressions in `styled` template literals must be **statically
evaluable** — no runtime theme objects or closures over component state.
- **Static styles** use `themeCssVariables` which map to CSS custom
properties (`var(--theme-color-x)`)
- **Runtime theme access** (for non-CSS use cases like icon `color`
props) uses `useContext(ThemeContext)` instead of Emotion's `useTheme()`
2026-03-03 23:50:06 +00:00
|
|
|
"next-with-linaria": "^1.3.0",
|
2024-02-13 21:16:21 +00:00
|
|
|
"planer": "^1.2.0",
|
2024-04-17 08:52:10 +00:00
|
|
|
"pluralize": "^8.0.0",
|
2024-01-05 13:59:58 +00:00
|
|
|
"react": "^18.2.0",
|
|
|
|
|
"react-dom": "^18.2.0",
|
|
|
|
|
"react-responsive": "^9.0.2",
|
2026-03-05 08:36:33 +00:00
|
|
|
"react-router-dom": "^6.30.3",
|
2024-01-05 13:59:58 +00:00
|
|
|
"react-tooltip": "^5.13.1",
|
2026-02-25 22:12:23 +00:00
|
|
|
"remark-gfm": "^4.0.1",
|
2024-01-05 13:59:58 +00:00
|
|
|
"rxjs": "^7.2.0",
|
|
|
|
|
"semver": "^7.5.4",
|
2024-10-21 16:28:31 +00:00
|
|
|
"slash": "^5.1.0",
|
Refactored Date to Temporal in critical date zones (#16544)
Fixes https://github.com/twentyhq/twenty/issues/16110
This PR implements Temporal to replace the legacy Date object, in all
features that are time zone sensitive. (around 80% of the app)
Here we define a few utils to handle Temporal primitives and obtain an
easier DX for timezone manipulation, front end and back end.
This PR deactivates the usage of timezone from the graph configuration,
because for now it's always UTC and is not really relevant, let's handle
that later.
Workflows code and backend only code that don't take user input are
using UTC time zone, the affected utils have not been refactored yet
because this PR is big enough.
# New way of filtering on date intervals
As we'll progressively rollup Temporal everywhere in the codebase and
remove `Date` JS object everywhere possible, we'll use the way to filter
that is recommended by Temporal.
This way of filtering on date intervals involves half-open intervals,
and is the preferred way to avoid edge-cases with DST and smallest time
increment edge-case.
## Filtering endOfX with DST edge-cases
Some day-light save time shifts involve having no existing hour, or even
day on certain days, for example Samoa Islands have no 30th of December
2011 : https://www.timeanddate.com/news/time/samoa-dateline.html, it
jumps from 29th to 31st, so filtering on `< next period start` makes it
easier to let the date library handle the strict inferior comparison,
than filtering on `≤ end of period` and trying to compute manually the
end of the period.
For example for Samoa Islands, is end of day `2011-12-29T23:59:59.999`
or is it `2011-12-30T23:59:59.999` ? If you say I don't need to know and
compute it, because I want everything strictly before
`2011-12-29T00:00:00 + start of next day (according to the library which
knows those edge-cases)`, then you have a 100% deterministic way of
computing date intervals in any timezone, for any day of any year.
Of course the Samoa example is an extreme one, but more common ones
involve DST shifts of 1 hour, which are still problematic on certain
days of the year.
## Computing the exact _end of period_
Having an open interval filtering, with `[included - included]` instead
of half-open `[included - excluded)`, forces to compute the open end of
an interval, which often involves taking an arbitrary unit like minute,
second, microsecond or nanosecond, which will lead to edge-case of
unhandled values.
For example, let's say my code computes endOfDay by setting the time to
`23:59:59.999`, if another library, API, or anything else, ends up
giving me a date-time with another time precision `23:59:59.999999999`
(down to the nanosecond), then this date-time will be filtered out,
while it should not.
The good deterministic way to avoid 100% of those complex bugs is to
create a half-open filter :
`≥ start of period` to `< start of next period`
For example :
`≥ 2025-01-01T00:00:00` to `< 2025-01-02T00:00:00` instead of `≥
2025-01-01T00:00:00` to `≤ 2025-01-01T23:59:59.999`
Because, `2025-01-01T00:00:00` = `2025-01-01T00:00:00.000` =
`2025-01-01T00:00:00.000000` = `2025-01-01T00:00:00.000000000` => no
risk of error in computing start of period
But `2025-01-01T23:59:59` ≠ `2025-01-01T23:59:59.999` ≠
`2025-01-01T23:59:59.999999` ≠ `2025-01-01T23:59:59.999999999` =>
existing risk of error in computing end of period
This is why an half-open interval has no risk of error in computing a
date-time interval filter.
Here is a link to this debate :
https://github.com/tc39/proposal-temporal/issues/2568
> For this reason, we recommend not calculating the exact nanosecond at
the end of the day if it's not absolutely necessary. For example, if
it's needed for <= comparisons, we recommend just changing the
comparison code. So instead of <= zdtEndOfDay your code could be <
zdtStartOfNextDay which is easier to calculate and not subject to the
issue of not knowing which unit is the right one.
>
> [Justin Grant](https://github.com/justingrant), top contributor of
Temporal
## Application to our codebase
Applying this half-open filtering paradigm to our codebase means we
would have to rename `IS_AFTER` to `IS_AFTER_OR_EQUAL` and to keep
`IS_BEFORE` (or even `IS_STRICTLY_BEFORE`) to make this half-open
interval self-explanatory everywhere in the codebase, this will avoid
any confusion.
See the relevant issue :
https://github.com/twentyhq/core-team-issues/issues/2010
In the mean time, we'll keep this operand and add this semantic in the
naming everywhere possible.
## Example with a different user timezone
Example on a graph grouped by week in timezone Pacific/Samoa, on a
computer running on Europe/Paris :
<img width="342" height="511" alt="image"
src="https://github.com/user-attachments/assets/9e7d5121-ecc4-4233-835b-f59293fbd8c8"
/>
Then the associated data in the table view, with our **half-open
date-time filter** :
<img width="804" height="262" alt="image"
src="https://github.com/user-attachments/assets/28efe1d7-d2fc-4aec-b521-bada7f980447"
/>
And the associated SQL query result to see how DATE_TRUNC in Postgres
applies its internal start of week logic :
<img width="709" height="220" alt="image"
src="https://github.com/user-attachments/assets/4d0542e1-eaae-4b4b-afa9-5005f48ffdca"
/>
The associated SQL query without parameters to test in your SQL client :
```SQL
SELECT "opportunity"."closeDate" as "close_date", TO_CHAR(DATE_TRUNC('week', "opportunity"."closeDate", 'Pacific/Samoa') AT TIME ZONE 'Pacific/Samoa', 'YYYY-MM-DD') AS "DATE_TRUNC by week start in timezone Pacific/Samoa", "opportunity"."name" FROM "workspace_1wgvd1injqtife6y4rvfbu3h5"."opportunity" "opportunity" ORDER BY "opportunity"."closeDate" ASC NULLS LAST
```
# Date picker simplification (not in this PR)
Our DatePicker component, which is wrapping `react-datepicker` library
component, is now exposing plain dates as string instead of Date object.
The Date object is still used internally to manage the library
component, but since the date picker calendar is only manipulating plain
dates, there is no need to add timezone management to it, and no need to
expose a handleChange with Date object.
The timezone management relies on date time inputs now.
The modification has been made in a previous PR :
https://github.com/twentyhq/twenty/issues/15377 but it's good to
reference it here.
# Calendar feature refactor
Calendar feature has been refactored to rely on Temporal.PlainDate as
much as possible, while leaving some date-fns utils to avoid re-coding
them.
Since the trick is to use utils to convert back and from Date object in
exec env reliably, we can do it everywhere we need to interface legacy
Date object utils and Temporal related code.
## TimeZone is now shown on Calendar :
<img width="894" height="958" alt="image"
src="https://github.com/user-attachments/assets/231f8107-fad6-4786-b532-456692c20f1d"
/>
## Month picker has been refactored
<img width="503" height="266" alt="image"
src="https://github.com/user-attachments/assets/cb90bc34-6c4d-436d-93bc-4b6fb00de7f5"
/>
Since the days weren't useful, the picker has been refactored to remove
the days.
# Miscellaneous
- Fixed a bug with drag and drop edge-case with 2 items in a list.
# Improvements
## Lots of chained operations
It would be nice to create small utils to avoid repeated chained
operations, but that is how Temporal is designed, a very small set of
primitive operations that allow to compose everything needed. Maybe
we'll have wrappers on top of Temporal in the coming years.
## Creation of Temporal objects is throwing errors
If the input is badly formatted Temporal will throw, we might want to
adopt a global strategy to avoid that.
Example :
```ts
const newPlainDate = Temporal.PlainDate.from('bad-string'); // Will throw
```
2025-12-23 17:40:26 +00:00
|
|
|
"temporal-polyfill": "^0.3.0",
|
2024-01-05 13:59:58 +00:00
|
|
|
"ts-key-enum": "^2.0.12",
|
2025-08-12 13:18:59 +00:00
|
|
|
"tslib": "^2.8.1",
|
2024-02-13 21:16:21 +00:00
|
|
|
"type-fest": "4.10.1",
|
2025-08-12 13:18:59 +00:00
|
|
|
"typescript": "5.9.2",
|
2024-01-05 13:59:58 +00:00
|
|
|
"uuid": "^9.0.0",
|
|
|
|
|
"vite-tsconfig-paths": "^4.2.1",
|
|
|
|
|
"xlsx-ugnis": "^0.19.3",
|
2025-09-24 16:29:05 +00:00
|
|
|
"zod": "^4.1.11"
|
2024-01-05 13:59:58 +00:00
|
|
|
},
|
|
|
|
|
"devDependencies": {
|
2024-01-29 09:17:12 +00:00
|
|
|
"@babel/core": "^7.14.5",
|
|
|
|
|
"@babel/preset-react": "^7.14.5",
|
2024-06-12 14:31:07 +00:00
|
|
|
"@babel/preset-typescript": "^7.24.6",
|
2026-01-13 08:18:07 +00:00
|
|
|
"@chromatic-com/storybook": "^4.1.3",
|
2024-01-05 13:59:58 +00:00
|
|
|
"@graphql-codegen/cli": "^3.3.1",
|
|
|
|
|
"@graphql-codegen/client-preset": "^4.1.0",
|
|
|
|
|
"@graphql-codegen/typescript": "^3.0.4",
|
|
|
|
|
"@graphql-codegen/typescript-operations": "^3.0.4",
|
|
|
|
|
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
2026-03-05 08:36:33 +00:00
|
|
|
"@nx/jest": "22.5.4",
|
|
|
|
|
"@nx/js": "22.5.4",
|
|
|
|
|
"@nx/react": "22.5.4",
|
|
|
|
|
"@nx/storybook": "22.5.4",
|
|
|
|
|
"@nx/vite": "22.5.4",
|
|
|
|
|
"@nx/web": "22.5.4",
|
Migrate from ESLint to OxLint (#18443)
## Summary
Fully replaces ESLint with OxLint across the entire monorepo:
- **Replaced all ESLint configs** (`eslint.config.mjs`) with OxLint
configs (`.oxlintrc.json`) for every package: `twenty-front`,
`twenty-server`, `twenty-emails`, `twenty-ui`, `twenty-shared`,
`twenty-sdk`, `twenty-zapier`, `twenty-docs`, `twenty-website`,
`twenty-apps/*`, `create-twenty-app`
- **Migrated custom lint rules** from ESLint plugin format to OxLint JS
plugin system (`@oxlint/plugins`), including
`styled-components-prefixed-with-styled`, `no-hardcoded-colors`,
`sort-css-properties-alphabetically`,
`graphql-resolvers-should-be-guarded`,
`rest-api-methods-should-be-guarded`, `max-consts-per-file`, and
Jotai-related rules
- **Migrated custom rule tests** from ESLint `RuleTester` + Jest to
`oxlint/plugins-dev` `RuleTester` + Vitest
- **Removed all ESLint dependencies** from `package.json` files and
regenerated lockfiles
- **Updated Nx targets** (`lint`, `lint:diff-with-main`, `fmt`) in
`nx.json` and per-project `project.json` to use `oxlint` commands with
proper `dependsOn` for plugin builds
- **Updated CI workflows** (`.github/workflows/ci-*.yaml`) — no more
ESLint executor
- **Updated IDE setup**: replaced `dbaeumer.vscode-eslint` with
`oxc.oxc-vscode` extension, configured `source.fixAll.oxc` and
format-on-save with Prettier
- **Replaced all `eslint-disable` comments** with `oxlint-disable`
equivalents across the codebase
- **Updated docs** (`twenty-docs`) to reference OxLint instead of ESLint
- **Renamed** `twenty-eslint-rules` package to `twenty-oxlint-rules`
### Temporarily disabled rules (tracked in `OXLINT_MIGRATION_TODO.md`)
| Rule | Package | Violations | Auto-fixable |
|------|---------|-----------|-------------|
| `twenty/sort-css-properties-alphabetically` | twenty-front | 578 | Yes
|
| `typescript/consistent-type-imports` | twenty-server | 3814 | Yes |
| `twenty/max-consts-per-file` | twenty-server | 94 | No |
### Dropped plugins (no OxLint equivalent)
`eslint-plugin-project-structure`, `lingui/*`, `@stylistic/*`,
`import/order`, `prefer-arrow/prefer-arrow-functions`,
`eslint-plugin-mdx`, `@next/eslint-plugin-next`,
`eslint-plugin-storybook`, `eslint-plugin-react-refresh`. Partial
coverage for `jsx-a11y` and `unused-imports`.
### Additional fixes (pre-existing issues exposed by merge)
- Fixed `EmailThreadPreview.tsx` broken import from main rename
(`useOpenEmailThreadInSidePanel`)
- Restored truthiness guard in `getActivityTargetObjectRecords.ts`
- Fixed `AgentTurnResolver` return types to match entity (virtual
`fileMediaType`/`fileUrl` are resolved via `@ResolveField()`)
## Test plan
- [x] `npx nx lint twenty-front` passes
- [x] `npx nx lint twenty-server` passes
- [x] `npx nx lint twenty-docs` passes
- [x] Custom oxlint rules validated with Vitest: `npx nx test
twenty-oxlint-rules`
- [x] `npx nx typecheck twenty-front` passes
- [x] `npx nx typecheck twenty-server` passes
- [x] CI workflows trigger correctly with `dependsOn:
["twenty-oxlint-rules:build"]`
- [x] IDE linting works with `oxc.oxc-vscode` extension
2026-03-06 00:03:50 +00:00
|
|
|
"@oxlint/plugins": "^1.51.0",
|
2025-06-05 10:28:42 +00:00
|
|
|
"@sentry/types": "^8",
|
2026-01-13 08:18:07 +00:00
|
|
|
"@storybook-community/storybook-addon-cookie": "^5.0.0",
|
|
|
|
|
"@storybook/addon-coverage": "^3.0.0",
|
2026-02-27 09:58:54 +00:00
|
|
|
"@storybook/addon-docs": "^10.2.13",
|
|
|
|
|
"@storybook/addon-links": "^10.2.13",
|
|
|
|
|
"@storybook/addon-vitest": "^10.2.13",
|
2026-01-13 08:18:07 +00:00
|
|
|
"@storybook/icons": "^2.0.1",
|
2026-02-27 09:58:54 +00:00
|
|
|
"@storybook/react-vite": "^10.2.13",
|
2026-01-13 08:18:07 +00:00
|
|
|
"@storybook/test-runner": "^0.24.2",
|
2026-03-05 08:36:33 +00:00
|
|
|
"@swc-node/register": "^1.11.1",
|
|
|
|
|
"@swc/cli": "^0.7.10",
|
|
|
|
|
"@swc/core": "^1.15.11",
|
|
|
|
|
"@swc/helpers": "~0.5.19",
|
2025-08-12 05:15:20 +00:00
|
|
|
"@swc/jest": "^0.2.39",
|
2026-01-11 13:54:41 +00:00
|
|
|
"@testing-library/dom": "^10.4.0",
|
2025-06-06 16:35:30 +00:00
|
|
|
"@testing-library/jest-dom": "^6.6.3",
|
|
|
|
|
"@testing-library/react": "^16.3.0",
|
2024-05-20 15:29:35 +00:00
|
|
|
"@types/addressparser": "^1.0.3",
|
2024-01-05 13:59:58 +00:00
|
|
|
"@types/bcrypt": "^5.0.0",
|
|
|
|
|
"@types/bytes": "^3.1.1",
|
2024-05-06 09:33:48 +00:00
|
|
|
"@types/chrome": "^0.0.267",
|
2024-01-05 13:59:58 +00:00
|
|
|
"@types/deep-equal": "^1.0.1",
|
2025-09-10 13:12:38 +00:00
|
|
|
"@types/fs-extra": "^11.0.4",
|
2024-01-05 13:59:58 +00:00
|
|
|
"@types/graphql-fields": "^1.3.6",
|
2025-09-10 13:12:38 +00:00
|
|
|
"@types/inquirer": "^9.0.9",
|
2025-08-11 10:02:33 +00:00
|
|
|
"@types/jest": "^30.0.0",
|
2024-01-05 13:59:58 +00:00
|
|
|
"@types/lodash.camelcase": "^4.3.7",
|
2024-05-03 17:19:21 +00:00
|
|
|
"@types/lodash.compact": "^3.0.9",
|
2025-01-02 12:43:27 +00:00
|
|
|
"@types/lodash.escaperegexp": "^4.1.9",
|
2024-03-20 13:43:41 +00:00
|
|
|
"@types/lodash.groupby": "^4.6.9",
|
2024-05-03 17:19:21 +00:00
|
|
|
"@types/lodash.identity": "^3.0.9",
|
2024-01-05 13:59:58 +00:00
|
|
|
"@types/lodash.isempty": "^4.4.7",
|
|
|
|
|
"@types/lodash.isequal": "^4.5.7",
|
|
|
|
|
"@types/lodash.isobject": "^3.0.7",
|
|
|
|
|
"@types/lodash.kebabcase": "^4.1.7",
|
2024-03-08 09:22:23 +00:00
|
|
|
"@types/lodash.mapvalues": "^4.6.9",
|
2024-03-25 15:26:28 +00:00
|
|
|
"@types/lodash.omit": "^4.5.9",
|
2024-05-03 17:19:21 +00:00
|
|
|
"@types/lodash.pickby": "^4.6.9",
|
2024-01-05 13:59:58 +00:00
|
|
|
"@types/lodash.snakecase": "^4.1.7",
|
|
|
|
|
"@types/lodash.upperfirst": "^4.3.7",
|
|
|
|
|
"@types/ms": "^0.7.31",
|
2025-08-07 15:02:12 +00:00
|
|
|
"@types/node": "^24.0.0",
|
2024-01-05 13:59:58 +00:00
|
|
|
"@types/passport-google-oauth20": "^2.0.11",
|
|
|
|
|
"@types/passport-jwt": "^3.0.8",
|
2025-08-11 13:58:05 +00:00
|
|
|
"@types/passport-microsoft": "^2.1.0",
|
2024-09-27 13:57:38 +00:00
|
|
|
"@types/pluralize": "^0.0.33",
|
2024-01-05 13:59:58 +00:00
|
|
|
"@types/react": "^18.2.39",
|
2024-04-16 14:58:08 +00:00
|
|
|
"@types/react-datepicker": "^6.2.0",
|
2024-01-05 13:59:58 +00:00
|
|
|
"@types/react-dom": "^18.2.15",
|
|
|
|
|
"@types/supertest": "^2.0.11",
|
|
|
|
|
"@types/uuid": "^9.0.2",
|
2026-01-19 11:46:34 +00:00
|
|
|
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
|
2026-02-19 15:27:56 +00:00
|
|
|
"@vitejs/plugin-react-swc": "4.2.3",
|
2026-02-12 13:35:22 +00:00
|
|
|
"@vitest/browser-playwright": "^4.0.18",
|
|
|
|
|
"@vitest/coverage-istanbul": "^4.0.18",
|
|
|
|
|
"@vitest/coverage-v8": "^4.0.18",
|
2025-02-27 14:18:07 +00:00
|
|
|
"@yarnpkg/types": "^4.0.0",
|
2024-01-05 13:59:58 +00:00
|
|
|
"chromatic": "^6.18.0",
|
2024-02-13 11:23:29 +00:00
|
|
|
"concurrently": "^8.2.2",
|
2025-10-22 09:42:24 +00:00
|
|
|
"danger": "^13.0.4",
|
2025-10-06 15:36:41 +00:00
|
|
|
"dotenv-cli": "^7.4.4",
|
2025-10-06 08:47:42 +00:00
|
|
|
"esbuild": "^0.25.10",
|
2024-01-05 13:59:58 +00:00
|
|
|
"http-server": "^14.1.1",
|
|
|
|
|
"jest": "29.7.0",
|
2025-06-06 16:35:30 +00:00
|
|
|
"jest-environment-jsdom": "30.0.0-beta.3",
|
2024-03-13 13:21:18 +00:00
|
|
|
"jest-environment-node": "^29.4.1",
|
2024-01-18 10:25:27 +00:00
|
|
|
"jest-fetch-mock": "^3.0.3",
|
2024-01-29 09:17:12 +00:00
|
|
|
"jsdom": "~22.1.0",
|
2026-01-18 03:25:45 +00:00
|
|
|
"msw": "^2.12.7",
|
|
|
|
|
"msw-storybook-addon": "^2.0.6",
|
2026-03-05 08:36:33 +00:00
|
|
|
"nx": "22.5.4",
|
2024-01-05 13:59:58 +00:00
|
|
|
"prettier": "^3.1.1",
|
|
|
|
|
"raw-loader": "^4.0.2",
|
|
|
|
|
"rimraf": "^5.0.5",
|
|
|
|
|
"source-map-support": "^0.5.20",
|
2026-02-27 09:58:54 +00:00
|
|
|
"storybook": "^10.2.13",
|
2026-01-13 08:18:07 +00:00
|
|
|
"storybook-addon-mock-date": "2.0.0",
|
2026-02-27 09:58:54 +00:00
|
|
|
"storybook-addon-pseudo-states": "^10.2.13",
|
2024-01-05 13:59:58 +00:00
|
|
|
"supertest": "^6.1.3",
|
|
|
|
|
"ts-jest": "^29.1.1",
|
|
|
|
|
"ts-loader": "^9.2.3",
|
|
|
|
|
"ts-node": "10.9.1",
|
2026-02-13 10:39:26 +00:00
|
|
|
"tsc-alias": "^1.8.16",
|
2024-02-20 13:35:09 +00:00
|
|
|
"tsconfig-paths": "^4.2.0",
|
2024-08-09 06:38:01 +00:00
|
|
|
"tsx": "^4.17.0",
|
2026-03-11 15:30:28 +00:00
|
|
|
"verdaccio": "^6.3.1",
|
2026-01-18 03:25:45 +00:00
|
|
|
"vite": "^7.0.0",
|
2026-02-12 13:35:22 +00:00
|
|
|
"vitest": "^4.0.18"
|
2024-01-05 13:59:58 +00:00
|
|
|
},
|
2023-12-11 09:54:57 +00:00
|
|
|
"engines": {
|
2025-08-07 16:22:28 +00:00
|
|
|
"node": "^24.5.0",
|
2023-12-11 09:54:57 +00:00
|
|
|
"npm": "please-use-yarn",
|
|
|
|
|
"yarn": ">=4.0.2"
|
|
|
|
|
},
|
2023-12-10 17:10:54 +00:00
|
|
|
"license": "AGPL-3.0",
|
2024-01-05 13:59:58 +00:00
|
|
|
"name": "twenty",
|
Refactor dependency graph for SDK, client-sdk and create-app (#18963)
## Summary
### Externalize `twenty-client-sdk` from `twenty-sdk`
Previously, `twenty-client-sdk` was listed as a `devDependency` of
`twenty-sdk`, which caused Vite to bundle it inline into the dist
output. This meant end-user apps had two copies of `twenty-client-sdk`:
one hidden inside `twenty-sdk`'s bundle, and one installed explicitly in
their `node_modules`. These copies could drift apart since they weren't
guaranteed to be the same version.
**Change:** Moved `twenty-client-sdk` from `devDependencies` to
`dependencies` in `twenty-sdk/package.json`. Vite's `external` function
now recognizes it and keeps it as an external `require`/`import` in the
dist output. End users get a single deduplicated copy resolved by their
package manager.
### Externalize `twenty-sdk` from `create-twenty-app`
Similarly, `create-twenty-app` had `twenty-sdk` as a `devDependency`
(bundled inline). After refactoring `create-twenty-app` to
programmatically import operations from `twenty-sdk` (instead of
shelling out via `execSync`), it became a proper runtime dependency.
**Change:** Moved `twenty-sdk` from `devDependencies` to `dependencies`
in `create-twenty-app/package.json`.
### Switch E2E CI to `yarn npm publish`
The `workspace:*` protocol in `dependencies` is a Yarn-specific feature.
`npm publish` publishes it as-is (which breaks for consumers), while
`yarn npm publish` automatically replaces `workspace:*` with the
resolved version at publish time (e.g., `workspace:*` becomes `=1.2.3`).
**Change:** Replaced `npm publish` with `yarn npm publish` in
`.github/workflows/ci-create-app-e2e.yaml`.
### Replace `execSync` with programmatic SDK calls in
`create-twenty-app`
`create-twenty-app` was shelling out to `yarn twenty remote add` and
`yarn twenty server start` via `execSync`, which assumed the `twenty`
binary was already installed in the scaffolded app. This was fragile and
created an implicit circular dependency.
**Changes:**
- Replaced `execSync('yarn twenty remote add ...')` with a direct call
to `authLoginOAuth()` from `twenty-sdk/cli`
- Replaced `execSync('yarn twenty server start')` with a direct call to
`serverStart()` from `twenty-sdk/cli`
- Deleted the duplicated `setup-local-instance.ts` from
`create-twenty-app`
### Centralize `serverStart` as a dedicated operation
The Docker server start logic was previously inline in the `server
start` CLI command handler (`server.ts`), and `setup-local-instance.ts`
was shelling out to `yarn twenty server start` to invoke it -- meaning
`twenty-sdk` was calling itself via a child process.
**Changes:**
- Extracted the Docker container management logic into a new
`serverStart` operation (`cli/operations/server-start.ts`)
- Merged the detect-or-start flow from `setup-local-instance.ts` into
`serverStart` (detect across multiple ports, start Docker if needed,
poll for health)
- Deleted `setup-local-instance.ts` from `twenty-sdk`
- Added `onProgress` callback (consistent with other operations like
`appBuild`) instead of direct `console.log` calls
- Both the `server start` CLI command and `create-twenty-app` now call
`serverStart()` programmatically
related to https://github.com/twentyhq/twenty-infra/pull/525
2026-03-26 10:56:52 +00:00
|
|
|
"packageManager": "yarn@4.13.0",
|
2024-01-05 13:59:58 +00:00
|
|
|
"resolutions": {
|
2025-10-22 14:07:47 +00:00
|
|
|
"graphql": "16.8.1",
|
2024-04-04 13:38:01 +00:00
|
|
|
"type-fest": "4.10.1",
|
2025-08-12 13:18:59 +00:00
|
|
|
"typescript": "5.9.2",
|
2026-03-31 19:55:50 +00:00
|
|
|
"nodemailer": "8.0.4",
|
2025-06-06 16:35:30 +00:00
|
|
|
"graphql-redis-subscriptions/ioredis": "^5.6.0",
|
2026-01-17 06:37:17 +00:00
|
|
|
"@lingui/core": "5.1.2",
|
2026-03-03 15:42:03 +00:00
|
|
|
"@types/qs": "6.9.16",
|
|
|
|
|
"@wyw-in-js/transform@npm:0.6.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
|
|
|
|
|
"@wyw-in-js/transform@npm:0.7.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch"
|
2024-01-05 13:59:58 +00:00
|
|
|
},
|
|
|
|
|
"version": "0.2.1",
|
2024-04-19 16:28:02 +00:00
|
|
|
"nx": {},
|
2024-02-13 11:23:29 +00:00
|
|
|
"scripts": {
|
2025-11-14 21:03:54 +00:00
|
|
|
"docs:generate": "tsx packages/twenty-docs/scripts/generate-docs-json.ts",
|
|
|
|
|
"docs:generate-navigation-template": "tsx packages/twenty-docs/scripts/generate-navigation-template.ts",
|
2026-01-25 12:29:20 +00:00
|
|
|
"docs:generate-paths": "tsx packages/twenty-docs/scripts/generate-documentation-paths.ts",
|
2024-11-07 14:57:12 +00:00
|
|
|
"start": "npx concurrently --kill-others 'npx nx run-many -t start -p twenty-server twenty-front' 'npx wait-on tcp:3000 && npx nx run twenty-server:worker'"
|
2024-02-13 11:23:29 +00:00
|
|
|
},
|
2023-12-10 17:10:54 +00:00
|
|
|
"workspaces": {
|
|
|
|
|
"packages": [
|
|
|
|
|
"packages/twenty-front",
|
2023-12-15 14:40:04 +00:00
|
|
|
"packages/twenty-server",
|
2024-01-22 15:21:56 +00:00
|
|
|
"packages/twenty-emails",
|
2024-03-13 13:21:18 +00:00
|
|
|
"packages/twenty-ui",
|
2023-12-15 14:40:04 +00:00
|
|
|
"packages/twenty-utils",
|
2023-12-23 09:08:55 +00:00
|
|
|
"packages/twenty-zapier",
|
2024-01-03 22:07:25 +00:00
|
|
|
"packages/twenty-website",
|
2026-03-27 12:48:03 +00:00
|
|
|
"packages/twenty-website-new",
|
2025-10-31 09:17:54 +00:00
|
|
|
"packages/twenty-docs",
|
2024-08-14 15:23:32 +00:00
|
|
|
"packages/twenty-e2e-testing",
|
2024-12-17 08:24:21 +00:00
|
|
|
"packages/twenty-shared",
|
2025-10-20 13:54:08 +00:00
|
|
|
"packages/twenty-sdk",
|
2026-03-30 17:06:06 +00:00
|
|
|
"packages/twenty-front-component-renderer",
|
Refactor twenty client sdk provisioning for logic function and front-component (#18544)
## 1. The `twenty-client-sdk` Package (Source of Truth)
The monorepo package at `packages/twenty-client-sdk` ships with:
- A **pre-built metadata client** (static, generated from a fixed
schema)
- A **stub core client** that throws at runtime (`CoreApiClient was not
generated...`)
- Both ESM (`.mjs`) and CJS (`.cjs`) bundles in `dist/`
- A `package.json` with proper `exports` map for
`twenty-client-sdk/core`, `twenty-client-sdk/metadata`, and
`twenty-client-sdk/generate`
## 2. Generation & Upload (Server-Side, at Migration Time)
**When**: `WorkspaceMigrationRunnerService.run()` executes after a
metadata schema change.
**What happens in `SdkClientGenerationService.generateAndStore()`**:
1. Copies the stub `twenty-client-sdk` package from the server's assets
(resolved via `SDK_CLIENT_PACKAGE_DIRNAME` — from
`dist/assets/twenty-client-sdk/` in production, or from `node_modules`
in dev)
2. Filters out `node_modules/` and `src/` during copy — only
`package.json` + `dist/` are kept (like an npm publish)
3. Calls `replaceCoreClient()` which uses `@genql/cli` to introspect the
**application-scoped** GraphQL schema and generates a real
`CoreApiClient`, then compiles it to ESM+CJS and overwrites
`dist/core.mjs` and `dist/core.cjs`
4. Archives the **entire package** (with `package.json` + `dist/`) into
`twenty-client-sdk.zip`
5. Uploads the single archive to S3 under
`FileFolder.GeneratedSdkClient`
6. Sets `isSdkLayerStale = true` on the `ApplicationEntity` in the
database
## 3. Invalidation Signal
The `isSdkLayerStale` boolean column on `ApplicationEntity` is the
invalidation mechanism:
- **Set to `true`** by `generateAndStore()` after uploading a new client
archive
- **Checked** by both logic function drivers before execution — if
`true`, they rebuild their local layer
- **Set back to `false`** by `markSdkLayerFresh()` after the driver has
successfully consumed the new archive
Default is `false` so existing applications without a generated client
aren't affected.
## 4a. Logic Functions — Local Driver
**`ensureSdkLayer()`** is called before every execution:
1. Checks if the local SDK layer directory exists AND `isSdkLayerStale`
is `false` → early return
2. Otherwise, cleans the local layer directory
3. Calls `downloadAndExtractToPackage()` which streams the zip from S3
directly to disk and extracts the full package into
`<tmpdir>/sdk/<workspaceId>-<appId>/node_modules/twenty-client-sdk/`
4. Calls `markSdkLayerFresh()` to set `isSdkLayerStale = false`
**At execution time**, `assembleNodeModules()` symlinks everything from
the deps layer's `node_modules/` **except** `twenty-client-sdk`, which
is symlinked from the SDK layer instead. This ensures the logic
function's `import ... from 'twenty-client-sdk/core'` resolves to the
generated client.
## 4b. Logic Functions — Lambda Driver
**`ensureSdkLayer()`** is called during `build()`:
1. Checks if `isSdkLayerStale` is `false` and an existing Lambda layer
ARN exists → early return
2. Otherwise, deletes all existing layer versions for this SDK layer
name
3. Calls `downloadArchiveBuffer()` to get the raw zip from S3 (no disk
extraction)
4. Calls `reprefixZipEntries()` which streams the zip entries into a
**new zip** with the path prefix
`nodejs/node_modules/twenty-client-sdk/` — this is the Lambda layer
convention path. All done in memory, no disk round-trip
5. Publishes the re-prefixed zip as a new Lambda layer via
`publishLayer()`
6. Calls `markSdkLayerFresh()`
**At function creation**, the Lambda is created with **two layers**:
`[depsLayerArn, sdkLayerArn]`. The SDK layer is listed last so it
overwrites the stub `twenty-client-sdk` from the deps layer (later
layers take precedence in Lambda's `/opt` merge).
## 5. Front Components
Front components are built by `app:build` with `twenty-client-sdk/core`
and `twenty-client-sdk/metadata` as **esbuild externals**. The stored
`.mjs` in S3 has unresolved bare import specifiers like `import {
CoreApiClient } from 'twenty-client-sdk/core'`.
SDK import resolution is split between the **frontend host** (fetching &
caching SDK modules) and the **Web Worker** (rewriting imports):
**Server endpoints**:
- `GET /rest/front-components/:id` —
`FrontComponentService.getBuiltComponentStream()` returns the **raw
`.mjs`** directly from file storage. No bundling, no SDK injection.
- `GET /rest/sdk-client/:applicationId/:moduleName` —
`SdkClientController` reads a single file (e.g. `dist/core.mjs`) from
the generated SDK archive via
`SdkClientGenerationService.readFileFromArchive()` and serves it as
JavaScript.
**Frontend host** (`FrontComponentRenderer` in `twenty-front`):
1. Queries `FindOneFrontComponent` which returns `applicationId`,
`builtComponentChecksum`, `usesSdkClient`, and `applicationTokenPair`
2. If `usesSdkClient` is `true`, renders
`FrontComponentRendererWithSdkClient` which calls the
`useApplicationSdkClient` hook
3. `useApplicationSdkClient({ applicationId, accessToken })` checks the
Jotai atom family cache for existing blob URLs. On cache miss, fetches
both SDK modules from `GET /rest/sdk-client/:applicationId/core` and
`/metadata`, creates **blob URLs** for each, and stores them in the atom
family
4. Once the blob URLs are cached, passes them as `sdkClientUrls`
(already blob URLs, not server URLs) to `SharedFrontComponentRenderer` →
`FrontComponentWorkerEffect` → worker's `render()` call via
`HostToWorkerRenderContext`
**Worker** (`remote-worker.ts` in `twenty-sdk`):
1. Fetches the raw component `.mjs` source as text
2. If `sdkClientUrls` are provided and the source contains SDK import
specifiers (`twenty-client-sdk/core`, `twenty-client-sdk/metadata`),
**rewrites** the bare specifiers to the blob URLs received from the host
(e.g. `'twenty-client-sdk/core'` → `'blob:...'`)
3. Creates a blob URL for the rewritten source and `import()`s it
4. Revokes only the component blob URL after the module is loaded — the
SDK blob URLs are owned and managed by the host's Jotai cache
This approach eliminates server-side esbuild bundling on every request,
caches SDK modules per application in the frontend, and keeps the
worker's job to a simple string rewrite.
## Summary Diagram
```
app:build (SDK)
└─ twenty-client-sdk stub (metadata=real, core=stub)
│
▼
WorkspaceMigrationRunnerService.run()
└─ SdkClientGenerationService.generateAndStore()
├─ Copy stub package (package.json + dist/)
├─ replaceCoreClient() → regenerate core.mjs/core.cjs
├─ Zip entire package → upload to S3
└─ Set isSdkLayerStale = true
│
┌────────┴────────────────────┐
▼ ▼
Logic Functions Front Components
│ │
├─ Local Driver ├─ GET /rest/sdk-client/:appId/core
│ └─ downloadAndExtract │ → core.mjs from archive
│ → symlink into │
│ node_modules ├─ Host (useApplicationSdkClient)
│ │ ├─ Fetch SDK modules
└─ Lambda Driver │ ├─ Create blob URLs
└─ downloadArchiveBuffer │ └─ Cache in Jotai atom family
→ reprefixZipEntries │
→ publish as Lambda ├─ GET /rest/front-components/:id
layer │ → raw .mjs (no bundling)
│
└─ Worker (browser)
├─ Fetch component .mjs
├─ Rewrite imports → blob URLs
└─ import() rewritten source
```
## Next PR
- Estimate perf improvement by implementing a redis caching for front
component client storage ( we don't even cache front comp initially )
- Implem frontent blob invalidation sse event from server
---------
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-24 18:10:25 +00:00
|
|
|
"packages/twenty-client-sdk",
|
2025-09-10 13:12:38 +00:00
|
|
|
"packages/twenty-apps",
|
|
|
|
|
"packages/twenty-cli",
|
2025-12-01 10:44:35 +00:00
|
|
|
"packages/create-twenty-app",
|
2026-03-25 09:45:43 +00:00
|
|
|
"packages/twenty-oxlint-rules",
|
|
|
|
|
"packages/twenty-companion"
|
2023-12-10 17:10:54 +00:00
|
|
|
]
|
2026-01-14 12:56:30 +00:00
|
|
|
},
|
|
|
|
|
"prettier": {
|
|
|
|
|
"singleQuote": true,
|
|
|
|
|
"trailingComma": "all",
|
|
|
|
|
"endOfLine": "lf"
|
2023-12-10 17:10:54 +00:00
|
|
|
}
|
|
|
|
|
}
|