SuperAPI AI Directory

arrow-js-obsidian-porting

Coding & development

View repository

Updated 10-15 23:48

About this skill

Porting parity (sandbox → Obsidian plugin)

The sandbox is the source of truth for a component; the plugin gets a near-verbatim copy. The risk is drift — someone hand-edits the plugin copy, or a port silently diverges. This makes components content-addressed: a canonical hash of the component's portable body must match on both sides, the same idea as "never hand-edit generated artifacts."

Copy utilities.css with the port

src/utilities.css is a set of oas--prefixed layout, spacing, typography, and border utilities built on Obsidian's CSS custom property scale. It covers two spacing families: the 4-px step scale (steps 1–6) and the 2-px sub-scale (oas-gap-2-1 = 2px, oas-gap-2-3 = 6px — class names mirror Obsidian token names). Components may use these classes — they are portable by design. When porting a component, copy this file into the plugin's styles directory and import it:

// In the plugin's entry (or the view file's companion CSS import):
import "./utilities.css";   // or however the plugin loads CSS

The oas- prefix means it won't conflict with any Obsidian selector. Copy it once per plugin — all ported components share it. If the sandbox adds new utility classes, re-copy the file.

Write portable components

So the body is byte-identical across sandbox and plugin and only the mount site differs:

  • Take data via props / getters, not hard-wired sources. The sandbox passes a stub (loadStatus()); the plugin passes the real source (rpc) — but that wiring lives at the call site (main.ts / ItemView.onOpen()), not in the component body.
  • Use Obsidian classes + var(--…) tokens (already the rule). Keep obsidian-API calls (setIcon, …) out of the component body where possible.

The tool: scripts/component-hash.mjs

Dependency-free Node; canonicalizer in scripts/lib/canonical-source.mjs.

node scripts/component-hash.mjs <file>                       # print canonical hash
node scripts/component-hash.mjs --verify <sandbox> <plugin>  # PARITY OK | DRIFT (exit 0/1)
node scripts/component-hash.mjs --check port-parity.json     # check all; exit 1 on drift
node scripts/component-hash.mjs --check port-parity.json --update   # re-bless baselines

Canonical form (define it so both sides agree by construction):

  • import …; statements are stripped (single- and multi-line) — imports are the main legit delta (stub vs real data, the obsidian API).
  • Line endings → \n, trailing whitespace trimmed, blank-line runs collapsed, leading/trailing blanks trimmed.
  • Comments are kept (they're part of the component; the sandbox owns them too).
  • SHA-256 of the result.

So two copies that differ only in imports/formatting hash equal; any change to the body/template/comments shows as drift.

Wiring the check in the plugin

The hook lives in the plugin repo (that's where ported copies drift). Copy the two dependency-free files in (scripts/lib/canonical-source.mjs, scripts/component-hash.mjs) and add a manifest:

// port-parity.json — paths resolve relative to THIS file
{
  "entries": [
    {
      "plugin": "src/chat/arrow/SettingsPanel.ts",
      "sandbox": "../../arrow-ui/obsidian-arrow-sandbox/src/components/SettingsPanel.ts", // optional
      "hash": "<sha256>"                                                                  // fallback when sandbox isn't checked out
    }
  ]
}
  • If the sandbox path exists (sibling checkout), the check compares plugin copy ↔ live sandbox source — catches drift in both directions.
  • If it doesn't, it falls back to the recorded hash (catches hand-edits to the plugin copy). Record/refresh it with --update after an intentional re-port.

husky pre-commit (and CI):

node scripts/component-hash.mjs --check port-parity.json

On drift the commit fails: edit the sandbox component and re-port (don't hand-edit the copy); use --update only to intentionally re-bless.

CSS porting — pnpm port:css

Components use oas-* utility classes and scoped custom CSS during development. When porting to the real plugin, class names need a project-specific prefix so they don't collide with other plugins. The port:css command handles this at port time — it never modifies source files.

Setup: porting.config.json

Create porting.config.json at the project root (copy from porting.config.example.json):

{
  "cssPrefix": "vault-mind-",        // prefix applied to every class selector
  "viewSubScope": true,              // view CSS gains ancestor selector (see below)
  "include": [                       // CSS files to process (glob patterns)
    "src/components/**/*.css",
    "src/views/**/*.css"
  ],
  "outDir": "port-output/css"        // where prefixed output is written
}

This file is user-owned — it is never touched by create-obsidian-arrow refresh.

Running it

pnpm port:css

Produces a prefixed copy of every matched CSS file under outDir/, plus an outDir/index.css that @imports them all. Copy those into your plugin's styles directory and import index.css.

What it does to class names

Every class selector in the output gets the prefix prepended:

/* source */
.composer { display: flex; }

/* output (cssPrefix: "vault-mind-") */
.vault-mind-composer { display: flex; }

viewSubScope: true — ancestor scoping for view CSS

When enabled, CSS files inside src/views/<ViewName>/ get an additional ancestor selector derived from the view folder name and the prefix:

/* source: src/views/ChatView/Composer.css */
.composer { display: flex; }

/* output (cssPrefix: "vault-mind-", viewSubScope: true) */
.vault-mind-chat-view .vault-mind-composer { display: flex; }

This means the view's CSS only activates inside its own workspace leaf — no cross-view bleed. The ancestor class (.vault-mind-chat-view) must be applied to the view's root element in the plugin's ItemView.onOpen() or equivalent.

Disable viewSubScope if your views don't need that isolation (e.g., a settings view that mounts as a modal rather than a leaf).

When to run

Run pnpm port:css as the final step before copying files into the plugin. It is not part of pnpm ci — it's a one-shot port operation, not a development check.


What this does NOT do (by design)

  • No runtime component-DOM hashing in Obsidian. Source parity already guarantees same code → same Arrow DOM, and the sandbox vs real Obsidian DOM intentionally differ (icon stub vs setIcon, MarkdownRenderer nodes, live theme), so a runtime markup hash would be tautological and full of false mismatches. The real runtime risk is visual/CSS — verify that by loading in Obsidian and looking, not with a hash.
  • Planned (not built): a styling-freshness check. A small helper that hashes the live Obsidian token set and compares it to the sandbox's pulled app.css snapshot, to flag "re-run pnpm pull-css." That's the one runtime hash worth having — environment parity, not component parity. Add it when needed.