chore(lint): upgrade Biome vers 2.x, active noExplicitAny/noConsole/noFloatingPromises

`@biomejs/biome` passe de 1.9.4 à 2.5.9 (config migrée via `biome migrate
--write`) — nécessaire pour noFloatingPromises, une règle type-aware
apparue en 2.0 (nursery).

- noExplicitAny : déjà "recommended", actif depuis toujours, aucun changement.
- noConsole (biome.json) : bloque tout `console.*` sauf error/warn/info/
  debug/table/assert — équivalent à "pas de console.log" sans interdire
  les niveaux nommés (voir le nouveau log service dans le prochain commit,
  qui centralise justement ces appels).
- noFloatingPromises (nursery) activé explicitement sous `rules.nursery`
  sans avoir besoin d'activer le domaine "types" au sens large (ça aurait
  aussi allumé des dizaines d'autres règles type-aware type
  noUnresolvedImports/noUnnecessaryConditions, hors scope ici).

Le reste du diff, c'est soit du reformatage automatique (import sort, 2.x
ordonne différemment de 1.9.4 — `biome check --write --unsafe`), soit les
corrections des ~20 promesses flottantes que la nouvelle règle a fait
remonter :

- La plupart sont des `navigate(...)` non attendus (react-router v7 type
  `navigate` en `void | Promise<void>`) — préfixés `void navigate(...)`,
  aucun changement de comportement.
- Trois chargements initiaux en useEffect (OnboardingAllergensPage,
  OnboardingDietPage, OnboardingHouseholdPage, HouseholdSettingsPage)
  n'avaient jamais de `.catch()` du tout — ajouté (dégradation silencieuse
  vers un état vide/par défaut, même raisonnement que le `.catch()` déjà
  présent dans OnboardingSourcesPage).
- HouseholdSettingsPage : `loadHouse` était une fonction déclarée à chaque
  render (donc une référence différente à chaque fois) utilisée comme
  dépendance de useEffect ET passée en callback à des enfants — le
  useEffect se re-déclenchait donc à chaque re-render provoqué par son
  propre fetch, un vrai bug de boucle infinie de requêtes que
  noFloatingPromises a fait remonter indirectement (via
  useExhaustiveDependencies). Corrigé avec useCallback([]).
