chore(lint): add perf category to oxlint and fix resulting warnings

- Enable `perf: warn` in `.oxlintrc.json` and add `builtin` env; disable `no-await-in-loop` and `react/no-array-index-key` globally
- Reorder plugins so `oxc` is first
- Add `.oxfmtrc.json` override to suppress trailing commas in JSON/JSONC files
- Replace spread-into-new-object patterns (`{ ...x, key: val }`) with `Object.assign(x, { key: val })` in discover, explore, integrations, search, credits, and image-cache to satisfy perf rules
- Memoize `ChartContext` and `ToggleGroupContext` provider values to avoid unnecessary re-renders
This commit is contained in:
2026-03-18 15:48:47 -04:00
parent 2b0c683b7b
commit 3a0374c825
10 changed files with 43 additions and 33 deletions
+8
View File
@@ -38,6 +38,14 @@
],
"functions": ["cn", "clsx", "cva", "useResolveClassNames"]
},
"overrides": [
{
"files": ["**/*.json", "**/*.jsonc"],
"options": {
"trailingComma": "none"
}
}
],
"ignorePatterns": [
"node_modules",
"dist",
+9 -3
View File
@@ -1,9 +1,13 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["eslint", "typescript", "unicorn", "oxc", "react", "import"],
"plugins": ["oxc", "eslint", "typescript", "react", "import", "unicorn"],
"env": {
"builtin": true
},
"categories": {
"correctness": "error",
"suspicious": "warn"
"suspicious": "warn",
"perf": "warn"
},
"rules": {
"react/react-in-jsx-scope": "off",
@@ -12,7 +16,9 @@
"import/no-named-as-default-member": "off",
"unicorn/no-array-sort": "off",
"unicorn/consistent-function-scoping": "off",
"no-new": "off"
"no-new": "off",
"no-await-in-loop": "off",
"react/no-array-index-key": "off"
},
"ignorePatterns": ["node_modules", "dist", "build", "docs", "**/routeTree.gen.ts"]
}
+2 -3
View File
@@ -50,11 +50,10 @@ export const discover = os.discover.use(authed).handler(async ({ input, context
const titleMap = ensureBrowseTitlesExist(baseItems);
const items = baseItems.map((item) => {
const entry = titleMap.get(`${item.tmdbId}-${item.type}`);
return {
...item,
return Object.assign(item, {
id: entry?.id ?? "",
posterThumbHash: entry?.posterThumbHash ?? null,
};
});
});
const titleIds = items.map((r) => r.id);
+4 -6
View File
@@ -64,11 +64,10 @@ export const trending = os.explore.trending.use(authed).handler(async ({ input,
const items = baseItems.map((item) => {
const entry = titleMap.get(`${item.tmdbId}-${item.type}`);
return {
...item,
return Object.assign(item, {
id: entry?.id ?? "",
posterThumbHash: entry?.posterThumbHash ?? null,
};
});
});
const heroEntry = heroResult
@@ -125,11 +124,10 @@ export const popular = os.explore.popular.use(authed).handler(async ({ input, co
const titleMap = ensureBrowseTitlesExist(baseItems);
const items = baseItems.map((item) => {
const entry = titleMap.get(`${item.tmdbId}-${item.type}`);
return {
...item,
return Object.assign(item, {
id: entry?.id ?? "",
posterThumbHash: entry?.posterThumbHash ?? null,
};
});
});
const titleIds = items.map((r) => r.id);
@@ -55,8 +55,7 @@ export const list = os.integrations.list.use(authed).handler(({ context }) => {
const result = userIntegrations.map((integration) => {
const events = eventsByIntegration.get(integration.id) ?? [];
return {
...serializeIntegration(integration),
return Object.assign(serializeIntegration(integration), {
recentEvents: events.map((e) => ({
id: e.id,
eventType: e.eventType,
@@ -65,7 +64,7 @@ export const list = os.integrations.list.use(authed).handler(({ context }) => {
status: e.status,
receivedAt: e.receivedAt.toISOString(),
})),
};
});
});
return { integrations: result };
+3 -6
View File
@@ -53,10 +53,7 @@ export const search = os.search.use(authed).handler(async ({ input }) => {
})),
);
return {
results: personItems.map((r) => ({
...r,
id: personMap.get(r.tmdbId),
})),
results: personItems.map((r) => Object.assign(r, { id: personMap.get(r.tmdbId) })),
page: personResults.page ?? input.page,
totalPages: personResults.total_pages ?? 1,
totalResults: personResults.total_results ?? 0,
@@ -140,9 +137,9 @@ export const search = os.search.use(authed).handler(async ({ input }) => {
);
const results = mapped.map((r) => {
if (r.type === "person") return { ...r, id: personMap.get(r.tmdbId) };
if (r.type === "person") return Object.assign(r, { id: personMap.get(r.tmdbId) });
const entry = titleMap.get(`${r.tmdbId}-${r.type}`);
return { ...r, id: entry?.id };
return Object.assign(r, { id: entry?.id });
});
return {
+2 -1
View File
@@ -44,9 +44,10 @@ function ChartContainer({
}) {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
const contextValue = React.useMemo(() => ({ config }), [config]);
return (
<ChartContext.Provider value={{ config }}>
<ChartContext.Provider value={contextValue}>
<div
data-slot="chart"
data-chart={chartId}
+5 -3
View File
@@ -31,6 +31,10 @@ function ToggleGroup({
spacing?: number;
orientation?: "horizontal" | "vertical";
}) {
const contextValue = React.useMemo(
() => ({ variant, size, spacing, orientation }),
[variant, size, spacing, orientation],
);
return (
<ToggleGroupPrimitive
data-slot="toggle-group"
@@ -45,9 +49,7 @@ function ToggleGroup({
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size, spacing, orientation }}>
{children}
</ToggleGroupContext.Provider>
<ToggleGroupContext.Provider value={contextValue}>{children}</ToggleGroupContext.Provider>
</ToggleGroupPrimitive>
);
}
+3 -4
View File
@@ -361,8 +361,7 @@ export function getCastForTitle(titleId: string): CastMember[] {
.orderBy(titleCast.displayOrder)
.all();
return rows.map((r) => ({
...r,
profilePath: tmdbImageUrl(r.profilePath, "profiles"),
}));
return rows.map((r) =>
Object.assign(r, { profilePath: tmdbImageUrl(r.profilePath, "profiles") }),
);
}
+5 -4
View File
@@ -146,10 +146,11 @@ export async function cacheImagesForTitle(titleId: string) {
// Parallel cache checks instead of sequential awaits
const checks = await Promise.all(
candidates.map(async (c) => ({
...c,
cached: await isImageCached(c.category, path.basename(c.imgPath)),
})),
candidates.map(async (c) =>
Object.assign(c, {
cached: await isImageCached(c.category, path.basename(c.imgPath)),
}),
),
);
const tasks = checks
.filter((c) => !c.cached)