Imported from philips/supernote-obsidian-plugin (
AGENTS.md). Install upstream withnpx skills add philips/supernote-obsidian-plugin. Copyright stays with the author.
Supernote Obsidian plugin
Project overview
- An Obsidian community plugin (TypeScript → bundled JavaScript) for viewing,
exporting, and importing Supernote
*.notefiles, plus device screen mirroring and Wi-Fi file transfer. - Entry point:
src/main.ts, compiled tomain.js(esbuild bundle) and loaded by Obsidian. - Release artifacts:
main.js,manifest.json,styles.css. - Plugin ID is
supernote(seemanifest.json) — never change this; it's Obsidian's stable identifier for installs, settings storage, and the community plugin listing.
Environment & tooling
- Package manager: npm. Bundler: esbuild (
esbuild.config.mjs). - This plugin depends on a git submodule, not a plain npm dependency —
see
CLAUDE.mdfor the full explanation of why (supernote-typescript/, developed alongside this plugin) and exactly what not to do about it (nonode_modulessymlink, nonpm link, nofile:dependency — all three were tried and broke the build in confusing ways). Run./scripts/buildto set everything up; it handles submodule init/update and both builds. - Tests: vitest (
npm test), scoped tosrc/**/*.test.tsonly — the submodule has its own separate test suite. - For UI/rendering changes, typecheck+lint+tests don't confirm the feature
actually renders correctly —
scripts/setup-obsidian-test-env(one-time, Fedora/RHEL) andscripts/obsidian-headless(start/stop/screenshot/click/ key) set up and drive a real Obsidian instance under Xvfb, so a change can be screenshotted and visually confirmed even with no display attached.
Common commands
./scripts/build # first-time setup: submodule + full build
npm run dev # esbuild watch mode
npm run build # tsc typecheck + production esbuild bundle (minified)
npm run lint # eslint .
npm test # vitest run
./scripts/setup-obsidian-test-env # one-time: set up a headless Obsidian test vault
./scripts/obsidian-headless start # launch it; screenshot/click/key/stop subcommands too
Linting
- ESLint 9 flat config (
eslint.config.mts) witheslint-plugin-obsidianmd's recommended (type-checked) rules — catches real Obsidian API misuse, not just generic TypeScript issues. npm run lintmust exit clean (0 errors) for CI. A handful of rules are intentionally left as warnings, not fixed, because fixing them has a real cost or risk beyond what a lint pass should take on:obsidianmd/commands/no-plugin-id-in-command-id(7 commands) — renaming a command ID changes its Obsidian-internal ID and orphans any user's saved hotkey bindings. Don't rename existing command IDs; this is a one-way door.no-restricted-globalsonfetchindeviceFetch.ts— this file implements its own timeout viaAbortController, whichrequestUrl(Obsidian's suggested replacement) doesn't support the same way. Migrating needs its own design pass, not a mechanical swap.obsidianmd/settings-tab/prefer-setting-definitions— adopting the declarative settings API (Obsidian 1.13+) is a real rewrite ofsettings.ts, not a lint fix.
- Obsidian's
loadPdfJs()returnsanyby design (it exposes whatever pdf.js build is bundled with the user's Obsidian install, unpinned).main.tsdefines localPdfJs*interfaces covering just the subset of that API this plugin uses, rather than depending on a specificpdfjs-distversion that may not match what's actually loaded at runtime. Extend those interfaces if you touch more of the pdf.js surface — don't reach back forany.
Community scan
Obsidian's automated community-plugin scan (runs for community-listing
submissions; the raw output is archived in issue #228) checks the source and
styles.css with eslint-plugin-obsidianmd plus its own checks. Findings
that are intentionally left as-is, and why:
'any' overrides all other types in this union/'error'-type members ("acting as any") —src/atelierComposite.worker.ts'sspdfield andsrc/webcomponent/SupernoteViewerElement.ts'ssnfield. The scanner's type-checker can't resolvesupernote-typescript: it's a git submodule wired up via esbuild/vitest/tsconfig aliases, not an npm dependency (the setupCLAUDE.mddocuments not changing; the scanner clones without the submodule checked out, so the mapped path is empty and those types collapse toany/error). Localtsc(npm run build) is clean.obsidianmd/prefer-create-elandobsidianmd/no-static-styles-assignmentinsrc/render/*andsrc/webcomponent/*— every flagged file is part of the standalone web-component path (issue #183) and deliberately has zeroobsidianimports so it runs in a plain browser.createEl/setCssStylesrequire importingobsidian, which would break the standalone bundles (esbuild.webcomponent*.mjsintentionally carry no obsidian alias/external). Many of the flagged style assignments are dynamic runtime values (pageaspect-ratio, thumbnail sizing, overlay positioning) that can't be CSS classes.fetchinsrc/webcomponent/— the standalone component fetches its ownsrcURL in plain browsers, whererequestUrldoesn't exist. Same rationale as thedeviceFetch.tsentry in the Linting section above.- Node built-ins in
src/sql-wasm.test-stub.ts— vitest-only alias target, never bundled (esbuild resolves the real.wasmvia itsbinaryloader); the imports are dynamic (issue #231). - CSS findings in
styles.css—display: contentson the.supernote-embedwrapper (the scanner's 1.7.4 baseline is below this plugin'sminAppVersion1.8.7, so every supported Obsidian has it), the single!importanton.internal-embed.supernote-embed(the narrowest robust way to beat Live Preview's own!importantreset — documented instyles.cssitself), and thesupernote-viewer/supernote-atelier-viewertype selectors (the plugin's own custom elements, which the CSS linter can't know about). Full triage: issue #232.
Already fixed rather than argued with:
- The scan's one error — created/attached
<style>elements in the two web components. False positive in spirit (the styles live in the shadow root, wherestyles.csscan't reach), but the scan is static and can't tell the difference, so both components now use constructable stylesheets (adoptedStyleSheets; issue #229). builtin-modulesdeprecated-package warning — the esbuild configs now usenode:module's ownbuiltinModuleslist (issue #230).
File & folder conventions
- Source lives in
src/, one concern per file:main.ts— plugin entry point, the note-viewingFileView(PDF rendering, zoom, text layer, find-in-note), PDF/image export.settings.ts/customDictionary.ts— settings tab and the custom dictionary sub-UI.FileListModal.ts/ImportTodayModal.ts— device browse/upload/ download and today's-notes import modals.deviceFetch.ts/deviceDate.ts— device HTTP client and date parsing, each with a co-located*.test.ts.rasterize.worker.ts/pdfBuild.worker.ts— two separate Web Workers (bundled viaesbuild-plugin-inline-worker), split so the standalone web component's bundle (src/webcomponent/) doesn't pull in pdf-lib just for sharing a worker script with the plugin's PDF export feature. Both consume vector ink when thevectorInksetting is on (on by default; turn off in settings if a render renders ink wrong):pdfBuild.worker.ts:buildPdfInWorker(main thread) calls the submodule's exportedprepareVectorInkPages/buildRenderNoteForVectorInk, slices the stripped note viaextractPdfPageData, and posts the per-pagestrokes/strokeStylesalongside the page slices; the worker passes them through toaddPdfPage, which draws them as vector paths.rasterize.worker.ts(the on-screen view + image export path, shared with the standalone<supernote-viewer>):convertToImagespreparesprepareVectorInkPageson the main thread (the worker'sIRenderableNoteslices lack theTOTALPATH/titlesthe decode reads) and posts the per-pageVectorInkPagealongside the slices; the worker nulls the ink layers on eachuseVectorInkpage's slice (sotoImagerasterizes only the background), thenaddSvgPagedraws the strokes as vector<path>s on top, returned as animage/svg+xmldata URL that drops into<img src>identically to the PNG data URLs. Full-res renders only — thumbnails keep the raster path (ascaledownsample would corrupt vector coordinates). The submodule exports those helpers from its public API (philips/supernote-typescript PR #108) specifically so these batched-worker paths can reach vector ink without thetoPdf/toSvgwrappers.
- Don't commit build artifacts (
main.js,node_modules/) —main.jsis gitignored and shipped only via GitHub releases.
Manifest & versioning
manifest.json: keepminAppVersionaccurate to what the code actually calls —eslint-plugin-obsidianmd'sno-unsupported-apirule checks this against@sincetags in Obsidian's own type declarations and will error on a mismatch.- There's no separate beta manifest file.
manifest.jsonas committed must only ever hold the last stable version — Obsidian's update-checker reads it straight from the repo (not from GitHub's "latest release"), so a beta version committed here would get offered to every installed user. Beta releases are just a git tag with a semver pre-release suffix (e.g.3.0.2-beta.1, no commit needed); the release workflow stamps that version intomanifest.jsononly inside its own build, for the release asset — see the "Releasing" section inREADME.md. npm run versionbumpsmanifest.jsonand appends toversions.json(version-bump.mjs), skipping the write if that version is already present — don't hand-editversions.json(a hand-edit is exactly what produced a bare syntax error there before; let the script own that file).
Security, privacy, and compliance
Follow Obsidian's Developer Policies and Plugin Guidelines: https://docs.obsidian.md/Developer+policies, https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines. In particular for this plugin: device IP/connection settings stay local (no telemetry), and the only network calls are to the user's own Supernote device on their own LAN.
References
CLAUDE.md— the submodule build setup, in full detail.- Obsidian sample plugin: https://github.com/obsidianmd/obsidian-sample-plugin
- API documentation: https://docs.obsidian.md