- RecipeDetailPanel : une clé de liste `${index}-...}` sur une liste
  statique (draft.steps, sans id stable — DraftRecipeStepView n'en a pas)
  — biome-ignore justifié, pas de bug réel.
- recipe.test.ts : variable `agent` non utilisée, retirée.

Vérifié : `pnpm --filter api test` (295/295), `pnpm lint` et `pnpm build`
clean sur tout le repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Nicolas 2026-08-21 10:11:57 +02:00
parent a53e8aff4a
commit 37a044a267
41 changed files with 167 additions and 139 deletions

View file

@ -1,6 +1,6 @@
import { import {
INGREDIENT_LABELS_EN,
INGREDIENT_LABEL_SYNONYMS_EN, INGREDIENT_LABEL_SYNONYMS_EN,
INGREDIENT_LABELS_EN,
UNIT_LABELS_EN, UNIT_LABELS_EN,
} from "@batch-cooking/shared"; } from "@batch-cooking/shared";
import { prisma } from "../db/prisma.js"; import { prisma } from "../db/prisma.js";

View file

@ -1,12 +1,12 @@
import type { UnitType } from "@batch-cooking/shared"; import type { UnitType } from "@batch-cooking/shared";
import { import {
type IngredientMatchEntry,
type UnitMatchEntry,
extractQuantity, extractQuantity,
type IngredientMatchEntry,
loadIngredientCatalog, loadIngredientCatalog,
loadUnitCatalog, loadUnitCatalog,
matchIngredientName, matchIngredientName,
matchUnit, matchUnit,
type UnitMatchEntry,
} from "./ingredient-matcher.js"; } from "./ingredient-matcher.js";
import type { import type {
ParsedRecipe, ParsedRecipe,
@ -14,9 +14,9 @@ import type {
ParsedRecipeStep, ParsedRecipeStep,
} from "./recipe-source-adapter.js"; } from "./recipe-source-adapter.js";
import { import {
type TechStepMappingRule,
loadTechStepMappingRules, loadTechStepMappingRules,
matchTechSteps, matchTechSteps,
type TechStepMappingRule,
} from "./tech-step-matcher.js"; } from "./tech-step-matcher.js";
/** /**

View file

@ -1,7 +1,7 @@
import { wrapAsyncHandler } from "@batch-cooking/express-tools"; import { wrapAsyncHandler } from "@batch-cooking/express-tools";
import { deleteAccountSchema, loginSchema, signupSchema } from "@batch-cooking/shared"; import { deleteAccountSchema, loginSchema, signupSchema } from "@batch-cooking/shared";
import { Router } from "express";
import type { CookieOptions, Response } from "express"; import type { CookieOptions, Response } from "express";
import { Router } from "express";
import { env } from "../../config/env.js"; import { env } from "../../config/env.js";
import { type AuthLocals, requireAuth } from "../../middlewares/require-auth.js"; import { type AuthLocals, requireAuth } from "../../middlewares/require-auth.js";
import { deleteAccount, login, signup } from "./auth.service.js"; import { deleteAccount, login, signup } from "./auth.service.js";

View file

@ -1,8 +1,8 @@
import { HttpError } from "@batch-cooking/error-tools"; import { HttpError } from "@batch-cooking/error-tools";
import { wrapAsyncHandler } from "@batch-cooking/express-tools"; import { wrapAsyncHandler } from "@batch-cooking/express-tools";
import { import {
ErrorCode,
createHouseSchema, createHouseSchema,
ErrorCode,
joinHouseSchema, joinHouseSchema,
renameHouseSchema, renameHouseSchema,
updateHouseSourcesSchema, updateHouseSourcesSchema,

View file

@ -1,7 +1,7 @@
import { parseDateOnly } from "@batch-cooking/date-tools"; import { parseDateOnly } from "@batch-cooking/date-tools";
import { HttpError } from "@batch-cooking/error-tools"; import { HttpError } from "@batch-cooking/error-tools";
import { wrapAsyncHandler } from "@batch-cooking/express-tools"; import { wrapAsyncHandler } from "@batch-cooking/express-tools";
import { ErrorCode, addPlanningItemSchema, getPlanningByDateSchema } from "@batch-cooking/shared"; import { addPlanningItemSchema, ErrorCode, getPlanningByDateSchema } from "@batch-cooking/shared";
import { Router } from "express"; import { Router } from "express";
import { type AuthLocals, requireAuth } from "../../middlewares/require-auth.js"; import { type AuthLocals, requireAuth } from "../../middlewares/require-auth.js";
import { addPlanningItem, getPlanningForDate, removePlanningItem } from "./planning.service.js"; import { addPlanningItem, getPlanningForDate, removePlanningItem } from "./planning.service.js";

View file

@ -1,8 +1,8 @@
import { HttpError } from "@batch-cooking/error-tools"; import { HttpError } from "@batch-cooking/error-tools";
import { wrapAsyncHandler } from "@batch-cooking/express-tools"; import { wrapAsyncHandler } from "@batch-cooking/express-tools";
import { import {
ErrorCode,
createRecipeSchema, createRecipeSchema,
ErrorCode,
listRecipesSchema, listRecipesSchema,
updateRecipeSchema, updateRecipeSchema,
} from "@batch-cooking/shared"; } from "@batch-cooking/shared";

View file

@ -1,6 +1,6 @@
import { HttpError } from "@batch-cooking/error-tools"; import { HttpError } from "@batch-cooking/error-tools";
import { wrapAsyncHandler } from "@batch-cooking/express-tools"; import { wrapAsyncHandler } from "@batch-cooking/express-tools";
import { ErrorCode, browseSourceSchema, createRecipeSchema } from "@batch-cooking/shared"; import { browseSourceSchema, createRecipeSchema, ErrorCode } from "@batch-cooking/shared";
import { Router } from "express"; import { Router } from "express";
import { type AuthLocals, requireAuth } from "../../middlewares/require-auth.js"; import { type AuthLocals, requireAuth } from "../../middlewares/require-auth.js";
import { browseSource, importSourceItem, previewSourceItem } from "./sources.service.js"; import { browseSource, importSourceItem, previewSourceItem } from "./sources.service.js";

View file

@ -12,11 +12,11 @@ import { prisma } from "../../db/prisma.js";
import { findImportedRecipeIds } from "../../db/recipe-source-sync.js"; import { findImportedRecipeIds } from "../../db/recipe-source-sync.js";
import { import {
type IngredientMatchEntry, type IngredientMatchEntry,
type UnitMatchEntry,
loadIngredientCatalog, loadIngredientCatalog,
loadUnitCatalog, loadUnitCatalog,
type UnitMatchEntry,
} from "../../lib/ingredient-matcher.js"; } from "../../lib/ingredient-matcher.js";
import { type RecipeSourceAdapter, markAlreadyImported } from "../../lib/recipe-source-adapter.js"; import { markAlreadyImported, type RecipeSourceAdapter } from "../../lib/recipe-source-adapter.js";
import { RecipeSourceError } from "../../lib/recipe-source-errors.js"; import { RecipeSourceError } from "../../lib/recipe-source-errors.js";
import { getRecipeSource } from "../../lib/recipe-source-registry.js"; import { getRecipeSource } from "../../lib/recipe-source-registry.js";
import { import {

View file

@ -2,13 +2,13 @@ import { INGREDIENT_LABEL_SYNONYMS_EN } from "@batch-cooking/shared";
import { expect } from "chai"; import { expect } from "chai";
import { prisma } from "../src/db/prisma.js"; import { prisma } from "../src/db/prisma.js";
import { import {
type IngredientMatchEntry,
type UnitMatchEntry,
extractQuantity, extractQuantity,
type IngredientMatchEntry,
loadIngredientCatalog, loadIngredientCatalog,
loadUnitCatalog, loadUnitCatalog,
matchIngredientName, matchIngredientName,
matchUnit, matchUnit,
type UnitMatchEntry,
} from "../src/lib/ingredient-matcher.js"; } from "../src/lib/ingredient-matcher.js";
import { resetDatabase } from "../test-support/reset-db.js"; import { resetDatabase } from "../test-support/reset-db.js";

View file

@ -3,12 +3,12 @@ import { prisma } from "../src/db/prisma.js";
import type { IngredientMatchEntry, UnitMatchEntry } from "../src/lib/ingredient-matcher.js"; import type { IngredientMatchEntry, UnitMatchEntry } from "../src/lib/ingredient-matcher.js";
import type { ParsedRecipe, ParsedRecipeIngredient } from "../src/lib/recipe-source-adapter.js"; import type { ParsedRecipe, ParsedRecipeIngredient } from "../src/lib/recipe-source-adapter.js";
import { import {
type TranslatedRecipeIngredient,
type UnitConversionEntry,
mergeDuplicateIngredients, mergeDuplicateIngredients,
type TranslatedRecipeIngredient,
translateRecipe, translateRecipe,
translateRecipeIngredients, translateRecipeIngredients,
translateRecipeSteps, translateRecipeSteps,
type UnitConversionEntry,
} from "../src/lib/recipe-translation.js"; } from "../src/lib/recipe-translation.js";
import type { TechStepMappingRule } from "../src/lib/tech-step-matcher.js"; import type { TechStepMappingRule } from "../src/lib/tech-step-matcher.js";
import { resetDatabase } from "../test-support/reset-db.js"; import { resetDatabase } from "../test-support/reset-db.js";

View file

@ -773,8 +773,8 @@ describe("Recipes", () => {
const tomate = await ingredientId("tomato"); const tomate = await ingredientId("tomato");
const piece = await unitId("piece"); const piece = await unitId("piece");
const chop = await techStepId("chop"); const chop = await techStepId("chop");
const mince = await techStepId("mince"); const _mince = await techStepId("mince");
const melt = await techStepId("melt"); const _melt = await techStepId("melt");
const simmer = await techStepId("simmer"); const simmer = await techStepId("simmer");
const bake = await techStepId("bake"); const bake = await techStepId("bake");
@ -886,7 +886,7 @@ describe("Recipes", () => {
}); });
it("rejects an edit from anyone other than the recipe's author with 403 NOT_RECIPE_AUTHOR", async () => { it("rejects an edit from anyone other than the recipe's author with 403 NOT_RECIPE_AUTHOR", async () => {
const { agent, profileId } = await signup(); const { profileId } = await signup();
const { agent: otherAgent } = await signup(); const { agent: otherAgent } = await signup();
const tomate = await ingredientId("tomato"); const tomate = await ingredientId("tomato");
const piece = await unitId("piece"); const piece = await unitId("piece");

View file

@ -1,11 +1,11 @@
import { expect } from "chai"; import { expect } from "chai";
import { prisma } from "../src/db/prisma.js"; import { prisma } from "../src/db/prisma.js";
import { import {
type TechStepMappingRule,
loadTechStepMappingRules, loadTechStepMappingRules,
matchTechStepSpans, matchTechStepSpans,
matchTechSteps, matchTechSteps,
normalizeText, normalizeText,
type TechStepMappingRule,
} from "../src/lib/tech-step-matcher.js"; } from "../src/lib/tech-step-matcher.js";
import { resetDatabase } from "../test-support/reset-db.js"; import { resetDatabase } from "../test-support/reset-db.js";

View file

@ -16,7 +16,7 @@ const authenticatedProfile = {
}; };
const vegetarien = { id: 1, key: "vegetarian" }; const vegetarien = { id: 1, key: "vegetarian" };
const gluten = { id: 1, key: "gluten", kind: "INTOLERANCE" }; const _gluten = { id: 1, key: "gluten", kind: "INTOLERANCE" };
const oeufs = { id: 2, key: "eggs", kind: "ALLERGY" }; const oeufs = { id: 2, key: "eggs", kind: "ALLERGY" };
const ratatouille = { const ratatouille = {

View file

@ -4,15 +4,15 @@ import { RequireAuth } from "./features/auth/RequireAuth";
import { AppLayout } from "./layouts/AppLayout"; import { AppLayout } from "./layouts/AppLayout";
import { ImportRecipePage } from "./pages/ImportRecipePage"; import { ImportRecipePage } from "./pages/ImportRecipePage";
import { LoginPage } from "./pages/LoginPage"; import { LoginPage } from "./pages/LoginPage";
import { OnboardingAllergensPage } from "./pages/onboarding/OnboardingAllergensPage";
import { OnboardingDietPage } from "./pages/onboarding/OnboardingDietPage";
import { OnboardingHouseholdPage } from "./pages/onboarding/OnboardingHouseholdPage";
import { OnboardingSourcesPage } from "./pages/onboarding/OnboardingSourcesPage";
import { PlanningPage } from "./pages/PlanningPage"; import { PlanningPage } from "./pages/PlanningPage";
import { RecipeFormPage } from "./pages/RecipeFormPage"; import { RecipeFormPage } from "./pages/RecipeFormPage";
import { RecipesPage } from "./pages/RecipesPage"; import { RecipesPage } from "./pages/RecipesPage";
import { ShoppingListPage } from "./pages/ShoppingListPage"; import { ShoppingListPage } from "./pages/ShoppingListPage";
import { SignupPage } from "./pages/SignupPage"; import { SignupPage } from "./pages/SignupPage";
import { OnboardingAllergensPage } from "./pages/onboarding/OnboardingAllergensPage";
import { OnboardingDietPage } from "./pages/onboarding/OnboardingDietPage";
import { OnboardingHouseholdPage } from "./pages/onboarding/OnboardingHouseholdPage";
import { OnboardingSourcesPage } from "./pages/onboarding/OnboardingSourcesPage";
import { AccountSettingsPage } from "./pages/settings/AccountSettingsPage"; import { AccountSettingsPage } from "./pages/settings/AccountSettingsPage";
import { CreditsPage } from "./pages/settings/CreditsPage"; import { CreditsPage } from "./pages/settings/CreditsPage";
import { HouseholdSettingsPage } from "./pages/settings/HouseholdSettingsPage"; import { HouseholdSettingsPage } from "./pages/settings/HouseholdSettingsPage";

View file

@ -1,4 +1,4 @@
import { type ReactElement, cloneElement, useId } from "react"; import { cloneElement, type ReactElement, useId } from "react";
import "./tooltip.scss"; import "./tooltip.scss";
/** /**

View file

@ -1,5 +1,5 @@
import type { LoginInput, SafeUserProfile, SignupInput } from "@batch-cooking/shared"; import type { LoginInput, SafeUserProfile, SignupInput } from "@batch-cooking/shared";
import { type ReactNode, createContext, useCallback, useContext, useEffect, useState } from "react"; import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from "react";
import { apiClient } from "../../api/client"; import { apiClient } from "../../api/client";
/** Shape of the auth state/actions exposed via {@link useAuth}. */ /** Shape of the auth state/actions exposed via {@link useAuth}. */

View file

@ -22,10 +22,10 @@ import { RecipeImportForm } from "../recipes/RecipeImportForm";
import { RecipeSourcesPanel, type SourceItemSelection } from "../recipes/RecipeSourcesPanel"; import { RecipeSourcesPanel, type SourceItemSelection } from "../recipes/RecipeSourcesPanel";
import { RecipeTable } from "../recipes/RecipeTable"; import { RecipeTable } from "../recipes/RecipeTable";
import { import {
RecipeTabs,
type RecipesPageTab,
isSourceTab, isSourceTab,
parseSourceTabValue, parseSourceTabValue,
type RecipesPageTab,
RecipeTabs,
} from "../recipes/RecipeTabs"; } from "../recipes/RecipeTabs";
import { tryBuildCompleteImport } from "../recipes/recipe-import-draft"; import { tryBuildCompleteImport } from "../recipes/recipe-import-draft";
import { useEnabledSources } from "../recipes/useEnabledSources"; import { useEnabledSources } from "../recipes/useEnabledSources";
@ -313,7 +313,7 @@ export function RecipePickerDialog({
// for the identical failure — see this dialog's `onImported` handler // for the identical failure — see this dialog's `onImported` handler
// below). // below).
setIsConfirmingDraft(false); setIsConfirmingDraft(false);
navigate(`/recettes/${saved.id}`); void navigate(`/recettes/${saved.id}`);
} }
} }
@ -361,7 +361,7 @@ export function RecipePickerDialog({
// to this slot failed. Same fallback as the transparent- // to this slot failed. Same fallback as the transparent-
// import path above: land on its own page instead of // import path above: land on its own page instead of
// retrying. // retrying.
navigate(`/recettes/${recipe.id}`); void navigate(`/recettes/${recipe.id}`);
} }
}} }}
/> />

