Add the Copy for LaTeX prototype - #11
Conversation
Ports the standalone roam/js prototype into this repository. Copy a discourse node as a LaTeX sentence carrying its own citation, or as a hyperlink, from a right-click. The port is not a copy. Three repository rules changed the design: - Graph reads move to roamAlphaAPI.data.async.*, so every read is async. That collides with the right-click: preventDefault has to be called synchronously or the native menu wins, but resolving a page heading's title now needs an await. Headings are therefore resolved ahead of time by the observer and the answer parked on the element, keyed to the page uid so navigation invalidates it. A synchronous reconstruction from the DOM covers the gap before the observer catches up. - Datalog inputs are parameterized rather than interpolated, which removes the string-escaping the old version needed. Node titles here really do contain LaTeX, and an unescaped backslash is an invalid Clojure string escape that throws. - The menu uses prototype-scoped classes instead of Roam's Blueprint menu classes, so styling this owns does not track Roam's Blueprint version. Also new since the standalone version: the menu opens on a node's own page heading, not only on inline references. Reading the heading's text is not enough, because a node title contains page references whose brackets Roam renders as separate elements, so the text reads "EVD - x - @y" and matches no node format. The page uid is authoritative; the DOM is the fallback. 106 tests, ported from the standalone suite and run under vitest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Roam prototype previewsThe preview deployment is ready. Paste a URL below into Load Developer Extensions from URL in Roam: |
Roam injects a published extension.css when it loads an extension from a URL. Nothing injects it when the module is pulled in with import() from a roam/js block, which is how preview builds actually get tested. The menu is position: fixed, and that rule lived only in the stylesheet. Loaded through a roam/js block it therefore became a static, unstyled list appended to the end of <body>: created, positioned nowhere, clickable in principle and invisible in practice. Reported from real use. Structural CSS for a floating menu is behavior, not theming, so it now lives with the code. src/styles.ts holds the rules, index.ts injects them with roamjs addStyle and removes them on unload. dist/extension.css is no longer emitted, which the artifact contract allows. Adds three regression tests, each verified against the defect it guards: onload must inject the rules and unload must remove them; the carried CSS must make the menu float; and every rule must stay scoped to this prototype so it cannot restyle the graph. Also documents both load paths in the README, including the loader block and the warning that any prototype shipping CSS separately will look broken when imported this way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Loading through a roam/js block produced "Cannot read properties of undefined (reading 'settings')" and nothing else. That error is not the fault: it is runExtension's failure reporter crashing on top of the real one. runExtension's production catch never logs the error. It posts the message to SamePage and shows a generic "Failed to load" toast, and while building that report it reads args.extensionAPI.settings.getAll(). extensionAPI is undefined whenever the module is loaded by import() from a roam/js block rather than by Roam itself, so the reporter throws its own TypeError and the original error is gone. Any block-loaded prototype hits this. So errors are caught here, where the message still exists: logged with the error object and shown in a toast that does not time out. Nothing is rethrown, so the broken reporter is never reached. Adds an explicit capability check, since the most likely real cause is a Roam build without roamAlphaAPI.data.async.q. A missing capability now names itself instead of surfacing as a TypeError deep in a helper. Also gives the observer's fire-and-forget heading resolution a catch, so a failure there is attributed rather than appearing as a bare unhandled rejection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The real error, once it stopped being swallowed, was
"(0, wo.default) is not a function" on the addStyle call.
roamjs-components is CommonJS. This repository builds with esbuild in ESM
format, and its __toESM helper is emitted in Node-interop mode: when
isNodeMode is set it defines `default` on the wrapper as the entire module
object, and __copyProps then skips the module's own `default` because the
key already exists. So
import addStyle from "roamjs-components/dom/addStyle";
binds `{ default: fn }` rather than `fn`, and calling it throws. Every
default import from that package has this shape. Named imports are
unaffected, which is why the generated template's `{ render }` and
`{ runExtension }` work and nothing warns you.
Vitest cannot catch it: it resolves CommonJS with ordinary interop, so the
same code passes every unit test and fails only in the built bundle. The
new tests/interop.spec.ts therefore checks the source text, and names the
offending line when it fires.
The stylesheet is now injected by six lines of local code, which also
removes a dependency from the load path.
Hardens the failure reporter as well. It called renderToast, which needs
Blueprint and a lazily-loaded Roam global and so can throw on its own; when
it did, the original error was lost again and the symptom reverted to the
misleading "reading 'settings'". The console now gets the error first and
unconditionally, with the toast attempted separately.
Verified against the built bundle in a jsdom harness with the host globals
stubbed: the buggy build reproduces the reported error and the fixed build
loads, injects its styles, marks references, and cleans up on unload.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A repo-wide trap worth fixing upstream: default imports from
|
Fixed in |
Copy a discourse node out of Roam as a LaTeX sentence carrying its own citation, or as a hyperlink, from a right-click. Ported from a standalone
roam/jsprototype that has been in real use.Preview URL once CI publishes:
What the repo's rules changed
The port was not a copy. Three rules in
AGENTS.mdchanged the design, and the reasoning is worth recording:Async reads collide with
preventDefault.roamAlphaAPI.qis banned, so every read is async. ButpreventDefaultmust be called synchronously during the contextmenu event or the native menu wins, and resolving a page heading's title needs anawait. Headings are therefore resolved ahead of time by the mutation observer and the answer parked on the element, keyed to the page uid so navigation invalidates it — Roam reuses the heading element across pages, so an unkeyed cache would answer for the page you were on before. A synchronous reconstruction from the DOM covers the gap before the observer catches up.Parameterized Datalog removed a real hazard, not just a style nit. The old version interpolated titles into query strings and needed backslash-then-quote escaping to survive them. Node titles in these graphs contain LaTeX (
$$\frac{a}{b}$$), and an unescaped backslash is an invalid Clojure string escape that throws. Passing inputs through:indeletes that whole class of bug; the escaping helper is gone.Prototype-scoped CSS instead of Roam's Blueprint classes. The menu no longer borrows
bp3-menu, so styling this owns does not track Roam's Blueprint version. One selector still does track the host:.bp3-darkfor dark mode, because there is no more semantic hook for it.Three things worth fixing in the repo
Each cost time and will cost the next person the same. All three are reported rather than fixed here, since they are shared tooling.
pnpm create:prototype -- --name ...fails. BothREADME.mdandCONTRIBUTING.mddocument the--separator, but pnpm 10.15.1 — the version this repo pins — forwards it to the script, which exits withUnknown argument: --. Dropping the--works.The root
pnpm testbreaks as soon as a prototype has tests. It runs a barenode --test, whose default discovery walks the whole tree and matches any*.test.ts. Node's runner then tries to execute prototype tests it cannot resolve (vitest, the~/alias) and the root command fails. This prototype works around it by naming its files*.spec.ts, which Node's patterns do not match. The tidier fix isnode --test test/at the root.The generated
vitest.config.tsdoes not resolve the~/alias its owntsconfig.jsondeclares. The esbuild CLI honours it, so source builds fine and only tests fail. Worth adding topackages/extension-base/template.Validation
pnpm test(31 root + 106 prototype),pnpm build, andpnpm prepare:artifactsall pass. Typechecked separately with TypeScript 5.6; the repo has no typecheck step, since esbuild strips types without checking them.One
@ts-expect-errorremains, on the sidebaraddWindowcall: the typings declarepage-uidfor an outline window but the runtime wantsblock-uid. Verified live, and the Discourse Graph plugin suppresses the same error in seven places. Left as@ts-expect-errorrather than a cast so it starts failing the dayroamjs-componentscorrects the type.🤖 Generated with Claude Code