All of SolidOS — solid-ui 5.0.1, mashlib 3.0.0, solid-panes 5.0.0 and the panes — as plain ES modules. Edit a file, reload. No Vite, no webpack, no TypeScript compiler, no plugins. Runs from inside a pod: a JSS pod serves it as its data browser with --mashlib-module (see "Running on a pod").
Live from this branch: https://jss.live/solidos-nobuild/ (the component gallery; mashlib/databrowser.html?uri=… is the full data browser on any public resource). Licence: MIT, see LICENSE and NOTICE.md.
python3 -m http.server 8731 # any static serverindex.html— gallery of every component (mock auth).?smokeruns 23 click-through checks,?tagschecks all 27 tags register.login.html— real Solid login throughsolid-ui-provider→ SolidAuth → solid-logic.?smokeruns 10 checks.mashlib/databrowser.html— SolidOS itself: mashlib's shell, solid-panes and every registered pane. On a pod, the page's own URL is the subject, as upstream; locally,?uri=picks one (e.g. a WebID, a folder, a Turtle file).pane.html?pane=profile-pane— a pane sandbox (same context and flow as solidos-toolkit's): loadspanes/<pane>/src/index.jsand renders it for?subject=.&smokeruns the pane's checks (demo/<name>-smoke.js).demo/load-each.html?dir=…— imports every module under a directory on its own and names the ones that fail (needs a server with directory listings, likepython3 -m http.server).legacy.html— the legacy library. Runs 16 behaviour checks (buttons, the legacy form renderer, tabs, CodeEditor, loginStatusBox, ACLs, chat signatures, header/footer, camera button);?loadimports all 85 modules and checkssrc/solid-ui.jsexports the same 45 names as upstream's publisheddist/index.esm.js.
Upstream clones are expected beside this repository as ../upstream-<package> (only for reconverting); tests/pod.mjs reads the local pod's password from ../jss-password.txt or POD_PASSWORD, and its data directory from POD_DATA.
python3 -m http.server 8731 & # in this directory
node tests/run-all.mjs # every page check, module loads, the shell on the public test pod
node tests/run-all.mjs --pod # … plus login, editing and creating on a local JSS pod (below)tools/cdp.mjs drives headless Chromium over the DevTools protocol (no npm dependencies); tests/shell.mjs and tests/pod.mjs use it. Failures with a confirmed external cause print as KNOWN with the reason (see "Upstream findings").
<script src="src/importmap.js"></script>
<link rel="stylesheet" href="src/styles/theme.css">
<script type="module" src="src/index.js"></script>
<solid-ui-provider>
<solid-ui-account></solid-ui-account>
</solid-ui-provider>The legacy API, as panes use it (src/importmap.js maps solid-ui and solid-ui/components/* to these files, as upstream's package.json exports does):
import * as UI from 'solid-ui' // UI.widgets, UI.login, UI.acl, UI.tabs, UI.initHeader, …
import { Combobox } from 'solid-ui/components/combobox'| Upstream (build) | Here (no build) |
|---|---|
| TypeScript | JS + JSDoc types |
@customElement, @property, @state |
define(), static properties, defaults in the constructor |
@consume / @provide |
consume() / provide() in src/lib/context.js |
@query('x') |
getter over renderRoot.querySelector('x') |
import styles from './X.styles.css' |
await loadSheet(new URL('./X.styles.css', import.meta.url)) |
~icons/<set>/<name> (unplugin-icons) |
src/lib/icons.js, same icon-<set>-<name> tags |
| npm dependencies | src/importmap.js → jsdelivr |
Component CSS files are byte-for-byte upstream, except src/lib/WebComponent.styles.css, which is upstream's built output: their build resets margin/padding on a list of HTML elements instead of *, and the raw Tailwind preflight breaks nested components.
-
src/lib/write-fallback.jswraps the shared store'sfetcher.webOperation: a Turtle/N3 write answered with 415 and anAccept-Put(orAccept) that lists JSON-LD is retried once as expanded JSON-LD. JSS stores ACLs only as JSON-LD; SolidOS writes them as Turtle. All panes write through this method, so this covers them all. -
Atkinson Hyperlegible Next is self-hosted (
src/fonts/, SIL OFL) instead of loaded from Google Fonts, so pages make no requests to Google. -
src/index.jsattaches aContextRoot, so a consumer that connects before its provider still gets the context. Upstream relies on registration order, which top-level-await stylesheets can't guarantee. -
widgets/forms.js: the "Multiple" field creates its list before logging it (upstream throws on a new resource). -
rdf-form: parts are looked up in the form document, so aformUrlwith a fragment works. Upstream uses the full URL as the graph and renders an empty form (upstream bug). -
SolidAuth.login()loads the login modal lazily. -
fieldParamskeeps only the keys the web components read; thestyle/size/patternentries serve the legacy widgets. -
The three login/logout/signup buttons are one factory (
components/auth-buttons.js); the three dialog wrappers are one file (components/dialog-parts.js).
widgets/, utils/, lib/, login/, signup/, acl/, chat/, create/, header/, footer/, matrix/, media/, v2/, icons/: 84 files, ~20,000 lines, imported once with tools/convert.mjs and edited by hand from then on:
node tools/convert.mjs tools/solid-ui.json.tsfiles go through Node's ownstripTypeScriptTypes(strip mode, which blanks types in place); the gaps it leaves are closed, and a check against the untidied output confirms the token stream is identical and no line break that matters to ASI moved. Strip mode refuses three constructs that emit code, so they are first rewritten into plain TypeScript: parameter properties (constructor (private x)→this.x = x), explicit-value enums (LogLevel→ frozen object) and<T>exprcasts (dropped: no runtime effect). No file needs transform mode, which would reformat it.- Imports become explicit relative
.jsURLs. - Upstream imports some types without
import type(tsc elides them; stripping can't). The second pass drops names the target module doesn't export at runtime: 23 in all, listed when it runs. - Skipped, because they are already hand-converted above:
lib/components,lib/auth,lib/dialogs,lib/forms,lib/ns,lib/timing,lib/values.
CodeMirror and Lezer load from esm.sh with every dependency external (* prefix) and are pinned once in src/importmap.js at upstream's lockfile versions, so there is exactly one @codemirror/state. jsdelivr's +esm builds pin six different ones, which breaks the editor.
src/solid-ui.js is upstream's src/index.ts (the package entry), written by hand: its lib/auth, lib/components and lib/dialogs re-exports point at the hand-converted modules. customElement is a plain function there: customElement(tag)(Class). Each src/components/<name>/index.js mirrors upstream's index.ts for that component.
Not carried over: storybook/ (replaced by the demo pages), types/ (declarations only), cdn/ (two re-export stubs), and generated versionInfo.ts.
panes/<name>/ holds a pane converted with the same tool and a config of its own:
node tools/convert.mjs tools/profile-pane.json # → panes/profile-pane/
python3 tools/check-bare-imports.py panes/profile-pane/src # type-only names imported from packages → config "bareTypes"Beyond what solid-ui needed, panes use Vite's asset imports, which the converter maps to the platform:
| Vite | No build |
|---|---|
import './x.css' (injected into <head>) |
importCss(new URL('./x.css', import.meta.url)) → a <link> |
import form from './x.ttl' (raw plugin) |
const form = await (await fetch(new URL('./x.ttl', import.meta.url))).text() |
import icon from '../icons-png/x.png' |
const icon = new URL('../icons-png/x.png', import.meta.url).href |
More that the shell needed:
| Build feature | No build |
|---|---|
Lit decorators: @property/@state() accessor x = v |
static properties + this.x = v in the constructor (class fields would shadow Lit's accessors) |
@consume / @provide |
ContextConsumer / ContextProvider + a get/set that calls setValue, as Lit's decorators do |
@query('sel'), @customElement('tag') |
a renderRoot.querySelector getter; customElements.define after the class |
import styles from './x.styles.css' (lit-css) |
await loadSheet(new URL(…)), a CSSStyleSheet |
import '~icons/lucide/x' (unplugin-icons) |
the package's icons.js, SVGs fetched once from Iconify, same icon-lucide-x elements |
@media (--solid-ui-mobile) (@custom-media) |
the query substituted, as their CSS build does |
@import "solid-ui/theme.css" in CSS |
a relative URL (no importmap applies to CSS) |
tsconfig baseUrl imports (src/…), generated versionInfo.ts |
config aliases, generated |
bareModules in a config points solid-ui and solid-ui/components/* at the local files, so type-only names imported from them are caught too.
-
solid-panes (v5.0.0, 72 files, ~17,600 lines): the outline, header, navbar, file-explorer header and the panes it defines itself. Its 9 Lit components use decorators; the converter rewrites them (below).
argument/argumentPane.jsfails to load on its own (CommonJSmodule.__dirname), as upstream: it is commented out inregisterPanes.jsand not in their bundle either. -
mashlib (v3.0.0, 4 files): the shell. One recorded patch:
window.panesis a copy of the solid-panes namespace, because a real module namespace is sealed (webpack's isn't) andrunDataBrowseris added to it. -
profile-pane (v4.0.0, 94 files, ~15,000 lines): renders the real test profile (
testingsolidos.solidcommunity.net) in both layouts;pane.html?pane=profile-pane&smokeruns 7 checks. 175 type-only names dropped, all files in strip mode. Panes rely on the host page for.sr-only(mashlib'smash-utilities.css); the sandbox provides it. -
folder-pane, source-pane, chat-pane, contacts-pane (v4.0.0, Vite + TypeScript) and issue-pane, meeting-pane (v4.0.0, webpack + babel): converted with the same tool (
tools/<pane>.json). The webpack ones import Turtle throughbabel-plugin-inline-import(import { formText } from './form.ttl') andrequire('./x.ttl')inline; both become fetched text.
Only activitystreams-pane (React) still loads as a published build: src/importmap.js maps it to esm.sh with every dependency external (* prefix), so solid-ui, rdflib, lit, … resolve to the single copies mapped here, and React 19 is mapped once. pane-registry is CommonJS that builds its exports at runtime, so src/vendor/pane-registry.js re-exports its named members.
?published=issue-pane,contacts-pane on any page swaps those panes back to their published npm builds. That is how each upstream finding below was checked: same behaviour with upstream's own build means it is not the conversion.
JSS serves an ES-module data browser with --mashlib-module <url>: for every resource a browser navigates to it returns a bare page that loads that module. mashlib/boot.js is that module; it adds the importmap and the databrowser.html markup and starts mashlib. Put this tree in the pod and SolidOS is served by the pod it browses:
jss start --port 8732 --host 127.0.0.1 --root ./jss-data --idp --conneg \
--single-user --single-user-name me --single-user-password "$PW" \
--mashlib-module http://127.0.0.1:8732/me/public/solidos/mashlib/boot.js
ln -s "$PWD/nobuild" jss-data/me/public/solidos # JSS follows symlinked folders: edit, reload--single-user-name meputs the pod at/me/. At the server root, JSS's landingindex.htmlis the folder's representation, so/can't be browsed as a folder.- Use
127.0.0.1, notlocalhost: the IdP's issuer is the address JSS binds to. tests/pod.mjslogs in through the IdP (skipping the passkey offer, allowing consent), edits the profile bio and checkscard.jsonldon disk, mints a tracker, address book, notepad, meeting, long chat, chat and schedule and opens each, and sends a chat message and checks it is written.
Found while converting and testing; each reproduced with upstream's own code or build.
| Where | What | Status |
|---|---|---|
solid-ui 5.0.1 rdf-form |
With a formUrl that has a #fragment (which findForm supports), parts are looked up in a graph named by the full URL, so the form renders empty |
Fixed here; patch ready (patches/solid-ui-rdf-form-fragment.patch) |
solid-ui 5.0.1 widgets/forms.js |
The legacy "Multiple" field logs list.elements before creating the list, so on a new resource it throws and never adds the minimum blank items (seen opening a new schedule) |
Fixed here (one line moved) |
| issue-pane 4.0.0 | A tracker it has just minted shows "Tracker settings need an update": fixSubClasses looks for wf:Task's owl:disjointUnionOf in the tracker's document, but it is only in the workflow ontology |
Same with the published build |
| JSS 0.0.212 + SolidOS | JSS accepts ACL documents only as JSON-LD (415 + Accept-Put for Turtle), by design; SolidOS writes ACLs as Turtle (long chat, address book, schedule, the sharing editor) |
Client-side fallback here (lib/write-fallback.js) |
| JSS 0.0.212 | The ACL reader read literal acl: keys, so a valid expanded (or differently prefixed) JSON-LD ACL was stored and granted nothing, locking the owner out |
Fixed upstream: JSS #631 / PR #632 (merged; expands JSON-LD, refuses PUT/PATCH of ACLs that grant no mode to anyone). Verified with tests/pod.mjs against the merged code |
| profile-pane 4.0.0 | SocialData: SocialPresentation is literal text inside the social card's markup (a type annotation pasted into a template) |
Harmless |
| pane-registry 5.0.1 | Published as CommonJS that assigns its exports at runtime, so ESM consumers (and CDNs) can't get named exports | Shimmed here |
| solid-ui 5.0.1 (built CSS) | The Tailwind reset in every component's :host sets font-family: ui-sans-serif, …, overriding the theme's Atkinson Hyperlegible for everything inside a component, panes included (they render in solid-panes' file-explorer component) |
Same with upstream's build; reproduced faithfully |
| solid-panes 5.0.0 | argument/argumentPane.js uses module.__dirname; dead code (commented out in registerPanes.js) |
Left as is |