View file

@ -11,8 +11,8 @@ import { CheckboxOption } from "../../components/ui/Checkbox";
import { SettingsIcon } from "../../layouts/nav-icons"; import { SettingsIcon } from "../../layouts/nav-icons";
import { AllergenBadges } from "./AllergenBadges"; import { AllergenBadges } from "./AllergenBadges";
import { DietBadges } from "./DietBadges"; import { DietBadges } from "./DietBadges";
import { ReproducibleBadge } from "./ReproducibleBadge";
import { CategoryIcon, IngredientTypeIcon, SubcategoryIcon } from "./ingredient-icons"; import { CategoryIcon, IngredientTypeIcon, SubcategoryIcon } from "./ingredient-icons";
import { ReproducibleBadge } from "./ReproducibleBadge";
import "./recipes.scss"; import "./recipes.scss";
/** "No filter at this level" — a UI-only pseudo-value, never sent to/received from the API (see {@link INGREDIENT_CATEGORIES}/{@link INGREDIENT_CATEGORY_SUBCATEGORIES} for the real, closed sets). */ /** "No filter at this level" — a UI-only pseudo-value, never sent to/received from the API (see {@link INGREDIENT_CATEGORIES}/{@link INGREDIENT_CATEGORY_SUBCATEGORIES} for the real, closed sets). */

