पेज · Mark It Down
Importer plugin system
Status: shipped in #246. First-party importers (Apple Notes, Google Keep, Notion, generic markdown) land in #247–#250 on top of this contract.
Mark It Down imports notes from external apps via a tiny plugin contract. Adding
a new importer is a matter of dropping one folder under
apps/electron/importers/<id>/ — no changes to the core renderer, main process,
loader, or chooser modal are required.
What ships in this issue
apps/electron/importers/types.ts— theImportercontract, plusImporterMetadata,ImportedNote,ImportContext,ImportedAttachment, and theisImportertype guard.apps/electron/importers/loader.ts— discoversindex.js(compiled) orindex.ts(dev) under eachapps/electron/importers/<id>/folder, validates the default export, and registers it in an in-memory map.apps/electron/importers/sample/index.ts— the smoke-test importer that yields one hardcoded markdown note. Deleting this folder leaves the system with zero importers and the chooser shows an empty-state message.- IPC surface in
apps/electron/main.ts:mid:importers-listreturns metadata only (no functions).mid:importers-runstarts a streaming import.mid:importers-progress,mid:importers-done,mid:importers-error,mid:importers-logpush events back to the renderer.
- Renderer surface in
apps/electron/renderer/renderer.ts(and matching CSS inrenderer.css):- "Import from…" button injected at the bottom of the activity bar.
- "Import from…" entry in the File menu (sends
mid:menu-import). - Modal dialog that lists registered importers, runs the chosen one, and streams progress lines as notes hit disk.
The contract
Every importer module under apps/electron/importers/<id>/index.ts default-
exports an object satisfying:
The contract intentionally returns an AsyncIterable<ImportedNote> instead of
buffering everything: the host writes each note to disk as it arrives, so a
giant Notion export doesn't sit in memory.
How the host uses an importer
- Discovery (one-shot at app startup):
loadImporters()reads every subdirectory ofapps/electron/importers/, requires the first availableindex.jsorindex.ts, validates the default export withisImporter, and registers it. Failures and duplicate ids are logged and skipped — they never crash the app. The startup log prints[importers] registered: id-a, id-b, …. - Listing (per renderer request):
mid:importers-liststrips thedetect/importfunctions and returns the metadata array. - Running (per chooser click):
- Renderer calls
mid:importers-run(importerId, input, workspaceFolder). - Main process resolves the importer, creates
<workspace>/Imported/<importer-id>/, and invokesimport(input, ctx). - For each yielded note, the host:
- Sanitises the title into a filename.
- Writes a markdown file with frontmatter (
created,updated,tags, any keys fromnote.meta). - If
note.attachmentsis non-empty, writes each attachment underImported/<id>/attachments/<sanitised-title>/. - Sends
mid:importers-progressto the renderer with the running count.
- On completion,
mid:importers-donefires with the final count; on failuremid:importers-errorcarries the message.
- Renderer calls
Adding a new importer
-
Create a folder:
apps/electron/importers/my-importer/. -
Add
index.tswhose default export satisfiesImporter:ts -
npm run compile:electron— TypeScript picks the new file up automatically becauseapps/electron/tsconfig.jsonincludesimporters/**/*.ts. -
Restart the app. The startup log lists your importer; it appears in both the activity-bar chooser and the File → Import from… menu.
That's it. No changes to:
apps/electron/importers/loader.tsapps/electron/main.tsapps/electron/preload.tsapps/electron/renderer/renderer.tsapps/electron/renderer/index.html
If a change to one of those files becomes necessary for a future importer (e.g.
to surface a new ImporterInput.kind), that's a contract change — bump the
contract in types.ts and update this doc in the same PR.
Testing locally
Click Import from… in the activity bar (or File → Import from…), pick the Sample (smoke test) entry, and confirm:
- Progress text reads
Imported 1 note…. - A note titled
Hello from the sample importer.mdlands under<workspace>/Imported/sample/. - The file tree refreshes once the importer is done.
If the loader can't find a registered importer, the chooser shows the empty-
state message instead — that's a sign the loader scan didn't pick anything up
(check the main-process console for [importers] skip <name> — … lines).