From b2a96dd0482b82fad0dc558a39c2dd5679eb16c4 Mon Sep 17 00:00:00 2001 From: u8array Date: Fri, 16 Oct 2026 02:00:55 +0200 Subject: [PATCH] chore(lint): the compiler-bailout fixes read like the rest of their files --- eslint.config.js | 4 ++-- src/components/Canvas/LabelCanvas.tsx | 8 ++++++-- src/components/Canvas/hooks/useKonvaTransformer.ts | 2 +- src/hooks/useMcpBridge.ts | 7 +++---- src/hooks/useNativeMenu.ts | 12 ++++++------ src/lib/tryFinally.ts | 4 ++-- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 2169cafb..91258da0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -26,12 +26,12 @@ export default defineConfig([ '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }], // Omit-via-rest (`const { drop, ...keep } = obj`) is the intended pattern. '@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }], - // Syntax the compiler cannot lower, so the whole hook silently loses its memoization. + // Syntax the compiler cannot lower, so the enclosing component silently loses its memoization. 'react-hooks/todo': 'error', }, }, { - files: ['**/*.test.ts', '**/*.test.tsx', 'src/test/**', 'tests/**/*.ts', 'e2e/**/*.ts'], + files: ['**/*.test.ts', '**/*.test.tsx', 'src/test/**', 'tests/**/*.ts'], rules: { '@typescript-eslint/no-non-null-assertion': 'off', }, diff --git a/src/components/Canvas/LabelCanvas.tsx b/src/components/Canvas/LabelCanvas.tsx index bed345c3..7948e0b7 100644 --- a/src/components/Canvas/LabelCanvas.tsx +++ b/src/components/Canvas/LabelCanvas.tsx @@ -1,4 +1,3 @@ -import { tryFinally } from "../../lib/tryFinally"; import { forwardRef, useImperativeHandle, @@ -36,6 +35,7 @@ import { registerBarcodeWidthProber, unregisterBarcodeWidthProber, type ProbeCtx import { resolveForMeasure } from "@zplab/core/lib/barcodeDims"; import { measureBarcodeFootprintDots } from "./bwipHelpers"; import { copyText } from "../../lib/clipboard"; +import { tryFinally } from "../../lib/tryFinally"; import { selectTidyTargets } from "../../lib/tidyClassify"; import { safeAreaRectDots } from "../../lib/safeArea"; import { measuredBoundsMap, subscribeMeasuredBounds, getMeasuredSnapshot } from "../../lib/measuredBoundsCache"; @@ -1828,7 +1828,11 @@ export const LabelCanvas = forwardRef(function LabelCa }} onTransform={onTransform} boundBoxFunc={boundBoxFunc} - onTransformEnd={() => tryFinally(onTransformEnd, () => { transformActiveRef.current = false; })} + onTransformEnd={() => + tryFinally(onTransformEnd, () => { + transformActiveRef.current = false; + }) + } borderStroke={colors.selection} anchorStroke={colors.selection} anchorFill="#ffffff" diff --git a/src/components/Canvas/hooks/useKonvaTransformer.ts b/src/components/Canvas/hooks/useKonvaTransformer.ts index ddb345d3..bc5bd796 100644 --- a/src/components/Canvas/hooks/useKonvaTransformer.ts +++ b/src/components/Canvas/hooks/useKonvaTransformer.ts @@ -1,5 +1,4 @@ import { editBounds, moduleScaledProps, wireBounds, type Range } from "@zplab/core/types/propSpec"; -import { tryFinally } from "../../../lib/tryFinally"; import { useRef, useEffect, useLayoutEffect } from "react"; import { flushSync } from "react-dom"; import type Konva from "konva"; @@ -49,6 +48,7 @@ import { projectedAnchorXDots, projectMultiResize } from "../../../lib/multiResi import { lineHandlesNodeId, lineRootNodeId } from "../konvaObjectProps"; import { isAxisSwapped, objectRotation } from "@zplab/core/registry/rotation"; import { getMeasuredSnapshot } from "../../../lib/measuredBoundsCache"; +import { tryFinally } from "../../../lib/tryFinally"; import { computeResizeSnap, deriveActiveEdges, diff --git a/src/hooks/useMcpBridge.ts b/src/hooks/useMcpBridge.ts index 8c924a93..039a20f3 100644 --- a/src/hooks/useMcpBridge.ts +++ b/src/hooks/useMcpBridge.ts @@ -13,9 +13,8 @@ import { } from "../lib/mcpServer"; import { isDesktopShell } from "../lib/platform"; -// Module level, since the compiler cannot lower an import expression inside the hook. -const tauriEvent = () => import("@tauri-apps/api/event"); -const tauriCore = () => import("@tauri-apps/api/core"); +// Hoisted: an import expression inside the hook is a compiler bailout. +const loadTauri = () => Promise.all([import("@tauri-apps/api/event"), import("@tauri-apps/api/core")]); /** Register the sidecar listeners, then have Rust flush the events it * queued while none existed (see event_buffer in mcp.rs). */ @@ -28,7 +27,7 @@ export function useMcpBridge(): void { // teardown withdraws its own and never the one that replaced it. let attachedSession: string | null = null; void (async () => { - const [{ listen }, { invoke }] = await Promise.all([tauriEvent(), tauriCore()]); + const [{ listen }, { invoke }] = await loadTauri(); const fns = await Promise.all([ listen<[number, string]>("mcp://open-draft", (e) => { void respondToOpenDraft(e.payload[0], e.payload[1]).catch(() => undefined); diff --git a/src/hooks/useNativeMenu.ts b/src/hooks/useNativeMenu.ts index d8d12c16..587a1706 100644 --- a/src/hooks/useNativeMenu.ts +++ b/src/hooks/useNativeMenu.ts @@ -92,9 +92,9 @@ interface InstalledMenu { let latest: MenuData | null = null; let installed: InstalledMenu | null = null; let updateGen = 0; +// Hoisted: ++ on a module variable inside the hook is a compiler bailout. +const bumpUpdateGen = (): number => ++updateGen; let queue: Promise = Promise.resolve(); -// A function, since the compiler cannot lower ++ on a module variable inside the hook. -const nextUpdateGen = (): number => ++updateGen; /** The OS toggles a clicked CheckMenuItem's checkmark itself; after a jump we * re-assert every step's checkmark so that stray toggle can't linger. */ let checkmarkDirty = false; @@ -106,7 +106,7 @@ if (import.meta.hot) { import.meta.hot.dispose(() => { // Invalidate queued rebuilds and drain the in-flight update before closing, // so a mid-await patch/rebuild can't touch freed handles. - nextUpdateGen(); + bumpUpdateGen(); queue = queue.then(() => { if (installed) closeAll(installed.resources); installed = null; @@ -386,7 +386,7 @@ async function patchMenu(inst: InstalledMenu, d: MenuData): Promise { // for the next unrelated signature change. if (installed) closeAll(installed.resources); installed = null; - const gen = nextUpdateGen(); + const gen = bumpUpdateGen(); queue = queue.then(() => runUpdate(gen)).catch(() => undefined); } } @@ -436,12 +436,12 @@ export function useNativeMenu( useEffect(() => { if (!isDesktopShell) return; - const gen = nextUpdateGen(); + const gen = bumpUpdateGen(); queue = queue.then(() => runUpdate(gen)).catch(() => undefined); // Invalidate a queued-but-unstarted update on re-run/unmount (StrictMode // remounts included); the re-run queues a fresh one. return () => { - nextUpdateGen(); + bumpUpdateGen(); }; }, [signature]); } diff --git a/src/lib/tryFinally.ts b/src/lib/tryFinally.ts index c7b66d45..2186447f 100644 --- a/src/lib/tryFinally.ts +++ b/src/lib/tryFinally.ts @@ -1,5 +1,5 @@ -/** The React Compiler cannot lower a try/finally inside a hook, so the finally lives here. - * `body` is a closure, so a return inside it leaves only the closure. */ +/** Keeps the finally out of a component or hook, where the React Compiler cannot lower it. + * A `return` inside `body` leaves `body`, not the caller. */ export function tryFinally(body: () => void, cleanup: () => void): void { try { body();