View file

@ -2,8 +2,8 @@ import type { IngredientView, UnitView } from "@batch-cooking/shared";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { AllergenBadges } from "./AllergenBadges"; import { AllergenBadges } from "./AllergenBadges";
import { DietBadges } from "./DietBadges"; import { DietBadges } from "./DietBadges";
import { ReproducibleBadge } from "./ReproducibleBadge";
import { IngredientTypeIcon } from "./ingredient-icons"; import { IngredientTypeIcon } from "./ingredient-icons";
import { ReproducibleBadge } from "./ReproducibleBadge";
import "./recipes.scss"; import "./recipes.scss";
/** One selected ingredient line in the recipe form — the ingredient itself (picked via `IngredientPicker`) plus its quantity/unit for this recipe. Quantity is kept as a raw string while editing (not parsed to a number until submit) so an in-progress/invalid value doesn't fight the input; `unit` picks from `unitsCatalog` (a closed reference list, see `Unit` in schema.prisma) rather than free text. */ /** One selected ingredient line in the recipe form — the ingredient itself (picked via `IngredientPicker`) plus its quantity/unit for this recipe. Quantity is kept as a raw string while editing (not parsed to a number until submit) so an in-progress/invalid value doesn't fight the input; `unit` picks from `unitsCatalog` (a closed reference list, see `Unit` in schema.prisma) rather than free text. */

View file

