Files
twenty/packages/twenty-shared/src/vite/createWywProfilingPlugin.ts
T
Charles BochetandGitHub 9d57bc39e5 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 01:03:50 +01:00

140 lines
4.5 KiB
TypeScript

/* oxlint-disable no-console */
import { type Plugin } from 'vite';
const LINARIA_IMPORT_RE = /@linaria/;
// Minimal Linaria code used to trigger WYW's Babel JIT compilation before
// the real build starts, so the first real file doesn't pay the cold-start cost.
// The ID must be inside the project root so WYW can resolve @linaria/react
// from node_modules. It is set in configResolved once config.root is known.
const WARMUP_CODE = `import { styled } from '@linaria/react';
const StyledDiv = styled.div\`color: red;\`;
`;
type WywProfilingOptions = {
// Used only for dev-mode real-time slow-file alerts. Summary always uses 10x avg.
devSlowThresholdMs?: number;
topSlowFilesCount?: number;
warmupThresholdMs?: number;
};
export const createWywProfilingPlugin = (
wywPlugin: Plugin,
options?: WywProfilingOptions,
): Plugin => {
const devSlowThresholdMs = options?.devSlowThresholdMs ?? 200;
const topSlowFilesCount = options?.topSlowFilesCount ?? 10;
const warmupThresholdMs = options?.warmupThresholdMs ?? 500;
let totalMs = 0;
let fileCount = 0;
let skippedCount = 0;
let isDevMode = false;
let warmupId = `${process.cwd()}/src/__wyw_warmup__.tsx`;
const allTransforms: { id: string; ms: number }[] = [];
const originalTransform = wywPlugin.transform;
return {
...wywPlugin,
enforce: 'pre' as const,
configResolved(config) {
isDevMode = config.command === 'serve';
warmupId = `${config.root}/src/__wyw_warmup__.tsx`;
if (typeof wywPlugin.configResolved === 'function') {
(wywPlugin.configResolved as Function).call(this, config);
}
},
async buildStart() {
console.log(`[linaria/wyw] Starting CSS pre-build`);
const warmupStart = performance.now();
try {
const warmupResult = (originalTransform as Function).call(
this,
WARMUP_CODE,
warmupId,
);
if (
warmupResult !== null &&
typeof warmupResult === 'object' &&
'then' in warmupResult
) {
await warmupResult;
}
} catch {
// Expected: fake file path causes module resolution errors, but
// Babel's JIT compilation is already triggered — that's all we need.
}
const warmupMs = performance.now() - warmupStart;
const warmupWarning = warmupMs > warmupThresholdMs ? ' ⚠️ slow' : '';
console.log(
`[linaria/wyw] Pre-warm: ${warmupMs.toFixed(0)}ms${warmupWarning}`,
);
},
transform(code: string, id: string, ...rest: unknown[]) {
if (!LINARIA_IMPORT_RE.test(code)) {
skippedCount++;
return null;
}
const start = performance.now();
const result = (originalTransform as Function).call(
this,
code,
id,
...rest,
);
const handleTiming = (elapsed: number) => {
totalMs += elapsed;
fileCount++;
allTransforms.push({ id, ms: elapsed });
if (isDevMode && elapsed > devSlowThresholdMs) {
console.log(
`[linaria/wyw] slow: ${id.replace(process.cwd(), '')} ${elapsed.toFixed(0)}ms`,
);
}
};
if (result && typeof result === 'object' && 'then' in result) {
return (result as Promise<unknown>).then((res) => {
handleTiming(performance.now() - start);
return res;
});
}
handleTiming(performance.now() - start);
return result;
},
closeBundle: () => {
const avg = fileCount > 0 ? totalMs / fileCount : 0;
const dynamicThreshold = Math.round(10 * avg);
const slowFiles = allTransforms.filter((f) => f.ms > dynamicThreshold);
console.log('\n[linaria/wyw] ===== CSS PRE-BUILD SUMMARY =====');
console.log(`[linaria/wyw] Files transformed: ${fileCount}`);
console.log(`[linaria/wyw] Files skipped (no @linaria): ${skippedCount}`);
console.log(`[linaria/wyw] Transform time: ${totalMs.toFixed(0)}ms`);
console.log(
`[linaria/wyw] Avg per transformed file: ${avg.toFixed(1)}ms`,
);
if (slowFiles.length > 0) {
console.log(
`[linaria/wyw] Slow files (>10x avg = ${dynamicThreshold}ms):`,
);
slowFiles
.sort((a, b) => b.ms - a.ms)
.slice(0, topSlowFilesCount)
.forEach((slowFile) =>
console.log(
`[linaria/wyw] ${slowFile.ms.toFixed(0)}ms ${slowFile.id.replace(process.cwd(), '')}`,
),
);
}
console.log('[linaria/wyw] ==========================================\n');
},
};
};