Skip to content

Commit ccb1f97

Browse files
authored
Merge pull request #2892 from zsviczian/excalidraw-type-import-fix
Fix @excalidraw/common type resolution, eliminating a huge any-collapse
2 parents b7472cb + 8d06fe1 commit ccb1f97

19 files changed

Lines changed: 293 additions & 64 deletions

RefactorPlan.md

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,6 +1381,187 @@ this was a pure type-annotation change with no logic touched).
13811381
both targeted manual tests — the stale-image retry loop and general text-
13821382
element/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

13861567
Started 2026-08-14. Independent of the other work on this page; not blocked

src/shared/Dialogs/ExportDialog.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export class ExportDialog extends Modal {
8181
this.theme = getExportTheme(
8282
this.plugin,
8383
this.file,
84-
this.api.getAppState().theme as string,
84+
this.api.getAppState().theme,
8585
);
8686
this.boundingBox = this.ea.getBoundingBox(this.ea.getViewElements());
8787
this.embedScene = shouldEmbedScene(this.plugin, this.file);

src/shared/EmbeddedFileLoader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1490,7 +1490,7 @@ export class EmbeddedFilesLoader {
14901490
depth,
14911491
inFile: null,
14921492
hasSVGwithBitmap: false,
1493-
elements: result.elements as ExcalidrawElement[],
1493+
elements: result.elements,
14941494
});
14951495
if (this.terminate) {
14961496
return;

src/shared/ExcalidrawAutomate.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,7 +1447,7 @@ export class ExcalidrawAutomate {
14471447
elements,
14481448
appState: {
14491449
...templateAppstate,
1450-
theme: (templateAppstate.theme ?? this.canvas.theme) as string,
1450+
theme: (templateAppstate.theme ?? this.canvas.theme),
14511451
viewBackgroundColor:
14521452
templateAppstate.viewBackgroundColor ??
14531453
this.canvas.viewBackgroundColor,
@@ -1722,7 +1722,7 @@ export class ExcalidrawAutomate {
17221722
...{
17231723
appState: {
17241724
...scene.appState,
1725-
theme: view.getViewExportTheme(theme),
1725+
theme: view.getViewExportTheme(theme) as "dark" | "light",
17261726
exportEmbedScene: view.getViewExportEmbedScene(embedScene),
17271727
},
17281728
},

src/shared/ExcalidrawData.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
NonDeletedExcalidrawElement,
4343
ExcalidrawTextElement,
4444
FileId,
45+
Theme,
4546
} from "@zsviczian/excalidraw/types/element/src/types";
4647
import {
4748
BinaryFiles,
@@ -670,7 +671,7 @@ export class ExcalidrawData {
670671
this.plugin,
671672
this.file,
672673
"light",
673-
);
674+
) as Theme;
674675
} else if (this.plugin.settings.matchThemeAlways) {
675676
this.scene.appState.theme = isObsidianThemeDark() ? "dark" : "light";
676677
}

src/shared/LaTeX.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { ExcalidrawExtrasAPI } from "@zsviczian/excalidraw-extras-api";
88

99
export const updateEquation = async (
1010
equation: string,
11-
fileId: string,
11+
fileId: FileId,
1212
view: ExcalidrawView,
1313
addFiles: (files: FileData[], view: ExcalidrawView) => void,
1414
) => {

src/utils/dynamicStyling.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export const setDynamicStyle = (
110110
//const doc = view.ownerDocument;
111111
const st = view?.excalidrawAPI?.getAppState?.();
112112

113-
const isLightTheme = st?.theme === "light" || st?.theme === "light";
113+
const isLightTheme = st?.theme === "light";
114114

115115
if (color === "transparent") {
116116
color = "#ffffff";

src/utils/excalidrawAutomateUtils.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
FileId,
1414
FixedPoint,
1515
FontString,
16+
Theme,
1617
} from "@zsviczian/excalidraw/types/element/src/types";
1718
import { normalizePath, TFile } from "obsidian";
1819

@@ -552,8 +553,7 @@ export async function createPNG(
552553
source: `${URLs.GITHUB_COM_ZSVICZIAN_OBSIDIAN_EXCALIDRAW_PLUGIN_RELEASES_TAG}/${PLUGIN_VERSION}`,
553554
elements,
554555
appState: {
555-
theme:
556-
forceTheme ?? template?.appState?.theme ?? canvasTheme ?? "light",
556+
theme: (forceTheme ?? template?.appState?.theme ?? canvasTheme ?? "light") as Theme,
557557
viewBackgroundColor:
558558
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
559559
...(template?.appState?.frameRendering
@@ -627,7 +627,7 @@ export const updateElementLinksToObsidianLinks = ({
627627
linkedFile: file,
628628
hostFile,
629629
}) ?? link;
630-
} catch (e) {
630+
} catch (e: unknown) {
631631
errorlog({
632632
where: "excalidrawAutomateUtils.updateElementLinksToObsidianLinks",
633633
fn: window.ExcalidrawAutomate.onUpdateElementLinkForExportHook,
@@ -694,8 +694,7 @@ export async function createSVG(
694694
});
695695
}
696696

697-
const theme =
698-
forceTheme ?? template?.appState?.theme ?? canvasTheme ?? "light";
697+
const theme = (forceTheme ?? template?.appState?.theme ?? canvasTheme ?? "light") as Theme;
699698
const withTheme =
700699
exportSettings?.withTheme ?? plugin.settings.exportWithTheme;
701700

src/utils/excalidrawViewUtils.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
ExcalidrawImageElement,
2828
ExcalidrawTextElement,
2929
FileId,
30+
NonDeletedExcalidrawElement,
3031
} from "@zsviczian/excalidraw/types/element/src/types";
3132
import { getAllNestedExcalidrawFiles } from "./fileUtils";
3233
import {
@@ -686,7 +687,12 @@ export async function addBackOfTheNoteCard(
686687
if (activate) {
687688
window.setTimeout(() => {
688689
api.updateScene({
689-
appState: { activeEmbeddable: { element: el, state: "active" } },
690+
appState: {
691+
activeEmbeddable: {
692+
element: el as NonDeletedExcalidrawElement,
693+
state: "active",
694+
},
695+
},
690696
captureUpdate: CaptureUpdateAction.NEVER,
691697
});
692698
if (found) {
@@ -929,10 +935,17 @@ export function getViewColorPalette(
929935
return getDefaultColorPalette();
930936
}
931937

932-
const basePalette = colorPalette[palette];
938+
// AppState["colorPalette"][palette] is typed as ColorPaletteCustom (a
939+
// config-shaped record) upstream, but at the AppState/runtime level the
940+
// fork actually stores it as a flat list of single colors and/or grouped
941+
// color tuples -- confirmed against this function's own already-typed
942+
// flattenPalette() helper below (readonly (string | string[])[]).
943+
const basePalette = colorPalette[palette] as unknown as
944+
| string
945+
| readonly (string | string[])[];
933946

934947
if (!Array.isArray(basePalette)) {
935-
return [basePalette];
948+
return [basePalette as string];
936949
}
937950

938951
const cmFactory =

src/utils/screenshot.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getEA } from "src/core";
44
import { t } from "src/lang/helpers";
55
import ExcalidrawView from "src/view/ExcalidrawView";
66
import type { NormalizedZoomValue } from "@zsviczian/excalidraw/types/excalidraw/types";
7+
import type { Theme } from "@zsviczian/excalidraw/types/element/src/types";
78
import { hideElement, setStyle, showElement } from "./styleUtils";
89

910
declare const mainDocument: Document;
@@ -122,7 +123,7 @@ export async function captureScreenshot(
122123
appState: {
123124
viewModeEnabled: true,
124125
linkOpacity: 0,
125-
theme: options.theme,
126+
theme: options.theme as Theme,
126127
},
127128
});
128129

0 commit comments

Comments
 (0)