| name | arrow-js-obsidian-porting |
| description | Use when porting an Arrow component from the obsidian-arrow-sandbox into the real Obsidian plugin, or wiring a check that the plugin copy hasn't drifted from the sandbox source. Covers the content-addressed porting-parity tool (scripts/component-hash.mjs), the canonical-form rules, a port-parity manifest, and a husky/CI pre-commit hook. The sandbox is the source of truth; the plugin copy must match. |
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:
import "./utilities.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>
node scripts/component-hash.mjs --verify <sandbox> <plugin>
node scripts/component-hash.mjs --check port-parity.json
node scripts/component-hash.mjs --check port-parity.json --update
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:
{
"entries": [
{
"plugin": "src/chat/arrow/SettingsPanel.ts",
"sandbox": "../../arrow-ui/obsidian-arrow-sandbox/src/components/SettingsPanel.ts",
"hash": "<sha256>"
}
]
}
- 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-",
"viewSubScope": true,
"include": [
"src/components/**/*.css",
"src/views/**/*.css"
],
"outDir": "port-output/css"
}
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:
.composer { display: flex; }
.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:
.composer { display: flex; }
.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.