@@ -1381,6 +1381,187 @@ this was a pure type-annotation change with no logic touched).
13811381both targeted manual tests — the stale-image retry loop and general text-
13821382element/link/back-of-card parsing — succeeded with no issues; committed.
13831383
1384+ ## Related, separate effort: ` @excalidraw/common ` type-resolution fix
1385+
1386+ Started 2026-08-14 (session 3), on branch ` excalidraw-type-import-fix ` .
1387+ Directly grew out of the ` excalidrawAutomateUtils.ts ` triage above, whose
1388+ "flagged, needs its own investigation" finding turned out to be the single
1389+ largest lint win of the whole ` no-unsafe-* ` effort by a wide margin.
1390+
1391+ ** Root cause (confirmed against both this repo and upstream, no fork changes
1392+ needed):** ` @zsviczian/excalidraw ` 's bundled ` .d.ts ` files import bare-specifier
1393+ ` @excalidraw/common ` , ` @excalidraw/common/utility-types ` , ` @excalidraw/element ` ,
1394+ ` @excalidraw/math ` , ` @excalidraw/utils ` , and self-referencing ` @excalidraw/excalidraw/* `
1395+ — packages this plugin's ` package.json ` never lists as dependencies. Confirmed
1396+ this is not fork-specific: the same gap exists in upstream's own published
1397+ ` @excalidraw/excalidraw@0.18.1 ` (` npm view @excalidraw/excalidraw dependencies `
1398+ lists neither ` @excalidraw/common ` nor ` @excalidraw/element ` /` math ` /` utils `
1399+ either, and its own shipped ` .d.ts ` files contain the identical bare imports).
1400+ It's an inherent characteristic of how the Excalidraw monorepo publishes
1401+ per-package types, not a bug introduced by the fork. Every value that
1402+ transitively touched one of those unresolvable imports (` Theme ` ,
1403+ ` BinaryFileData["id"] ` , most of ` AppState ` , large parts of the
1404+ ` ExcalidrawElement ` union) silently collapsed to ` any ` , which is what the
1405+ ` no-unsafe-* ` backlog had actually been measuring all along.
1406+
1407+ ** The fix, and how it evolved (read this before touching it again):**
1408+
1409+ 1 . First attempt: added ` @excalidraw/element@0.18.0-f0063e113 ` (npm's
1410+ ` latest ` tag) as a ` devDependency ` and mapped all four ` @excalidraw/* `
1411+ specifiers to it via ` tsconfig.json ` ` paths ` . This worked for simple
1412+ leaf types (` Theme ` , ` FileId ` ) but ** introduced genuine false-positive
1413+ type errors** for complex types: the externally-installed package's own
1414+ ` ExcalidrawElement ` (e.g. `ExcalidrawArrowElement.lastCommittedPoint:
1415+ LocalPoint | null` ) structurally disagreed with ` @zsviczian/excalidraw`'s
1416+ own bundled ` ExcalidrawElement ` (no such optional field) — two
1417+ same-named-but-different types competing, caught via
1418+ ` InsertPDFModal.ts ` 's ` selectElements() ` /` zoomToFit() ` calls suddenly
1419+ failing with a real structural mismatch that hadn't existed before.
1420+ ** Reverted** (` npm uninstall @excalidraw/element ` ) once this was
1421+ understood — an independently-versioned external package is fundamentally
1422+ the wrong source of truth here, no matter which version is pinned.
1423+ 2 . ** Correct fix:** ` node_modules/@zsviczian/excalidraw/types/ ` already
1424+ vendors its own exact-match copies of ` common/ ` , ` element/ ` , ` math/ ` ,
1425+ ` utils/ ` , and ` excalidraw/ ` (it has to, to be self-contained) — so
1426+ ` tsconfig.json ` ` paths ` redirects ` @excalidraw/common|element|math|utils `
1427+ (bare and ` /* ` subpaths) straight into that same already-installed
1428+ package's own ` types/ ` tree instead of an external one. Zero version-drift
1429+ risk by construction (literally the same files), and ** no new
1430+ dependency at all** — ` package.json ` ended up completely untouched.
1431+ Also explains why the first attempt's ` devDependency ` alone (before
1432+ the ` paths ` redirect existed) did nothing: this project's
1433+ ` "moduleResolution": "node" ` (classic/legacy) never consults
1434+ ` package.json ` ` exports ` maps for ** subpath** imports at all, only
1435+ bare ones via the top-level ` types ` field — ` @excalidraw/common/utility-types `
1436+ could not have resolved through package installation alone regardless
1437+ of version. A global ` "moduleResolution": "bundler" ` switch (which does
1438+ support subpath ` exports ` ) was tested and immediately reverted: it fixed
1439+ this pattern but broke hundreds of other, previously-clean type checks
1440+ elsewhere in the project — ` paths ` remapping is the correctly scoped
1441+ tool here, a global resolution-mode change is not.
1442+
1443+ ** Fallout, fixed file by file, small-to-large, each verified with a fresh
1444+ ` npm run build ` before moving on:** turning the fix on project-wide surfaced
1445+ 117 real, previously-masked compile errors across 18 files (the same
1446+ narrowing-gap shape as the two ` ExcalidrawData.ts `
1447+ ` as Mutable<ExcalidrawTextElement>[] ` fixes in the prior session, now at
1448+ project scale). Fixed via the same idioms throughout — casting to the
1449+ narrower literal/branded type at the exact site where the code already
1450+ behaved as if it had that type (` as Theme ` / ` as "dark" | "light" ` for
1451+ theme strings, ` as FileId ` for branded IDs, ` as NonDeletedExcalidrawElement `
1452+ / ` as unknown as NonDeletedExcalidrawElement ` for the ` isDeleted: boolean `
1453+ vs ` isDeleted: false ` narrowing gap, matching the user's explicit "readonly
1454+ complaints are deliberate, fix as mutable" guidance generalized to this
1455+ whole family of narrowing gaps), or widening an explicit type annotation at
1456+ a ` let ` /` const ` declaration when the array was later reassigned to a
1457+ narrower produced type. Files fully cleared: ` LaTeX.ts ` , ` dynamicStyling.ts ` ,
1458+ ` screenshot.ts ` , ` ExcalidrawData.ts ` , ` excalidrawAutomateUtils.ts ` ,
1459+ ` ExcalidrawAutomate.ts ` , ` ExcalidrawRoot.ts ` , ` InsertPDFModal.ts ` ,
1460+ ` ExcalidrawView.ts ` , ` ViewExcalidrawExtensionRenderer.ts ` ,
1461+ ` ViewExportManager.ts ` , ` ObsidianMenu.tsx ` , ` EmbeddableActionsMenu.tsx ` ,
1462+ ` CustomEmbeddable.tsx ` . A final ` eslint --fix ` pass (scoped — confirmed
1463+ beforehand that every "potentially fixable" finding at that point was
1464+ ` no-unnecessary-type-assertion ` , i.e. removing a now-redundant cast this
1465+ same fix made unnecessary, never a behavior-changing rule) mechanically
1466+ cleaned up a further cascade of now-redundant ` as X ` /` as unknown as X `
1467+ casts across files this session hadn't touched directly (` DropManager.ts ` ,
1468+ ` EmbeddedFileLoader.ts ` , ` ExportDialog.ts ` ), each confirmed zero-risk by
1469+ definition (an assertion ESLint proved changes nothing about the expression's
1470+ type cannot change its runtime value either). One resulting unused import
1471+ (` ExtendedFillStyle ` in ` ObsidianMenu.tsx ` , superseded by the real ` FillStyle `
1472+ cast) was removed by hand.
1473+
1474+ ** Two real bugs found and fixed along the way (not type-only — flagged and
1475+ confirmed before fixing, per the user's explicit instruction):**
1476+
1477+ - ` ExcalidrawView.ts ` 's ` addFiles() ` : ` isDark = s.scene.appState.theme; `
1478+ assigned the literal string ` "light" ` /` "dark" ` directly to a ` boolean `
1479+ parameter. Proof this was live and wrong, not just a type nag: three lines
1480+ later the code did ` isDark: !!isDark ` — ` !! ` on any non-empty string is
1481+ always ` true ` , so every call through this fallback path (whenever the
1482+ caller didn't pass ` isDark ` explicitly) had unconditionally treated the
1483+ scene as dark-themed regardless of the actual theme, since the very first
1484+ version of this code. User caught this by inspection and supplied the
1485+ fix directly; applied as ` isDark = s.scene.appState.theme === "dark" ` ,
1486+ matching the already-correct sibling usage at the same file's line ~ 4088
1487+ (` isDark: st.theme === "dark" ` ).
1488+ - ` ExcalidrawView.ts ` 's ` getSelectedTextElement() ` : the "selected element is
1489+ part of a group containing a text element" branch returned
1490+ `{id: selectedElement[ 0] .id, text: (selectedElement[ 0] as
1491+ ExcalidrawTextElement).text}` — casting the * originally selected* element
1492+ (proven only to be grouped with a text element, never proven to be text
1493+ itself) instead of ` textElement[0] ` , the group's actual text element the
1494+ same branch had just found two lines above via ` .filter(type === "text") `
1495+ and then never used. Silently wrong whenever the selected element itself
1496+ wasn't literally text (e.g. a shape grouped with a caption): ` .text ` would
1497+ read ` undefined ` off a non-text element at runtime, previously invisible
1498+ because the cast was ` any ` -permissive. The sibling "bound text elements"
1499+ branch immediately above already does this correctly (` id ` /` text ` both
1500+ from its own found ` textElement[0] ` ). Asked the user whether ` id ` should
1501+ also switch to ` textElement[0].id ` (this method is exposed via the public
1502+ ` ExcalidrawAutomate ` scripting API, so changing which ` id ` a script
1503+ receives needed explicit confirmation, not an assumption) — confirmed yes;
1504+ both ` id ` and ` text ` now come from ` textElement[0] ` , matching the sibling
1505+ branch exactly.
1506+
1507+ ** One found, initially flagged as a suspected logic bug — corrected by the
1508+ user, then fixed as type-only after all.** ` excalidrawViewUtils.ts ` 's
1509+ ` getViewColorPalette() ` : ` AppState["colorPalette"][palette] ` 's * declared*
1510+ type is ` ColorPaletteCustom = {[key: string]: ColorTuple | string} ` (a
1511+ config-shaped record), which made the function's ` Array.isArray(basePalette) `
1512+ check look like dead code guarding a shape the value could never have. Wrong
1513+ — the user tested it directly and confirmed ` getViewColorPalette() ` already
1514+ returns correct values, then pointed at the authoritative fork-side type
1515+ (` packages/excalidraw/types.ts ` , marked ` //zsviczian ` ) to settle it. The real
1516+ runtime shape is a flat list of single colors and/or grouped 5-color tuples,
1517+ not a record — confirmed independently by the function's own pre-existing
1518+ ` flattenPalette() ` helper a few lines below, whose parameter was * already*
1519+ explicitly typed ` readonly (string | string[])[] ` . So ` ColorPaletteCustom `
1520+ describes the record-shaped * settings/config* input, but the fork
1521+ transforms it into this flat list by the time it lands in ` AppState ` — a
1522+ type-declaration imprecision in the fork's own upstream-facing type, not a
1523+ logic bug in the plugin. Fixed as a documented bridge cast at the one read
1524+ site (` as unknown as string | readonly (string | string[])[] ` , matching the
1525+ shape ` flattenPalette() ` already assumed) plus one follow-on cast the first
1526+ one's ` Array.isArray ` narrowing didn't propagate through
1527+ (` readonly (string|string[])[] ` 's negative-array branch doesn't narrow
1528+ cleanly to ` string ` in this TS version — cast directly instead of relying on
1529+ control-flow narrowing). Zero logic touched; build now fully clean.
1530+
1531+ ** Outcome:** ` npm run build ` , ` npm run lib ` , ` node --check dist/main.js ` all
1532+ pass (exit 0) with ** zero remaining diagnostics** — every one of the
1533+ originally-surfaced 117 build errors is now fixed; the 33-warning
1534+ circular-dependency baseline is unchanged;
1535+ ` dist/main.js ` is 4,716,853 bytes, effectively unchanged (this was
1536+ exclusively a ` tsconfig.json ` /source type-annotation effort, nothing
1537+ touched the runtime bundle). Confirmed via a ` git stash -u ` /` pop ` full-repo
1538+ ESLint diff against the pre-session-3 commit (` b7472cb8 ` ): ** 411 → 229
1539+ findings (182 fewer, 44%), with zero files regressing** — every file with a
1540+ changed count went down, none went up. ` ExcalidrawView.ts ` alone dropped
1541+ 144 → 18 (87%). Several files neither this session nor the prior one
1542+ touched directly also improved as pure beneficiaries of the project-wide
1543+ type-resolution fix: ` CropImage.ts ` , ` InsertImageDialog.ts ` , ` carveout.ts ` ,
1544+ and ` utils.ts ` . ` ExcalidrawRoot.ts ` , ` getElementAtPointer.ts ` ,
1545+ ` screenshot.ts ` , and ` carveout.ts ` are now fully clean (0 findings).
1546+ ` ExcalidrawAutomate.ts ` (still the largest remaining cluster at 57, down
1547+ from 79) and ` excalidrawViewUtils.ts ` (20, down from 23) have not been
1548+ individually re-triaged since this fix landed — worth a fresh look before
1549+ assuming their remaining findings are all external-boundary cases, since
1550+ this session found real bugs by just reading the surfaced errors in
1551+ context. Manual testing pending, prioritizing the two real bug fixes above
1552+ (dark/light theme detection when embedding freshly-pasted images/PDFs
1553+ without an explicit theme argument; selecting a non-text element that's
1554+ grouped with a text element, e.g. via a script calling
1555+ ` getSelectedTextElement ` /its public API surface) over the purely type-level
1556+ changes elsewhere, including ` getViewColorPalette() ` since its logic itself
1557+ was already confirmed correct and unchanged.
1558+
1559+ ** If resumed:** ` ExcalidrawAutomate.ts ` (57) is the natural next file-by-file
1560+ triage target, followed by re-checking ` AIUtils.ts ` (46, previously declined
1561+ as external-boundary — worth confirming that conclusion still holds now
1562+ that so much else has changed) and a final full-repo sweep once the large
1563+ clusters are gone.
1564+
13841565## Related, separate effort: ` ExcalidrawData.ts ` structural extraction
13851566
13861567Started 2026-08-14. Independent of the other work on this page; not blocked
0 commit comments