## 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
140 lines
4.5 KiB
TypeScript
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');
|
|
},
|
|
};
|
|
};
|