some more fixes

This commit is contained in:
pj committed 2026-10-03 22:48:49 +05:30
1 parent 7a3c04f170
commit 2b86a407ba
63 files changed
+12172 -44

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
# Repo layout
Where shared code lives, how four repositories consume it, and what each package is called. Every
other document in this directory uses the names defined here.
## The problem with what exists
`margin-shared` is a real package with the right contents, living in the wrong place. It sits inside
Margin's git repository at `python/margin/shared`, and Margin Docs and Margin Mail depend on it as
`"margin-shared": "file:../../python/margin/shared"`, a path that walks out of one repository and
into a sibling checkout.
On this machine pnpm has resolved that to a symlink and it works. Nowhere else does it. A fresh
clone of Margin Docs fails `pnpm install` unless Margin happens to be checked out at exactly that
relative path. No CI runner reproduces it. No second contributor reproduces it. The user's own
machine reproduces it only because of the order things were created in.
Margin Calendar sidesteps the whole thing by not depending on it at all and keeping its own copies of
the same tokens, which is how the tokens drifted.
There is a second problem underneath. Margin is FSL-1.1-MIT and Margin Mail will be. Margin Calendar
and Margin Docs are MIT. Shared code cannot be under two licences, and shared code that lives inside
the FSL repo inherits the wrong one.
## The shape of the answer
A fifth repository, `margin-shared`, holding npm packages under `packages/` and Rust crates under
`crates/`, licensed MIT so all four apps can consume it whatever their own licence says.
The four app repositories stay separate, as the user wants. They depend on the shared repo by
version, not by relative path.
A monorepo containing all four apps would be simpler than this. Atomic changes across an app and its
dependency, one lockfile, one CI, no publishing step, no version skew. It is the obvious engineering
answer and it should be said out loud rather than implied. It is not what is being built here,
because the four apps ship separately, have separate release cadences, have separate App Store
records, and one of them is FSL while others are MIT. Those are real reasons and the decision stands,
but the cost is real too: every shared change becomes a two-step, and [risks.md](risks.md) says what
that costs in practice.
## The npm packages
Scope `@margin`. Every one of them is source-only with no build step, resolved through Vite the way
`margin-shared` already is, because a build step in a design token package buys nothing and costs a
watch mode.
`@margin/tokens`. The CSS custom properties, the base reset, the focus ring, the scrollbar, the
title bar metrics, light and dark under `data-theme`. One stylesheet, imported first by every app.
This is the single highest-value package and the one to do first.
`@margin/fonts`. The six bundled faces: the catalogue in TypeScript, the `@font-face` block, the
variable font binaries and their licences, and the `sync-fonts` binary that vendors them into an
app's `public/fonts` for the Rust PDF exporters to read with `include_bytes!`. This is today's
`shared/src/fonts.ts` and `shared/fonts/`, moved intact.
`@margin/icons`. The glyph paths on a 24 unit grid for a 1.6 stroke, as bare strings with no React
dependency. Today's `shared/src/icons.ts` plus every path the four apps have each drawn separately.
`@margin/ui`. The React primitives. This is the package that does not exist today and is the reason
the same button gets built four times: `Icon`, `Button`, `IconButton`, `Field`, `Switch`, `Select`,
`Dialog`, `Confirm`, `Sheet`, `Toast`, `Banner`, `Menu`, `RowMenu`, `ResizeHandle`, `FindBar`,
`Palette`, `SettingsShell`, `EmptyState`, `Spinner`, `Kbd`. Every one of them carries the busy and
disabled states that [guidelines/errors-and-feedback.md](guidelines/errors-and-feedback.md) requires,
so no app can forget them. Details in [ui-kit.md](ui-kit.md).
`@margin/hooks`. `useMedia` and `useCompact`, the theme hook and its `data-theme` writer, the escape
stack, the keyboard registry and its scope stack, focus trapping and restoration, and the small
utilities each app reinvented: clamp, debounce, relative time, byte size, platform detection.
`@margin/ipc`. The typed wrapper around Tauri's `invoke` and `listen`: the `call` function that logs
every failure, the phase union type the conventions require, the error shape that crosses the
boundary, and the `isTauri` check. Also the dev-mode stub that lets the app run in a plain browser
against fixtures, which is currently reinvented in three `src/dev` directories.
`@margin/test`. The Playwright config factory, the fixture loader, and the assertions every app
should share: that a control has a busy state, that a colour resolves to a token, that an icon is
aligned. Details in [testing.md](testing.md).
`@margin/config`. A base `tsconfig.json` to extend and a Vite config factory taking the app's name
and port. Details in [toolchain.md](toolchain.md).
`@margin/typeset`. The TypeScript half of the Typst pipeline, which is the half that matters for
correctness: the string escaper and its sanitiser. Both apps build Typst source in TypeScript, so a
Rust-only escaper would not fix the bug that put JSON escape sequences on the page. The two halves
share one table of test vectors. Details in [typesetting.md](typesetting.md).
## The Rust crates
Same repository, under `crates/`, consumed as cargo git dependencies pinned to a tag.
This list was drafted before the audits ran and the audits disagree with parts of it.
[rust-crates.md](rust-crates.md) is the authority: it argues against a shared settings crate, a
shared error crate, a shared filesystem crate and a shared async crate, with the evidence, and it
renames others. Where the two documents differ, that one is right and says why.
`margin-paths`. App data directory, library directory, atomic write, trash, path validation. Four
apps have four versions of this and Margin's already lives in `library.rs` as `pub(crate)` helpers.
`margin-log`. The log file in the app data directory, with rotation. Margin Mail has it; the other
three log nothing, which is why an error report from them starts with guesswork.
`margin-db`. Opening a rusqlite connection with the busy timeout and WAL that three apps each set
separately, the migration runner and its version table, and the FTS5 helpers.
`margin-secrets`. The XChaCha20-Poly1305 sealed file that replaced `keyring`, with the key derivation
and the atomic replace. Margin Calendar and Margin Mail have near-identical copies at different
crate versions.
`margin-google`. The OAuth client: PKCE, the loopback listener on desktop, the deep link scheme on
mobile, token exchange, refresh with clock skew, revocation, and the multi-account store. Three apps
talk to Google and two have full implementations of this.
`margin-http`. One `reqwest` client with the timeouts, retry and backoff policy, and the user agent,
so a rate limit is handled the same way everywhere.
`margin-ipc`. The serde conventions for types crossing into the webview and the error type that
crosses with them.
`margin-typeset`. The Typst pipeline: preamble generation, the font resolver, escaping user text into
Typst source, image handling, page setup, error mapping. Plus the `fontdb` catalogue. Margin and
Margin Docs each carry a copy of all of it. Details in [typesetting.md](typesetting.md).
`margin-mac`. The macOS integrations behind one `cfg(target_os = "macos")` boundary: `NSSpellChecker`,
Apple Writing Tools, `UNUserNotificationCenter` posting, and the title bar inset work. Margin and
Margin Docs duplicate the first two. Only Margin Mail posts notifications at all, and it is the only
one that does so correctly; the other three do not have the bug because they do not have the feature,
which is the state [typesetting.md](typesetting.md) records.
`margin-grammar`. Harper and the `[patch.crates-io]` stubs that keep a CUDA and LLVM subtree out of
the build. This crate is not in the list above because it was written before the audit; grammar is
neither typesetting nor macOS and needs its own home. See [typesetting.md](typesetting.md), which is
the authority on this group.
`margin-update`. The updater wiring and the release overlay convention.
## How an app depends on them
For npm, publish `@margin/*` to the registry and depend on them by semver range. It is the boring
option and it is the one that works on a fresh clone, in CI and on someone else's machine. The cost
is a publish step, which a `just release-shared` recipe and a CI job on tag reduce to one command.
The no-registry alternative, if publishing is unwanted, is a git subpath dependency, which pnpm
supports: `"@margin/tokens": "github:priyanshujain/margin-shared#<tag>&path:/packages/tokens"`. It
works, it needs no account, and it is less standard, so document it if it is chosen.
For local iteration on shared code, `pnpm.overrides` in the app's `package.json` pointing at a local
checkout, committed as a comment and not as a value, or `pnpm link`. The point is that the override is
the exception a developer turns on, not the default the repo ships.
For Rust, cargo git dependencies pinned to a tag are already idiomatic and need no registry at all:
margin-google = { git = "https://github.com/priyanshujain/margin-shared", tag = "google-v0.3.0" }
For local iteration, a `[patch]` section or a `paths` entry in `.cargo/config.toml`, again as the
exception rather than the default.
## Versioning
One version for the whole shared repo, tagged `v0.3.0`, with every package and crate moving together.
Independent versioning of eleven packages consumed by four apps is a matrix nobody wants to reason
about, and these packages are not independent: a token rename breaks the UI kit.
The apps keep their own versions, which is what the App Store and the updater care about.