yamlover 0.3.10 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -143
- package/bin/yamlover.js +51 -2
- package/dist/agent-docs/AGENTS.md +267 -0
- package/dist/agent-docs/CLAUDE.md +7 -0
- package/dist/builtin-taxonomy/$defs/.yamlover/meta.yamlover +25 -0
- package/dist/builtin-taxonomy/$defs/annotation +18 -0
- package/dist/builtin-taxonomy/$defs/board +15 -0
- package/dist/builtin-taxonomy/$defs/chapter +19 -0
- package/dist/builtin-taxonomy/$defs/chunk +6 -0
- package/dist/builtin-taxonomy/$defs/config +20 -0
- package/dist/builtin-taxonomy/$defs/fragment +44 -0
- package/dist/builtin-taxonomy/$defs/tag +17 -0
- package/dist/builtin-taxonomy/$defs/task +33 -0
- package/dist/builtin-taxonomy/$defs/workflow +18 -0
- package/dist/builtin-taxonomy/tags/.yamlover/body.yamlover +46 -0
- package/dist/client/assets/{decoded-M-9MjrR3.js → decoded-DVR13gJ5.js} +1 -1
- package/dist/client/assets/{djvu-M6ksPMNz.js → djvu-CZbDJKwH.js} +1 -1
- package/dist/client/assets/{docx-D4h81KWC.js → docx-BWH54ddd.js} +1 -1
- package/dist/client/assets/{heic-BoSpW_tk.js → heic-CV3oZ6BF.js} +1 -1
- package/dist/client/assets/{imagemap-CIJFPvXv.js → imagemap-DRJrF_aD.js} +1 -1
- package/dist/client/assets/index-CCUPHCB4.js +620 -0
- package/dist/client/assets/{index-DHqicFJy.css → index-D5xvCkZE.css} +1 -1
- package/dist/client/assets/{map-MATRMboW.js → map-Y_zmEjLV.js} +1 -1
- package/dist/client/assets/{paged-BQLUFBor.js → paged-DKLVDozm.js} +1 -1
- package/dist/client/assets/{panzoom-Do4jskMZ.js → panzoom-NbtFbptF.js} +1 -1
- package/dist/client/assets/{pdf-BShpl2D5.js → pdf-DM1MQkYa.js} +1 -1
- package/dist/client/assets/{psd-DIxfuhrV.js → psd-PXeZB_nC.js} +1 -1
- package/dist/client/assets/{spreadsheet-DL0NoDcm.js → spreadsheet-B-3t2XCi.js} +1 -1
- package/dist/client/assets/{tiff-C7zPNKoD.js → tiff-BQq-nYyI.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server.js +545 -339
- package/package.json +1 -1
- package/dist/client/assets/index-DZ1FuhId.js +0 -620
package/README.md
CHANGED
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
Browse a yamlover tree in the web browser.
|
|
4
4
|
|
|
5
5
|
```console
|
|
6
|
-
$ npx yamlover [ROOT] [--port N] [--host ADDR] [--no-gitignore]
|
|
6
|
+
$ npx yamlover [ROOT] [--port N] [--headless] [--host ADDR] [--no-gitignore] [--prod]
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
`ROOT` is any yamlover entity — a directory with a `.yamlover
|
|
10
|
-
plain directory, or a single file
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
`ROOT` is any yamlover entity — a project directory (one with a `.yamlover/`),
|
|
10
|
+
a plain directory, or a single file. It defaults to the current directory. The
|
|
11
|
+
command starts a local web server — **bound to `127.0.0.1` (local only) by
|
|
12
|
+
default**, the safe default for a personal viewer and for the desktop wrapper
|
|
13
|
+
(`tools/desktop`) — and serves a React single-page app that browses the entity
|
|
14
|
+
starting from `ROOT`. Pass `--headless` to bind `0.0.0.0` (all interfaces, e.g.
|
|
15
|
+
for remote access with no GUI), or `--host ADDR` for an explicit override.
|
|
16
|
+
`--prod` forces the prebuilt static client + bundled server (the default when
|
|
17
|
+
the dev sources aren't present, e.g. an installed package); without it, a
|
|
18
|
+
checkout runs the client from source via Vite with HMR.
|
|
15
19
|
|
|
16
20
|
The page is split into two independently scrolling panes:
|
|
17
21
|
|
|
@@ -20,164 +24,206 @@ The page is split into two independently scrolling panes:
|
|
|
20
24
|
`05-scalar-as-file` is listed and clickable). A node is *expandable* when it is
|
|
21
25
|
a container with children and has no *active* renderer (see *Renderers*); the
|
|
22
26
|
first three levels load expanded, and deeper branches load lazily via the
|
|
23
|
-
chevron. Each entry is labeled by its
|
|
24
|
-
|
|
27
|
+
chevron. Each entry is labeled by its `title` when present, otherwise by its
|
|
28
|
+
key (objects) or `[index]` (arrays).
|
|
25
29
|
|
|
26
|
-
Each row carries a **type/format icon** (chosen by the
|
|
27
|
-
back to
|
|
30
|
+
Each row carries a **type/format icon** (chosen by the node's `format`, falling
|
|
31
|
+
back to its type — `{}`/`[]` for object/array, `"`/`#`/`◧` for
|
|
28
32
|
string/number/boolean, 📅/✉️/🔗/🖼️… for formats). The tree is agnostic to a
|
|
29
33
|
node's *concrete* (how it is stored) — that shows in the node view's tags.
|
|
30
|
-
- **Right — the selected node
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- **
|
|
34
|
-
- **
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
preserved as you navigate and is shareable.
|
|
42
|
-
|
|
43
|
-
A binary leaf's bytes are read only when you select it and view its value
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
- **Right — the selected node.** A node with a registered renderer shows that
|
|
35
|
+
renderer's view; every node also offers a set of **data-representation tabs**,
|
|
36
|
+
always including:
|
|
37
|
+
- **yamlover** *(default)* — the node's value in yamlover (YAML-family) syntax.
|
|
38
|
+
- **yamlover/schema** — the node's instance schema.
|
|
39
|
+
|
|
40
|
+
plus **json5p** for a JSON-family file. All representations behave
|
|
41
|
+
**identically**: syntax-highlighted, **one level deep**, with every nested node
|
|
42
|
+
shown as a **hyperlink** you click to descend (never inlined) —
|
|
43
|
+
`{ object with N properties }`, `[ array with M items ]`, or
|
|
44
|
+
`< binary of N bytes >`. The selected representation is part of the URL
|
|
45
|
+
(`?format=`), so it is preserved as you navigate and is shareable.
|
|
46
|
+
|
|
47
|
+
A binary leaf's bytes are read only when you select it and view its value.
|
|
48
|
+
|
|
49
|
+
The page updates **live**: an FS watcher re-indexes on external edits and pushes
|
|
50
|
+
what changed over an SSE stream (`/api/events`), so every surface (tree, node
|
|
51
|
+
view, tag pages) refreshes without a reload.
|
|
52
|
+
|
|
53
|
+
Surfaced *stray* files (those not described by the tree) honor `.gitignore` by
|
|
48
54
|
default — `node_modules/`, build output, etc. are hidden. Pass `--no-gitignore`
|
|
49
|
-
to show everything.
|
|
55
|
+
to show everything.
|
|
50
56
|
|
|
51
57
|
The browser URL is the node's path in **JSON space** — `/examples[0]/markup` —
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
the TOC along the path and selects (and scrolls to) the target node.
|
|
58
|
+
plus `?format=` for the representation. Each key is percent-encoded, so a key
|
|
59
|
+
that itself contains a `/` (e.g. `@vitejs/plugin-react`) stays a single segment.
|
|
60
|
+
It updates as you navigate and is shareable / back-button friendly — opening a
|
|
61
|
+
deep link expands the TOC along the path and selects (and scrolls to) the target.
|
|
57
62
|
|
|
58
63
|
## How it works
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
`
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
The server is backed by the yamlover **engine** (`tools/engine`): `walkDir`
|
|
66
|
+
turns the directory concrete into the parser's IR, and a `Store` (a SQLite
|
|
67
|
+
property-graph index) holds nodes and pointer edges. The HTTP layer
|
|
68
|
+
(`src/server/engine-api.ts`) reads from the `Store` and emits the response
|
|
69
|
+
shapes the React client consumes (the `$yamloverLink` / `$yamloverBinary` /
|
|
70
|
+
`$yamloverMixed` markers, the schema view).
|
|
71
|
+
|
|
72
|
+
The on-disk index lives at `<root>/.yamlover/index.db`. It is a derived cache
|
|
73
|
+
with a persistent **file manifest** (path + hash + size + mtime): startup
|
|
74
|
+
re-indexes against it (an offline reconcile — unchanged blobs are never re-read,
|
|
75
|
+
so it is cheap), and the FS watcher re-indexes on edits and broadcasts the diff
|
|
76
|
+
over `/api/events`.
|
|
77
|
+
|
|
78
|
+
**Long-running work runs as background tasks** (`src/server/tasks.ts`): the HTTP
|
|
79
|
+
server listens immediately and serves the previous index (or an empty one on a
|
|
80
|
+
cold start) while the initial walk and the background hasher (which fills in
|
|
81
|
+
content hashes for large blobs the walk no longer reads) run. Store-mutating
|
|
82
|
+
jobs (index, mv, paste, annotate) serialize through one writer queue; reads
|
|
83
|
+
never wait. Progress lands both on the console and in the web UI (SSE `task`
|
|
84
|
+
frames + `GET /api/tasks`).
|
|
85
|
+
|
|
86
|
+
Leaf bytes are read **lazily**: materialization builds structure but does not
|
|
87
|
+
read a leaf's content until that node is serialized.
|
|
88
|
+
|
|
89
|
+
### Build
|
|
90
|
+
|
|
91
|
+
There are two run modes:
|
|
92
|
+
|
|
93
|
+
- **Dev** (a checkout): the launcher runs [Vite](https://vitejs.dev) in
|
|
94
|
+
middleware mode, serving the client from source with HMR and loading the
|
|
95
|
+
server handler through Vite's `ssrLoadModule`.
|
|
96
|
+
- **Prod** (`--prod`, or an installed package): the client is a prebuilt static
|
|
97
|
+
SPA under `dist/client` and the server handler is a single bundle at
|
|
98
|
+
`dist/server.js`. `scripts/build.mjs` produces both — `vite build` for the
|
|
99
|
+
client and `esbuild` for the server (bundling the engine, parser, and the
|
|
100
|
+
`ignore` / `js-yaml` / `xxhash-wasm` deps, plus the dynamically-imported
|
|
101
|
+
thumbnail codecs). It runs on `prepack`, so the published npm package ships
|
|
102
|
+
`dist/` and has **no runtime dependencies**.
|
|
73
103
|
|
|
74
104
|
### API
|
|
75
105
|
|
|
76
|
-
All endpoints take a JSON-space `path` (default `/`);
|
|
77
|
-
optional `depth` (container-nesting limit).
|
|
106
|
+
All endpoints take a JSON-space `path` (default `/`); the value/schema endpoints
|
|
107
|
+
take an optional `depth` (container-nesting limit).
|
|
78
108
|
|
|
79
109
|
| endpoint | returns |
|
|
80
110
|
|----------|---------|
|
|
81
|
-
| `GET /api/info` |
|
|
82
|
-
| `GET /api/tree?path&depth` | the
|
|
83
|
-
| `GET /api/json?path&depth` | the node's value, one level deep (`depth` default 1); nested containers become link markers (
|
|
84
|
-
| `GET /api/schema?path&depth` | the node's instance
|
|
85
|
-
| `GET /api/blob?path` | a file-backed node's **raw bytes**, with its (inferred) format as the `Content-Type`
|
|
111
|
+
| `GET /api/info` | the breadcrumb head (the root label) |
|
|
112
|
+
| `GET /api/tree?path&depth` | the TOC subtree at `path`, `depth` levels deep (default 3) — fetched again per branch for lazy expansion |
|
|
113
|
+
| `GET /api/json?path&depth&binary` | the node's value, one level deep (`depth` default 1); nested containers become link markers (`&binary=1` for a binary leaf's base64) |
|
|
114
|
+
| `GET /api/schema?path&depth` | the node's instance schema, with the same link markers |
|
|
115
|
+
| `GET /api/blob?path` | a file-backed node's **raw bytes**, with its (inferred) format as the `Content-Type` |
|
|
116
|
+
| `GET /api/thumb?path&w&h` | a lazily-generated thumbnail of a file-backed blob |
|
|
117
|
+
| `GET /api/tagged?path` | the materials filed under a tag (annotations → targets) |
|
|
118
|
+
| `GET /api/annotations?path` | the annotations on a node |
|
|
119
|
+
| `GET /api/query?q&path` | the query evaluator (colon match templates) |
|
|
120
|
+
| `GET /api/dangling` | pointers that did not resolve at index time |
|
|
121
|
+
| `GET /api/events` | SSE: `{type:"diff",…}` reindex diffs + `{type:"task",…}` progress |
|
|
122
|
+
| `GET /api/tasks` | long-running tasks in flight (a snapshot for a fresh page) |
|
|
123
|
+
| `POST /api/reindex` | manual reconcile (the watcher's fallback) |
|
|
124
|
+
| `POST /api/paste` | clipboard paste / upload (text or files) |
|
|
125
|
+
| `POST /api/mv` | mediated move (surgical inbound-ref rewrite + auto-relink) |
|
|
126
|
+
| `POST /api/tag` | create-on-miss a tag in the taxonomy |
|
|
127
|
+
| `POST / DELETE /api/annotate` | add / remove an annotation on a target |
|
|
128
|
+
| `POST /api/fragment` | upsert a fragment (region of a target) |
|
|
129
|
+
| `POST /api/board` | board mutations |
|
|
130
|
+
| `POST /api/agent-docs` | write the LLM-agent guide (`AGENTS.md` + `CLAUDE.md`) into the root |
|
|
86
131
|
|
|
87
132
|
A **link marker** — `{ "$yamloverLink": { kind, path, count|size } }` — stands in
|
|
88
133
|
for a node shown only as a link (a nested container past the one-level view, or
|
|
89
134
|
any binary leaf); the client renders it as a `{ object with N properties }`,
|
|
90
135
|
`[ array with M items ]`, or `< binary of N bytes >` hyperlink. The same marker
|
|
91
|
-
appears in both the value and the schema, so every representation
|
|
92
|
-
|
|
93
|
-
|
|
136
|
+
appears in both the value and the schema, so every representation renders
|
|
137
|
+
identically. A selected binary leaf's bytes arrive as
|
|
138
|
+
`{ "$yamloverBinary": {format,size,base64} }`.
|
|
94
139
|
|
|
95
140
|
### Renderers
|
|
96
141
|
|
|
97
|
-
A renderer
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
142
|
+
A renderer declares an **`accepts` predicate** over a node's **type facets**
|
|
143
|
+
(value-type, `format`, and the keyed/ordinal capability flags) — most are
|
|
144
|
+
`byFormat("…")`, which matches on the node's format and tolerates the other
|
|
145
|
+
facets (so tagging a markdown node doesn't break its markdown rendering). The
|
|
146
|
+
most specific matching renderer wins; a node with no match falls through to the
|
|
147
|
+
default data-representation tabs and expands normally in the TOC.
|
|
101
148
|
|
|
102
|
-
The registry lives in `src/client/renderers/`. A renderer participates
|
|
103
|
-
|
|
149
|
+
The registry lives in `src/client/renderers/`. A renderer participates several
|
|
150
|
+
ways:
|
|
104
151
|
|
|
105
152
|
- **`render`** — the full RHS page (its tab is the node's default representation).
|
|
106
|
-
- **`renderChunk`** — its *inline* form, when embedded in another renderer's page
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
chunk's renderer (a `string`/`text/markdown` chunk → the `text` renderer; an
|
|
110
|
-
`image/png` chunk would route to an image renderer, no change to `chapter`).
|
|
153
|
+
- **`renderChunk`** — its *inline* form, when embedded in another renderer's page
|
|
154
|
+
(e.g. the `chapter` renderer draws each chunk by delegating to that chunk's
|
|
155
|
+
own renderer).
|
|
111
156
|
- **`tocView`** — how the node appears in the TOC: which children are navigable,
|
|
112
|
-
whether it expands
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
|
123
|
-
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
157
|
+
whether it expands and is loaded. A renderer can unwrap or filter (e.g.
|
|
158
|
+
`chapter` surfaces its subchapters and keeps its chunks off the tree).
|
|
159
|
+
- **`depth`** — the value depth `NodeView` fetches for it (default 1).
|
|
160
|
+
|
|
161
|
+
Registered today (a representative slice — the registry is the source of truth):
|
|
162
|
+
|
|
163
|
+
| renderer | matches (format) | draws with |
|
|
164
|
+
|----------|------------------|------------|
|
|
165
|
+
| `chapter` | `x-yamlover-chapter` | numbered chunks + subchapter links |
|
|
166
|
+
| `tag` / `board` | `x-yamlover-tag` / `x-yamlover-board` | tag-hierarchy diagram / board (handled outside the specificity loop) |
|
|
167
|
+
| `task` | `x-yamlover-task` | task view |
|
|
168
|
+
| `markdown` / `marklower` | `text/markdown` | [marked](https://marked.js.org) |
|
|
169
|
+
| `asciidoc` | `text/asciidoc` | [@asciidoctor/core](https://asciidoctor.org) |
|
|
170
|
+
| `csv` | `text/csv`, `text/tab-separated-values` | a table |
|
|
171
|
+
| `plaintext` | `text/plain` | preformatted text |
|
|
172
|
+
| `latex` | `text/x-latex` | [KaTeX](https://katex.org) |
|
|
173
|
+
| `plantuml` | `text/x-plantuml` | rendered diagram |
|
|
174
|
+
| `map` | KML / KMZ | [Leaflet](https://leafletjs.com) |
|
|
175
|
+
| `image` | `image/png`, `jpeg`, `gif`, `webp`, `avif`, `svg+xml`, … | native `<img>` |
|
|
176
|
+
| `html` | `text/html` | sandboxed `<iframe>` |
|
|
177
|
+
| `pdf` | `application/pdf` | [pdf.js](https://mozilla.github.io/pdf.js/) via react-pdf |
|
|
178
|
+
| `djvu` | `image/vnd.djvu` | [DjVu.js](https://djvu.js.org) (vendored) |
|
|
179
|
+
| `epub` / `fb2` | `application/epub+zip` / FictionBook | paged e-book view |
|
|
180
|
+
| `docx` / `doc` / `rtf` | Word / RTF | [mammoth](https://github.com/mwilliamson/mammoth.js) etc. |
|
|
181
|
+
| `spreadsheet` | xlsx / xls | [SheetJS](https://sheetjs.com) |
|
|
182
|
+
| `psd` / `tiff` / `heic` | Photoshop / TIFF / HEIC | decoded to a canvas/image |
|
|
132
183
|
|
|
133
184
|
Adding a shape is still a single registry entry.
|
|
134
185
|
|
|
186
|
+
Two implementation notes:
|
|
187
|
+
|
|
188
|
+
- **DjVu has no native browser support**, so it is decoded client-side by
|
|
189
|
+
**DjVu.js**, vendored as a prebuilt bundle at `src/client/vendor/djvu.js`.
|
|
190
|
+
The library is **GPL-v2** (see `src/client/vendor/README.md` for provenance);
|
|
191
|
+
the rest of this package is not.
|
|
192
|
+
- **pdf.js and DjVu.js (and several heavier codecs) reach for browser globals at
|
|
193
|
+
import time**, which would break the (Node/jsdom) test run, so their renderers
|
|
194
|
+
are **lazy-loaded** — importing the registry never pulls them in until such a
|
|
195
|
+
node is actually shown.
|
|
196
|
+
|
|
135
197
|
### File rendering and format inference
|
|
136
198
|
|
|
137
|
-
The
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
`file/binary` — renders without a `format:` line. An explicit schema `format`
|
|
143
|
-
always wins.
|
|
199
|
+
The file renderers turn the browser into a viewer for the common file types a
|
|
200
|
+
tree carries. They hang off one rule, applied to any file-backed node that
|
|
201
|
+
carries **no explicit `format`**: the server **infers a format from the file
|
|
202
|
+
extension**. So a stray `.pdf`, `.png`, or `.md` renders without a `format:`
|
|
203
|
+
line. An explicit `format` always wins.
|
|
144
204
|
|
|
145
205
|
Inference splits two ways by how the renderer consumes the file:
|
|
146
206
|
|
|
147
|
-
- **Served as bytes** — images, `application/pdf`, `text/html`, `image/vnd.djvu
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
207
|
+
- **Served as bytes** — images, `application/pdf`, `text/html`, `image/vnd.djvu`,
|
|
208
|
+
etc. Their renderer points an `<img>`/`<iframe>`/loader at **`/api/blob`** (or
|
|
209
|
+
fetches the `ArrayBuffer`), so the bytes stream straight from disk with no
|
|
210
|
+
base64 round-trip.
|
|
151
211
|
- **Read as text** — `.md`/`.adoc` keep a **string** value (the file's text),
|
|
152
|
-
which the
|
|
153
|
-
existing "a `.md` file is a string" behavior.)
|
|
154
|
-
|
|
155
|
-
Two implementation notes:
|
|
156
|
-
|
|
157
|
-
- **DjVu has no native browser support**, so it is decoded client-side by
|
|
158
|
-
**DjVu.js**, vendored as a prebuilt bundle at `src/client/vendor/djvu.js`.
|
|
159
|
-
The library is **GPL-v2** (see `src/client/vendor/README.md` for provenance and
|
|
160
|
-
how to regenerate); the rest of this package is not.
|
|
161
|
-
- **pdf.js and DjVu.js reach for browser globals at import time**, which would
|
|
162
|
-
break the (Node/jsdom) test run. So the `pdf` and `djvu` renderers are
|
|
163
|
-
**lazy-loaded** (`React.lazy`) — importing the registry, as the TOC and the
|
|
164
|
-
tests do, never pulls them in until a PDF/DjVu node is actually shown.
|
|
165
|
-
|
|
166
|
-
`examples/18-pdf-tags` exercises this: a library of real papers stored as PDFs
|
|
167
|
-
and one saved HTML page, each rendered in place.
|
|
212
|
+
which the renderer parses to HTML.
|
|
168
213
|
|
|
169
214
|
## Requirements
|
|
170
215
|
|
|
171
|
-
- Node.js
|
|
216
|
+
- Node.js **22+**.
|
|
172
217
|
|
|
173
|
-
|
|
174
|
-
`
|
|
175
|
-
|
|
218
|
+
There are **no runtime dependencies**: the client deps are bundled into
|
|
219
|
+
`dist/client` by `vite build` and the server deps into `dist/server.js` by
|
|
220
|
+
esbuild at `prepack`. The `devDependencies` (React, Vite, the engine/parser, and
|
|
221
|
+
the renderer libraries) are build- and test-time only. DjVu.js is **vendored**
|
|
176
222
|
(`src/client/vendor/djvu.js`), not an npm dependency.
|
|
177
223
|
|
|
178
224
|
## Tests
|
|
179
225
|
|
|
180
|
-
|
|
226
|
+
This package is covered by [Vitest](https://vitest.dev) (it runs the TypeScript
|
|
181
227
|
directly, no build):
|
|
182
228
|
|
|
183
229
|
```console
|
|
@@ -185,30 +231,27 @@ $ npm test # run once
|
|
|
185
231
|
$ npm run test:watch
|
|
186
232
|
```
|
|
187
233
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
`buildTree` (all nodes, depth, titles), `.gitignore` filtering, and the API
|
|
193
|
-
endpoints (`/api/info`, `/api/tree`, `/api/json`, `/api/schema`).
|
|
194
|
-
- **Client** (`test/client/*`, jsdom + React Testing Library) — path/URL helpers
|
|
195
|
-
and breadcrumbs, type/format icons, the renderer registry, the unified
|
|
196
|
-
`Render` (scalars, `{ object with N properties }` / `[ array with M items ]` /
|
|
197
|
-
`< binary of N bytes >` links, `!!binary`, YAML vs JSON), the `Tree`
|
|
198
|
-
(selection, lazy expand, leaves), `NodeView` (markers, tab switches, binary,
|
|
199
|
-
schema), and `App` (breadcrumb head + TOC).
|
|
234
|
+
This server suite is **separate** from the repository root's `npm test`, which
|
|
235
|
+
runs only the parser and engine suites. CI gates on the server suite, so run
|
|
236
|
+
both before pushing. (Run everything from the project root — see the root
|
|
237
|
+
`package.json`.)
|
|
200
238
|
|
|
201
239
|
## Layout
|
|
202
240
|
|
|
203
241
|
```
|
|
204
|
-
bin/yamlover.js
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
242
|
+
bin/yamlover.js CLI entry — arg parsing + dev (Vite) / prod (dist) wiring
|
|
243
|
+
scripts/build.mjs prod build: vite build → dist/client, esbuild → dist/server.js
|
|
244
|
+
src/server/ the engine-backed JSON API
|
|
245
|
+
engine-api.ts createHandlers: all /api/* routes, backed by the engine Store
|
|
246
|
+
embed.ts annotation / fragment / thumbnail embedding (overlay writes)
|
|
247
|
+
node-kind.ts node-kind classification (object|array|scalar|binary|omni|mix)
|
|
248
|
+
tasks.ts background task registry + SSE task frames
|
|
249
|
+
gitignore.ts .gitignore predicate for surfaced stray files
|
|
250
|
+
extract/ per-type extractors (thumbnails, fragments)
|
|
251
|
+
agent-docs/ the AGENTS.md / CLAUDE.md guide installed by POST /api/agent-docs
|
|
252
|
+
src/client/ the React SPA (tree, node view, render, icons, paths, live SSE)
|
|
253
|
+
renderers/ facet-predicate renderer registry + per-format renderers
|
|
254
|
+
vendor/djvu.js prebuilt DjVu.js bundle (GPL-v2; see vendor/README.md)
|
|
255
|
+
test/ Vitest suite (server logic + client components)
|
|
256
|
+
index.html SPA shell
|
|
214
257
|
```
|
package/bin/yamlover.js
CHANGED
|
@@ -48,6 +48,20 @@ let port = 5173;
|
|
|
48
48
|
let host = "127.0.0.1";
|
|
49
49
|
let gitignore = true; // hide .gitignore'd stray files by default
|
|
50
50
|
let prodFlag = false; // force production (static) mode even in the repo checkout
|
|
51
|
+
// URL prefix to serve the whole app under (e.g. `/demo/abc123`), so many instances can sit behind
|
|
52
|
+
// one host on distinct paths (the demo server). "" = served at the document root (the normal case).
|
|
53
|
+
// Seeded from $BASE_PATH so a shell-less image (e.g. distroless) can inject it via env without
|
|
54
|
+
// needing `sh -c` to expand it into a `--base-path` flag; an explicit flag below still overrides.
|
|
55
|
+
let basePath = process.env.BASE_PATH ?? "";
|
|
56
|
+
// Normalize a base path: leading `/`, no trailing `/`; `""`/`"/"` → disabled.
|
|
57
|
+
function normBase(s) {
|
|
58
|
+
let b = (s ?? "").trim();
|
|
59
|
+
if (b === "" || b === "/") return "";
|
|
60
|
+
if (!b.startsWith("/")) b = "/" + b;
|
|
61
|
+
if (b.endsWith("/")) b = b.slice(0, -1);
|
|
62
|
+
return b;
|
|
63
|
+
}
|
|
64
|
+
basePath = normBase(basePath); // normalize the $BASE_PATH seed (a flag below re-normalizes its own value)
|
|
51
65
|
const argv = process.argv.slice(2);
|
|
52
66
|
for (let i = 0; i < argv.length; i++) {
|
|
53
67
|
const a = argv[i];
|
|
@@ -56,11 +70,14 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
56
70
|
else if (a === "--headless") host = "0.0.0.0"; // serve on all interfaces (no GUI / remote access)
|
|
57
71
|
else if (a === "--host") host = argv[++i];
|
|
58
72
|
else if (a.startsWith("--host=")) host = a.slice("--host=".length);
|
|
73
|
+
else if (a === "--base-path") basePath = normBase(argv[++i]);
|
|
74
|
+
else if (a.startsWith("--base-path=")) basePath = normBase(a.slice("--base-path=".length));
|
|
59
75
|
else if (a === "--no-gitignore") gitignore = false;
|
|
60
76
|
else if (a === "--prod") prodFlag = true;
|
|
61
77
|
else if (a === "--help" || a === "-h") {
|
|
62
|
-
console.log("usage: npx yamlover [ROOT] [--port N] [--headless] [--host ADDR] [--no-gitignore] [--prod]");
|
|
78
|
+
console.log("usage: npx yamlover [ROOT] [--port N] [--headless] [--host ADDR] [--base-path PREFIX] [--no-gitignore] [--prod]");
|
|
63
79
|
console.log(" default: serve on 127.0.0.1 (local only); --headless serves on all interfaces");
|
|
80
|
+
console.log(" --base-path PREFIX: serve the whole app under PREFIX (e.g. /demo/abc) instead of /");
|
|
64
81
|
process.exit(0);
|
|
65
82
|
} else if (!a.startsWith("-")) rootArg = a;
|
|
66
83
|
}
|
|
@@ -85,7 +102,21 @@ let handle;
|
|
|
85
102
|
let serveClient;
|
|
86
103
|
|
|
87
104
|
const server = createHttpServer((req, res) => {
|
|
88
|
-
|
|
105
|
+
let url = new URL(req.url, "http://localhost");
|
|
106
|
+
// Under `--base-path`, strip the prefix up front (and rewrite req.url) so every downstream —
|
|
107
|
+
// the engine API (exact `/api/...` matches), the static server, and the Vite middleware — sees
|
|
108
|
+
// root-relative paths and stays oblivious to the prefix. Anything outside the prefix is 404.
|
|
109
|
+
if (basePath) {
|
|
110
|
+
const p = url.pathname;
|
|
111
|
+
if (p === basePath || p.startsWith(basePath + "/")) {
|
|
112
|
+
req.url = (p.slice(basePath.length) || "/") + url.search;
|
|
113
|
+
url = new URL(req.url, "http://localhost");
|
|
114
|
+
} else {
|
|
115
|
+
res.statusCode = 404;
|
|
116
|
+
res.end("not found");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
89
120
|
if (url.pathname.startsWith("/api/")) {
|
|
90
121
|
handle(req, res, url);
|
|
91
122
|
return;
|
|
@@ -193,6 +224,7 @@ if (prod) {
|
|
|
193
224
|
try {
|
|
194
225
|
let html = fs.readFileSync(indexHtmlPath, "utf-8");
|
|
195
226
|
html = await vite.transformIndexHtml(url.pathname, html);
|
|
227
|
+
html = injectBase(html); // base-path-aware shell (no-op without --base-path)
|
|
196
228
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
197
229
|
res.end(html);
|
|
198
230
|
} catch (e) {
|
|
@@ -210,6 +242,7 @@ if (prod) {
|
|
|
210
242
|
handle = createHandlers(dataRoot, {
|
|
211
243
|
gitignore,
|
|
212
244
|
watch: true, // re-index + push on external edits
|
|
245
|
+
ensureSettings: true, // create .yamlover/settings.yamlover with defaults if absent (so the gear opens)
|
|
213
246
|
log: (line) => console.log(`yamlover ${line}`),
|
|
214
247
|
});
|
|
215
248
|
handle.ready.catch((e) => console.error("yamlover: indexing failed:", e));
|
|
@@ -262,9 +295,25 @@ function serveStatic(res, url, distClient, distIndex) {
|
|
|
262
295
|
function serveIndex(res, distIndex) {
|
|
263
296
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
264
297
|
res.setHeader("Cache-Control", "no-cache");
|
|
298
|
+
if (basePath) {
|
|
299
|
+
// The shell must learn its prefix (the client prepends it to every server URL) and its
|
|
300
|
+
// root-absolute asset refs must point under the prefix (the strip above maps them back).
|
|
301
|
+
res.end(injectBase(fs.readFileSync(distIndex, "utf-8")));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
265
304
|
fs.createReadStream(distIndex).pipe(res);
|
|
266
305
|
}
|
|
267
306
|
|
|
307
|
+
/** Make a served index.html base-path-aware: expose `window.__BASE__` for the client's URL helper
|
|
308
|
+
* and prefix root-absolute `src="/…"` / `href="/…"` asset refs with the base path (protocol-relative
|
|
309
|
+
* `//…` left alone). No-op when no base path is set. */
|
|
310
|
+
function injectBase(html) {
|
|
311
|
+
if (!basePath) return html;
|
|
312
|
+
html = html.replace(/((?:src|href)=")\/(?!\/)/g, `$1${basePath}/`);
|
|
313
|
+
const tag = `<script>window.__BASE__=${JSON.stringify(basePath)}</script>`;
|
|
314
|
+
return html.includes("<head>") ? html.replace("<head>", `<head>${tag}`) : tag + html;
|
|
315
|
+
}
|
|
316
|
+
|
|
268
317
|
// Listen on `port`; if it is already in use, fall back to the next port (up to
|
|
269
318
|
// `MAX_PORT_TRIES`), so two instances — or a leftover one — don't collide.
|
|
270
319
|
const shown = host === "0.0.0.0" || host === "::" ? "localhost" : host;
|