@ -135,6 +135,7 @@ export function RecipeDetailPanel({
<h3>{t("recipes.stepsTitle")}</h3> <h3>{t("recipes.stepsTitle")}</h3>
<ol className="recipe-detail-panel__steps"> <ol className="recipe-detail-panel__steps">
{draft.steps.map((step, index) => ( {draft.steps.map((step, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: DraftRecipeStepView (packages/shared) has no id — it's an unsaved source preview, nothing to key on yet — and this list is a static read-only render, never reordered/edited here.
<li key={`${index}-${step.description}`}> <li key={`${index}-${step.description}`}>
{step.picture && <img src={step.picture} alt="" />} {step.picture && <img src={step.picture} alt="" />}
<StepDescription description={step.description} techSteps={step.techSteps} /> <StepDescription description={step.description} techSteps={step.techSteps} />
@ -218,13 +219,7 @@ export function RecipeDetailPanel({
} }
/** Delete action with an inline two-step confirmation, same pattern as `HouseholdSettingsPage`'s danger zone. */ /** Delete action with an inline two-step confirmation, same pattern as `HouseholdSettingsPage`'s danger zone. */
function DeleteRecipeButton({ function DeleteRecipeButton({ recipeId, onDeleted }: { recipeId: number; onDeleted: () => void }) {
recipeId,
onDeleted,
}: {
recipeId: number;
onDeleted: () => void;
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [isConfirming, setIsConfirming] = useState(false); const [isConfirming, setIsConfirming] = useState(false);
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);

View file

@ -1,5 +1,6 @@
import { import {
type CreateRecipeInput, type CreateRecipeInput,
createRecipeSchema,
type DietView, type DietView,
ErrorCode, ErrorCode,
type IngredientView, type IngredientView,
@ -9,7 +10,6 @@ import {
type RecipeVisibility, type RecipeVisibility,
type UnitView, type UnitView,
type WeekDay, type WeekDay,
createRecipeSchema,
} from "@batch-cooking/shared"; } from "@batch-cooking/shared";
import { type FormEvent, useEffect, useState } from "react"; import { type FormEvent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";

View file

@ -1,7 +1,7 @@
import { import {
type CreateRecipeInput, type CreateRecipeInput,
type RecipeImportDraftView,
createRecipeSchema, createRecipeSchema,
type RecipeImportDraftView,
} from "@batch-cooking/shared"; } from "@batch-cooking/shared";
/** /**

View file

@ -1,5 +1,5 @@
import type { ThemePreference } from "@batch-cooking/shared"; import type { ThemePreference } from "@batch-cooking/shared";
import { type ReactNode, createContext, useCallback, useContext, useEffect, useState } from "react"; import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from "react";
import { apiClient } from "../../api/client"; import { apiClient } from "../../api/client";
import { useAuth } from "../auth/AuthContext"; import { useAuth } from "../auth/AuthContext";

View file

@ -195,7 +195,7 @@ function AccountMenu() {
async function handleLogout() { async function handleLogout() {
setIsOpen(false); setIsOpen(false);
await logout(); await logout();
navigate("/login"); void navigate("/login");
} }
const initial = user?.firstName?.charAt(0).toUpperCase() ?? ""; const initial = user?.firstName?.charAt(0).toUpperCase() ?? "";

View file

@ -13,18 +13,18 @@
// every consumer passes `aria-hidden="true"` itself (unlike the old custom // every consumer passes `aria-hidden="true"` itself (unlike the old custom
// `Icon` wrapper, lucide doesn't set this by default). // `Icon` wrapper, lucide doesn't set this by default).
export { export {
Calendar as PlanningIcon,
BookOpen as RecipesIcon, BookOpen as RecipesIcon,
ShoppingCart as ShoppingListIcon, Calendar as PlanningIcon,
Settings as SettingsIcon,
User as AccountIcon,
Leaf as DietPreferencesIcon,
Home as HouseholdIcon,
Palette as UserPreferencesIcon,
Info as CreditsIcon,
ChevronLeft as ChevronLeftIcon, ChevronLeft as ChevronLeftIcon,
Star as FavoriteIcon,
Globe as PublicIcon,
Rss as SourcesIcon,
ExternalLink as SourceLinkIcon, ExternalLink as SourceLinkIcon,
Globe as PublicIcon,
Home as HouseholdIcon,
Info as CreditsIcon,
Leaf as DietPreferencesIcon,
Palette as UserPreferencesIcon,
Rss as SourcesIcon,
Settings as SettingsIcon,
ShoppingCart as ShoppingListIcon,
Star as FavoriteIcon,
User as AccountIcon,
} from "lucide-react"; } from "lucide-react";

View file

@ -72,7 +72,7 @@ export function ImportRecipePage() {
externalId={externalId} externalId={externalId}
planningSlot={planningSlot ?? undefined} planningSlot={planningSlot ?? undefined}
onImported={({ recipe, planningItem }) => { onImported={({ recipe, planningItem }) => {
navigate(planningItem ? "/" : `/recettes/${recipe.id}`); void navigate(planningItem ? "/" : `/recettes/${recipe.id}`);
}} }}
/> />
</div> </div>

View file

@ -50,7 +50,7 @@ export function LoginPage() {
setIsSubmitting(true); setIsSubmitting(true);
try { try {
await login(result.data); await login(result.data);
navigate("/"); void navigate("/");
} catch (err) { } catch (err) {
// ApiError.code is looked up through ErrorMessageService so the // ApiError.code is looked up through ErrorMessageService so the
// label is centralized and localized — never display err.message // label is centralized and localized — never display err.message

View file

@ -1,7 +1,7 @@
import { import {
DateTime,
addWeeks, addWeeks,
buildCalendarMonth, buildCalendarMonth,
DateTime,
formatDateOnly, formatDateOnly,
getWeekStart, getWeekStart,
toDateOnly, toDateOnly,

View file

@ -1,11 +1,11 @@
import { import {
type CreateRecipeInput, type CreateRecipeInput,
createRecipeSchema,
type DietView, type DietView,
ErrorCode, ErrorCode,
type IngredientView, type IngredientView,
type RecipeVisibility, type RecipeVisibility,
type UnitView, type UnitView,
createRecipeSchema,
} from "@batch-cooking/shared"; } from "@batch-cooking/shared";
import { type FormEvent, useEffect, useState } from "react"; import { type FormEvent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -196,7 +196,7 @@ export function RecipeFormPage() {
recipeId !== null recipeId !== null
? await apiClient.updateRecipe(recipeId, result.data) ? await apiClient.updateRecipe(recipeId, result.data)
: await apiClient.createRecipe(result.data); : await apiClient.createRecipe(result.data);
navigate(`/recettes/${saved.id}`); void navigate(`/recettes/${saved.id}`);
} catch (err) { } catch (err) {
const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR; const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR;
setFormError(errorMessageService.getLabel(code)); setFormError(errorMessageService.getLabel(code));

View file

@ -10,10 +10,10 @@ import {
} from "../features/recipes/RecipeSourcesPanel"; } from "../features/recipes/RecipeSourcesPanel";
import { RecipeTable } from "../features/recipes/RecipeTable"; import { RecipeTable } from "../features/recipes/RecipeTable";
import { import {
RecipeTabs,
type RecipesPageTab,
isSourceTab, isSourceTab,
parseSourceTabValue, parseSourceTabValue,
type RecipesPageTab,
RecipeTabs,
sourceTabValue, sourceTabValue,
} from "../features/recipes/RecipeTabs"; } from "../features/recipes/RecipeTabs";
import { useEnabledSources } from "../features/recipes/useEnabledSources"; import { useEnabledSources } from "../features/recipes/useEnabledSources";
@ -164,7 +164,7 @@ export function RecipesPage() {
/** After a delete, the removed recipe can no longer be selected, and the table must drop it too. */ /** After a delete, the removed recipe can no longer be selected, and the table must drop it too. */
function handleDeleted(recipeId: number) { function handleDeleted(recipeId: number) {
navigate("/recettes"); void navigate("/recettes");
setListState((prev) => setListState((prev) =>
prev.status === "loaded" prev.status === "loaded"
? { status: "loaded", recipes: prev.recipes.filter((r) => r.id !== recipeId) } ? { status: "loaded", recipes: prev.recipes.filter((r) => r.id !== recipeId) }
@ -219,7 +219,7 @@ export function RecipesPage() {
} }
onSelectImportedRecipe={(recipe) => { onSelectImportedRecipe={(recipe) => {
setActiveTab("favoris"); setActiveTab("favoris");
navigate(`/recettes/${recipe.id}`); void navigate(`/recettes/${recipe.id}`);
}} }}
/> />
) : ( ) : (

View file

@ -52,7 +52,7 @@ export function SignupPage() {
setIsSubmitting(true); setIsSubmitting(true);
try { try {
await signup(result.data); await signup(result.data);
navigate("/onboarding/regime"); void navigate("/onboarding/regime");
} catch (err) { } catch (err) {
// ApiError.code is looked up through ErrorMessageService so the // ApiError.code is looked up through ErrorMessageService so the
// label is centralized and localized — never display err.message // label is centralized and localized — never display err.message

View file

@ -42,6 +42,14 @@ export function OnboardingAllergensPage() {
setAllergies(allergiesResult); setAllergies(allergiesResult);
setTotalSteps(house !== null ? 4 : 3); setTotalSteps(house !== null ? 4 : 3);
}) })
.catch(() => {
// Neither piece is essential for this last step to render — the
// allergy list just stays empty (nothing to pick from) and the
// step count keeps its 3-of-3 default, same "don't strand the
// visitor over a transient failure" reasoning as
// OnboardingSourcesPage's own catch, just with nothing to
// navigate away to since this already is the last step.
})
.finally(() => { .finally(() => {
if (!cancelled) setIsLoading(false); if (!cancelled) setIsLoading(false);
}); });
@ -57,7 +65,7 @@ export function OnboardingAllergensPage() {
setIsSubmitting(true); setIsSubmitting(true);
try { try {
await apiClient.updateAllergyIds(allergyIds); await apiClient.updateAllergyIds(allergyIds);
navigate("/"); void navigate("/");
} catch (err) { } catch (err) {
const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR; const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR;
setFormError(errorMessageService.getLabel(code)); setFormError(errorMessageService.getLabel(code));

View file

@ -34,6 +34,11 @@ export function OnboardingDietPage() {
.then((result) => { .then((result) => {
if (!cancelled) setDiets(result); if (!cancelled) setDiets(result);
}) })
// Not essential for this step to render — the diet list just stays
// empty (nothing to pick, "Aucun régime particulier" still works)
// rather than stranding the visitor over a transient failure, same
// reasoning as OnboardingSourcesPage's own catch.
.catch(() => {})
.finally(() => { .finally(() => {
if (!cancelled) setIsLoading(false); if (!cancelled) setIsLoading(false);
}); });
@ -48,7 +53,7 @@ export function OnboardingDietPage() {
setIsSubmitting(true); setIsSubmitting(true);
try { try {
await apiClient.updateDiet(dietId); await apiClient.updateDiet(dietId);
navigate("/onboarding/foyer"); void navigate("/onboarding/foyer");
} catch (err) { } catch (err) {
const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR; const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR;
setFormError(errorMessageService.getLabel(code)); setFormError(errorMessageService.getLabel(code));

View file

@ -36,6 +36,11 @@ export function OnboardingHouseholdPage() {
.then((result) => { .then((result) => {
if (!cancelled) setHouse(result); if (!cancelled) setHouse(result);
}) })
// Not essential for this step to render — `house` just stays `null`
// (same as a genuinely household-less profile) rather than
// stranding the visitor over a transient failure, same reasoning as
// OnboardingSourcesPage's own catch.
.catch(() => {})
.finally(() => { .finally(() => {
if (!cancelled) setIsLoading(false); if (!cancelled) setIsLoading(false);
}); });
@ -51,7 +56,7 @@ export function OnboardingHouseholdPage() {
* configure sources for without a household). * configure sources for without a household).
*/ */
function goToNextStep(hasHousehold: boolean) { function goToNextStep(hasHousehold: boolean) {
navigate(hasHousehold ? "/onboarding/sources" : "/onboarding/allergenes"); void navigate(hasHousehold ? "/onboarding/sources" : "/onboarding/allergenes");
} }
return ( return (

View file

@ -40,7 +40,7 @@ export function OnboardingSourcesPage() {
.then((result) => { .then((result) => {
if (cancelled) return; if (cancelled) return;
if (result.length === 0) { if (result.length === 0) {
navigate("/onboarding/allergenes", { replace: true }); void navigate("/onboarding/allergenes", { replace: true });
return; return;
} }
setSources(result); setSources(result);
@ -50,7 +50,7 @@ export function OnboardingSourcesPage() {
// Nothing to configure sources for if we can't even list them — the // Nothing to configure sources for if we can't even list them — the
// wizard shouldn't strand the visitor here over a transient failure // wizard shouldn't strand the visitor here over a transient failure
// fetching an optional step's own data. // fetching an optional step's own data.
if (!cancelled) navigate("/onboarding/allergenes", { replace: true }); if (!cancelled) void navigate("/onboarding/allergenes", { replace: true });
}); });
return () => { return () => {
cancelled = true; cancelled = true;
@ -63,7 +63,7 @@ export function OnboardingSourcesPage() {
setIsSubmitting(true); setIsSubmitting(true);
try { try {
await apiClient.updateHouseSourceIds(sourceIds); await apiClient.updateHouseSourceIds(sourceIds);
navigate("/onboarding/allergenes"); void navigate("/onboarding/allergenes");
} catch (err) { } catch (err) {
const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR; const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR;
setFormError(errorMessageService.getLabel(code)); setFormError(errorMessageService.getLabel(code));

View file

@ -30,7 +30,7 @@ export function AccountSettingsPage() {
setIsSubmitting(true); setIsSubmitting(true);
try { try {
await deleteAccount(password); await deleteAccount(password);
navigate("/login"); void navigate("/login");
} catch (err) { } catch (err) {
const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR; const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR;
setError(errorMessageService.getLabel(code)); setError(errorMessageService.getLabel(code));

View file

@ -1,10 +1,10 @@
import { import {
ErrorCode, ErrorCode,
type HouseView, type HouseView,
type SourceView,
renameHouseSchema, renameHouseSchema,
type SourceView,
} from "@batch-cooking/shared"; } from "@batch-cooking/shared";
import { type FormEvent, useEffect, useRef, useState } from "react"; import { type FormEvent, useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ApiError, apiClient } from "../../api/client"; import { ApiError, apiClient } from "../../api/client";
import { useAuth } from "../../features/auth/AuthContext"; import { useAuth } from "../../features/auth/AuthContext";
@ -48,11 +48,15 @@ export function HouseholdSettingsPage() {
const [loadError, setLoadError] = useState(false); const [loadError, setLoadError] = useState(false);
const [house, setHouse] = useState<HouseView | null>(null); const [house, setHouse] = useState<HouseView | null>(null);
useEffect(() => { // Stable across renders (empty deps — only closes over setters, which
loadHouse(); // React itself guarantees are stable) so the effect below only re-runs on
}, []); // mount, not on every render. It used to be a plain function declaration,
// a fresh reference every render — its `useEffect` dependency on it then
function loadHouse() { // re-triggered the effect (and thus the fetch, and thus a state update,
// and thus another render...) forever. `loadHouse` is also handed to
// `NoHousehold`/`HasHousehold` below as their `onChanged` callback, so it
// has to stay this stable regardless of the effect.
const loadHouse = useCallback(() => {
setIsLoading(true); setIsLoading(true);
setLoadError(false); setLoadError(false);
return apiClient return apiClient
@ -60,7 +64,11 @@ export function HouseholdSettingsPage() {
.then((result) => setHouse(result)) .then((result) => setHouse(result))
.catch(() => setLoadError(true)) .catch(() => setLoadError(true))
.finally(() => setIsLoading(false)); .finally(() => setIsLoading(false));
} }, []);
useEffect(() => {
void loadHouse();
}, [loadHouse]);
if (isLoading) { if (isLoading) {
return ( return (
@ -300,6 +308,11 @@ function SourcesSection() {
setSources(sourcesResult); setSources(sourcesResult);
setSourceIds(sourceIdsResult); setSourceIds(sourceIdsResult);
}) })
// Not essential for the rest of the page to render — the sources
// list just stays empty (nothing to toggle) rather than stranding
// the visitor over a transient failure, same reasoning as
// OnboardingSourcesPage's own catch.
.catch(() => {})
.finally(() => { .finally(() => {
if (!cancelled) setIsLoading(false); if (!cancelled) setIsLoading(false);
}); });
@ -368,13 +381,7 @@ function InviteCode({ code }: { code: string }) {
} }
/** Admin-only button removing one specific member from the household. */ /** Admin-only button removing one specific member from the household. */
function RemoveMemberButton({ function RemoveMemberButton({ memberId, onChanged }: { memberId: number; onChanged: () => void }) {
memberId,
onChanged,
}: {
memberId: number;
onChanged: () => void;
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [isRemoving, setIsRemoving] = useState(false); const [isRemoving, setIsRemoving] = useState(false);

View file

@ -1,5 +1,5 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", "$schema": "https://biomejs.dev/schemas/2.5.9/schema.json",
"vcs": { "vcs": {
"enabled": true, "enabled": true,
"clientKind": "git", "clientKind": "git",
@ -7,13 +7,14 @@
}, },
"files": { "files": {
"ignoreUnknown": false, "ignoreUnknown": false,
"ignore": [ "includes": [
"dist", "**",
"coverage", "!**/dist",
"**/node_modules", "!**/coverage",
".pnpm-store", "!**/node_modules",
"cypress/videos", "!**/.pnpm-store",
"cypress/screenshots" "!**/cypress/videos",
"!**/cypress/screenshots"
] ]
}, },
"formatter": { "formatter": {
@ -22,13 +23,21 @@
"indentWidth": 2, "indentWidth": 2,
"lineWidth": 100 "lineWidth": 100
}, },
"organizeImports": { "assist": { "actions": { "source": { "organizeImports": "on" } } },
"enabled": true
},
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
"recommended": true "preset": "recommended",
"nursery": {
"noFloatingPromises": "error"
},
"suspicious": {
"noConsole": {
"level": "error",
"options": { "allow": ["error", "warn", "info", "debug", "table", "assert"] }
},
"noExplicitAny": "error"
}
} }
}, },
"javascript": { "javascript": {

View file

@ -16,7 +16,7 @@
"dev:web": "pnpm --filter web dev" "dev:web": "pnpm --filter web dev"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.4", "@biomejs/biome": "^2.5.9",
"typescript": "^5.7.2" "typescript": "^5.7.2"
}, },
"pnpm": { "pnpm": {

View file

@ -4,9 +4,8 @@
// this package rather than hand-rolled `Date` arithmetic or a second, // this package rather than hand-rolled `Date` arithmetic or a second,
// differently-behaved date library creeping into one side only. // differently-behaved date library creeping into one side only.
export * from "./date-only.js";
export * from "./week.js";
// Re-exported so a consumer never needs its own direct `luxon` dependency // Re-exported so a consumer never needs its own direct `luxon` dependency
// just to type a `DateTime` value passed to/from this package's functions. // just to type a `DateTime` value passed to/from this package's functions.
export { DateTime } from "luxon"; export { DateTime } from "luxon";
export * from "./date-only.js";
export * from "./week.js";

View file

@ -9,8 +9,8 @@ importers:
.: .:
devDependencies: devDependencies:
'@biomejs/biome': '@biomejs/biome':
specifier: ^1.9.4 specifier: ^2.5.9
version: 1.9.4 version: 2.5.9
typescript: typescript:
specifier: ^5.7.2 specifier: ^5.7.2
version: 5.9.3 version: 5.9.3
@ -347,55 +347,55 @@ packages:
peerDependencies: peerDependencies:
esbuild: '>=0.17.0' esbuild: '>=0.17.0'
'@biomejs/biome@1.9.4': '@biomejs/biome@2.5.9':
resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==, tarball: https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz} resolution: {integrity: sha512-KkgCvdHB4IhtpHpF564plA9jo6fDOwWGQ/3jvreLzgOtRLEDoPqr7QO9qejNA8jKwDsSkAKr77hqBHnyUbIw4g==, tarball: https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.9.tgz}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
hasBin: true hasBin: true
'@biomejs/cli-darwin-arm64@1.9.4': '@biomejs/cli-darwin-arm64@2.5.9':
resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==, tarball: https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz} resolution: {integrity: sha512-am22pX2aBqznqq1eMyIj/bZ++riF3Lk6ct7cbv+gQK0csFhr+d8O0RkOi2FF2qSgFgANbqNkIZ0/PxlnW2pLFg==, tarball: https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.9.tgz}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@biomejs/cli-darwin-x64@1.9.4': '@biomejs/cli-darwin-x64@2.5.9':
resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==, tarball: https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz} resolution: {integrity: sha512-l44KWDHLDvEnD0N/XcrVs7VXb3A18xL7QS3WB0eL93wbmk529ffIG55vleGCqaunpRUjLrdnjK05Qki1dsjylg==, tarball: https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.9.tgz}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@biomejs/cli-linux-arm64-musl@1.9.4': '@biomejs/cli-linux-arm64-musl@2.5.9':
resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==, tarball: https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz} resolution: {integrity: sha512-7ImVPwBLCtkmpR5esd8RHhTqW94f0JLJQum6AneYcy94jRm18TaPPm7slaigGzFhfgt3QiD1Vj52LKmBAnKizA==, tarball: https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.9.tgz}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@biomejs/cli-linux-arm64@1.9.4': '@biomejs/cli-linux-arm64@2.5.9':
resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==, tarball: https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz} resolution: {integrity: sha512-ICaK+IYaVZvKbBxX2rwrPT0DdUDMnE9Vm3nQGe+mltQPmUg19pONzkPWGdY4FCsoreDETWDynvdt4ysCbF5gNQ==, tarball: https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.9.tgz}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@biomejs/cli-linux-x64-musl@1.9.4': '@biomejs/cli-linux-x64-musl@2.5.9':
resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==, tarball: https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz} resolution: {integrity: sha512-RXGaD0o1/pTTguYw1aeDJh9ad6Lfrui0fI7mBderTyGr7WuUJkBIttgLkR3XJyoxOkkgfBDspaUT8wXArTqLZw==, tarball: https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.9.tgz}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@biomejs/cli-linux-x64@1.9.4': '@biomejs/cli-linux-x64@2.5.9':
resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==, tarball: https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz} resolution: {integrity: sha512-z22Q/zFYSvbIJfW1CbfZPu4X8PddS6Qd2ORbc6h+aT6EcwAxUF3m6fA4HjNvA3TU4X0dTJRwNPB165ES3PJXzg==, tarball: https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.9.tgz}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@biomejs/cli-win32-arm64@1.9.4': '@biomejs/cli-win32-arm64@2.5.9':
resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==, tarball: https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz} resolution: {integrity: sha512-nHK+/HHC+D0ogAHUxomgoSTdjImb6fmNNVTKmf0tyu4eDL1DqPKIHc+i+UL8+b0RnAu8224qo8F2tCVnaT0A3w==, tarball: https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.9.tgz}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@biomejs/cli-win32-x64@1.9.4': '@biomejs/cli-win32-x64@2.5.9':
resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==, tarball: https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz} resolution: {integrity: sha512-Yiq0H56LjXSSw/hd9YkXgSLQfzyDJzbzU2TezozxyNw+uKWAqOtqGVvBfzKRRDiaFF5avGAhHdWKx7LtDOShUw==, tarball: https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.9.tgz}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@ -3963,39 +3963,39 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@biomejs/biome@1.9.4': '@biomejs/biome@2.5.9':
optionalDependencies: optionalDependencies:
'@biomejs/cli-darwin-arm64': 1.9.4 '@biomejs/cli-darwin-arm64': 2.5.9
'@biomejs/cli-darwin-x64': 1.9.4 '@biomejs/cli-darwin-x64': 2.5.9
'@biomejs/cli-linux-arm64': 1.9.4 '@biomejs/cli-linux-arm64': 2.5.9
'@biomejs/cli-linux-arm64-musl': 1.9.4 '@biomejs/cli-linux-arm64-musl': 2.5.9
'@biomejs/cli-linux-x64': 1.9.4 '@biomejs/cli-linux-x64': 2.5.9
'@biomejs/cli-linux-x64-musl': 1.9.4 '@biomejs/cli-linux-x64-musl': 2.5.9
'@biomejs/cli-win32-arm64': 1.9.4 '@biomejs/cli-win32-arm64': 2.5.9
'@biomejs/cli-win32-x64': 1.9.4 '@biomejs/cli-win32-x64': 2.5.9
'@biomejs/cli-darwin-arm64@1.9.4': '@biomejs/cli-darwin-arm64@2.5.9':
optional: true optional: true
'@biomejs/cli-darwin-x64@1.9.4': '@biomejs/cli-darwin-x64@2.5.9':
optional: true optional: true
'@biomejs/cli-linux-arm64-musl@1.9.4': '@biomejs/cli-linux-arm64-musl@2.5.9':
optional: true optional: true
'@biomejs/cli-linux-arm64@1.9.4': '@biomejs/cli-linux-arm64@2.5.9':
optional: true optional: true
'@biomejs/cli-linux-x64-musl@1.9.4': '@biomejs/cli-linux-x64-musl@2.5.9':
optional: true optional: true
'@biomejs/cli-linux-x64@1.9.4': '@biomejs/cli-linux-x64@2.5.9':
optional: true optional: true
'@biomejs/cli-win32-arm64@1.9.4': '@biomejs/cli-win32-arm64@2.5.9':
optional: true optional: true
'@biomejs/cli-win32-x64@1.9.4': '@biomejs/cli-win32-x64@2.5.9':
optional: true optional: true
'@colors/colors@1.5.0': '@colors/colors@1.5.0':