yamlover 0.3.0
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/LICENSE +21 -0
- package/README.md +214 -0
- package/bin/yamlover.js +202 -0
- package/dist/server.js +4681 -0
- package/index.html +12 -0
- package/package.json +72 -0
- package/src/client/App.tsx +372 -0
- package/src/client/NodeView.tsx +422 -0
- package/src/client/TaskStrip.tsx +34 -0
- package/src/client/Tree.tsx +97 -0
- package/src/client/api.ts +186 -0
- package/src/client/icons.ts +91 -0
- package/src/client/links.tsx +108 -0
- package/src/client/live.ts +42 -0
- package/src/client/main.tsx +10 -0
- package/src/client/paste-html.ts +228 -0
- package/src/client/paste-links.ts +42 -0
- package/src/client/paths.ts +109 -0
- package/src/client/render.tsx +326 -0
- package/src/client/renderers/annotate.tsx +507 -0
- package/src/client/renderers/asciidoc.tsx +35 -0
- package/src/client/renderers/chapter.tsx +138 -0
- package/src/client/renderers/csv.tsx +233 -0
- package/src/client/renderers/decoded.tsx +72 -0
- package/src/client/renderers/djvu.tsx +97 -0
- package/src/client/renderers/doc.tsx +40 -0
- package/src/client/renderers/docx.tsx +49 -0
- package/src/client/renderers/epub.tsx +147 -0
- package/src/client/renderers/explorer.tsx +209 -0
- package/src/client/renderers/fb2.tsx +149 -0
- package/src/client/renderers/headings.ts +69 -0
- package/src/client/renderers/heic.tsx +23 -0
- package/src/client/renderers/imagemap.tsx +157 -0
- package/src/client/renderers/kml.ts +46 -0
- package/src/client/renderers/latex.tsx +36 -0
- package/src/client/renderers/map.tsx +205 -0
- package/src/client/renderers/marklower.tsx +119 -0
- package/src/client/renderers/markup.tsx +64 -0
- package/src/client/renderers/media.tsx +19 -0
- package/src/client/renderers/panzoom.ts +101 -0
- package/src/client/renderers/pdf.tsx +176 -0
- package/src/client/renderers/plaintext.tsx +120 -0
- package/src/client/renderers/plantuml.tsx +82 -0
- package/src/client/renderers/psd.tsx +25 -0
- package/src/client/renderers/registry.tsx +389 -0
- package/src/client/renderers/rtf.tsx +210 -0
- package/src/client/renderers/spreadsheet.tsx +105 -0
- package/src/client/renderers/tag.tsx +113 -0
- package/src/client/renderers/text.tsx +41 -0
- package/src/client/renderers/tiff.tsx +33 -0
- package/src/client/styles.css +1115 -0
- package/src/client/vendor/README.md +30 -0
- package/src/client/vendor/djvu.js +15535 -0
- package/src/client/vite-env.d.ts +31 -0
- package/src/server/api.ts +147 -0
- package/src/server/engine-api.ts +1442 -0
- package/src/server/gitignore.ts +81 -0
- package/src/server/node-kind.ts +48 -0
- package/src/server/tasks.ts +83 -0
- package/src/server/yamlover.ts +1133 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dims
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# server
|
|
2
|
+
|
|
3
|
+
Browse a yamlover tree in the web browser.
|
|
4
|
+
|
|
5
|
+
```console
|
|
6
|
+
$ npx yamlover [ROOT] [--port N] [--host ADDR] [--no-gitignore]
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`ROOT` is any yamlover entity — a directory with a `.yamlover/schema.yaml`, a
|
|
10
|
+
plain directory, or a single file (the same shapes [`walker`](../walker/)
|
|
11
|
+
understands). It defaults to the current directory. The command starts a local
|
|
12
|
+
web server (on `0.0.0.0` by default, so it is reachable from the network — use
|
|
13
|
+
`--host 127.0.0.1` to keep it private) and serves a React single-page app that
|
|
14
|
+
lets you browse the entity starting from `ROOT`.
|
|
15
|
+
|
|
16
|
+
The page is split into two independently scrolling panes:
|
|
17
|
+
|
|
18
|
+
- **Left — a table of contents.** A collapsible tree of **every** node — object
|
|
19
|
+
keys and array elements alike, scalars included (so a leaf like
|
|
20
|
+
`05-scalar-as-file` is listed and clickable). A node is *expandable* when it is
|
|
21
|
+
a container with children and has no *active* renderer (see *Renderers*); the
|
|
22
|
+
first three levels load expanded, and deeper branches load lazily via the
|
|
23
|
+
chevron. Each entry is labeled by its schema `title` (or an instance `title`
|
|
24
|
+
child) when present, otherwise by its key (objects) or `[index]` (arrays).
|
|
25
|
+
|
|
26
|
+
Each row carries a **type/format icon** (chosen by the schema `format`, falling
|
|
27
|
+
back to `type` — `{}`/`[]` for object/array, `"`/`#`/`◧` for
|
|
28
|
+
string/number/boolean, 📅/✉️/🔗/🖼️… for formats). The tree is agnostic to a
|
|
29
|
+
node's *concrete* (how it is stored) — that shows in the node view's tags.
|
|
30
|
+
- **Right — the selected node**, in one of four representations (tabs):
|
|
31
|
+
- **yaml-schema** *(default)* — our schema (the yamlover schema concrete) as YAML.
|
|
32
|
+
- **yaml** — the node's value as YAML.
|
|
33
|
+
- **json** — the node's value as JSON.
|
|
34
|
+
- **json-schema** — the standard instance JSON Schema (every leaf `const`).
|
|
35
|
+
|
|
36
|
+
All four behave **identically**: syntax-highlighted, **one level deep**, with
|
|
37
|
+
every nested node shown as a **hyperlink** you click to descend (never inlined)
|
|
38
|
+
— `{ object with N properties }`, `[ array with M items ]`, or
|
|
39
|
+
`< binary of N bytes >`. A registered renderer, if any, replaces the value
|
|
40
|
+
views. The selected representation is part of the URL (`?format=`), so it is
|
|
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: the
|
|
44
|
+
yaml view shows a YAML `!!binary` block, json shows the `{format,size,base64}`
|
|
45
|
+
metadata.
|
|
46
|
+
|
|
47
|
+
Surfaced *stray* files (those the schema does not describe) honor `.gitignore` by
|
|
48
|
+
default — `node_modules/`, build output, etc. are hidden. Pass `--no-gitignore`
|
|
49
|
+
to show everything. Schema-described children are always shown.
|
|
50
|
+
|
|
51
|
+
The browser URL is the node's path in **JSON space** — `/examples[0]/markup` —
|
|
52
|
+
never schema space, so there are no `properties` segments, plus `?format=` for
|
|
53
|
+
the representation. Each key is percent-encoded, so a key that itself contains a
|
|
54
|
+
`/` (e.g. `@vitejs/plugin-react`) stays a single segment. It updates as you
|
|
55
|
+
navigate and is shareable / back-button friendly — opening a deep link expands
|
|
56
|
+
the TOC along the path and selects (and scrolls to) the target node.
|
|
57
|
+
|
|
58
|
+
## How it works
|
|
59
|
+
|
|
60
|
+
This is a TypeScript reimplementation of the read side of [`walker`](../walker/)
|
|
61
|
+
(concretes, `$ref`/`$defs`, inline `const`, collapsed files, expanded
|
|
62
|
+
subdirectories), plus a small server and SPA. There is **no build step**: the
|
|
63
|
+
launcher runs [Vite](https://vitejs.dev) in middleware mode, serving the client
|
|
64
|
+
from source with HMR and loading the server-side materializer through Vite's
|
|
65
|
+
`ssrLoadModule`.
|
|
66
|
+
|
|
67
|
+
Leaf bytes are **read lazily**: materialization builds the tree's structure but
|
|
68
|
+
does not read a leaf's content until that node is actually serialized. So listing
|
|
69
|
+
or eliding a node never reads its file — `value: 30` is read only when its node is
|
|
70
|
+
shown, and a binary blob only when its base64 is requested. The materialized tree
|
|
71
|
+
is cached per server for a short window, so a burst of clicks does not re-read the
|
|
72
|
+
filesystem; edits show up after the window on reload.
|
|
73
|
+
|
|
74
|
+
### API
|
|
75
|
+
|
|
76
|
+
All endpoints take a JSON-space `path` (default `/`); `json` and `schema` take an
|
|
77
|
+
optional `depth` (container-nesting limit).
|
|
78
|
+
|
|
79
|
+
| endpoint | returns |
|
|
80
|
+
|----------|---------|
|
|
81
|
+
| `GET /api/info` | `{ root }` — the root label (the served entity's yamlover title, else its directory name): the breadcrumb head and TOC root |
|
|
82
|
+
| `GET /api/tree?path&depth` | the table of contents rooted at `path`, `depth` levels deep (default 3) — fetched again per branch for lazy expansion |
|
|
83
|
+
| `GET /api/json?path&depth` | the node's value, one level deep (`depth` default 1); nested containers become link markers (add `&binary=1` for a binary leaf's base64) |
|
|
84
|
+
| `GET /api/schema?path&depth` | the node's instance JSON Schema, one level deep, with the same link markers |
|
|
85
|
+
| `GET /api/blob?path` | a file-backed node's **raw bytes**, with its (inferred) format as the `Content-Type` — what the image / html / pdf / djvu / markup renderers read, instead of base64 over `json` |
|
|
86
|
+
|
|
87
|
+
A **link marker** — `{ "$yamloverLink": { kind, path, count|size } }` — stands in
|
|
88
|
+
for a node shown only as a link (a nested container past the one-level view, or
|
|
89
|
+
any binary leaf); the client renders it as a `{ object with N properties }`,
|
|
90
|
+
`[ 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 (YAML/JSON ×
|
|
92
|
+
data/schema) renders identically — just a syntax choice over one structure. A
|
|
93
|
+
selected binary leaf's bytes arrive as `{ "$yamloverBinary": {format,size,base64} }`.
|
|
94
|
+
|
|
95
|
+
### Renderers
|
|
96
|
+
|
|
97
|
+
A renderer is keyed by a **(type, format)** tuple — the JSON-Schema `type` plus
|
|
98
|
+
its `format` — the same key the TOC icons and the link markers carry. A node with
|
|
99
|
+
no matching renderer falls through to the default tabbed view (YAML/JSON ×
|
|
100
|
+
data/schema) and expands normally in the TOC.
|
|
101
|
+
|
|
102
|
+
The registry lives in `src/client/renderers/`. A renderer participates three ways,
|
|
103
|
+
all on the one tuple:
|
|
104
|
+
|
|
105
|
+
- **`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
|
+
A renderer composes children by routing each to the renderer for the child's own
|
|
108
|
+
tuple: the `chapter` renderer draws each of its `chunks` by delegating to that
|
|
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`).
|
|
111
|
+
- **`tocView`** — how the node appears in the TOC: which children are navigable,
|
|
112
|
+
whether it expands, and whether they are loaded (default: its own children,
|
|
113
|
+
lazily loaded). A renderer can unwrap or filter — `chapter` surfaces its
|
|
114
|
+
subchapters directly (from its `children` array) and keeps its `chunks` off the
|
|
115
|
+
tree, so the TOC reads as a table of contents.
|
|
116
|
+
|
|
117
|
+
A renderer may also declare **`depth`** — the value depth `NodeView` fetches for
|
|
118
|
+
it (default 1; `chapter` needs 2 to reach its chunk/subchapter elements).
|
|
119
|
+
|
|
120
|
+
Registered today:
|
|
121
|
+
|
|
122
|
+
| renderer | (type, format) | draws with |
|
|
123
|
+
|----------|----------------|------------|
|
|
124
|
+
| `chapter` | `object` / `x-yamlover-chapter` | numbered chunks + subchapter links |
|
|
125
|
+
| `tag` | `object` / `x-yamlover-tag` | a hierarchy diagram: supertag (containment parent) and subtags (children) wired to the current tag |
|
|
126
|
+
| `text` | `string` / `text/markdown` | [marked](https://marked.js.org) |
|
|
127
|
+
| `asciidoc` | `string` / `text/asciidoc` | [@asciidoctor/core](https://asciidoctor.org) |
|
|
128
|
+
| `image` | `binary` / `image/*` | native `<img>` |
|
|
129
|
+
| `html` | `binary` / `text/html` | sandboxed `<iframe>` |
|
|
130
|
+
| `pdf` | `binary` / `application/pdf` | [pdf.js](https://mozilla.github.io/pdf.js/) via [react-pdf](https://github.com/wojtekmaj/react-pdf) |
|
|
131
|
+
| `djvu` | `binary` / `image/vnd.djvu` | [DjVu.js](https://djvu.js.org) (vendored) |
|
|
132
|
+
|
|
133
|
+
Adding a shape is still a single registry entry.
|
|
134
|
+
|
|
135
|
+
### File rendering and format inference
|
|
136
|
+
|
|
137
|
+
The bottom seven renderers above turn the browser into a viewer for the common
|
|
138
|
+
file types a tree carries. They hang off one rule, applied to any file-backed
|
|
139
|
+
node that carries **no explicit schema `format`**: the server **infers a format
|
|
140
|
+
from the file extension** (`formatFromExt` in `src/server/yamlover.ts`). So a
|
|
141
|
+
stray `.pdf`, `.png`, or `.md` — or one declared with only `concrete: file`/
|
|
142
|
+
`file/binary` — renders without a `format:` line. An explicit schema `format`
|
|
143
|
+
always wins.
|
|
144
|
+
|
|
145
|
+
Inference splits two ways by how the renderer consumes the file:
|
|
146
|
+
|
|
147
|
+
- **Served as bytes** — images, `application/pdf`, `text/html`, `image/vnd.djvu`
|
|
148
|
+
become `file/binary`. Their renderer points an `<img>`/`<iframe>`/pdf-loader at
|
|
149
|
+
**`/api/blob`** (or, for DjVu, fetches the `ArrayBuffer`), so the bytes stream
|
|
150
|
+
straight from disk with no base64 round-trip.
|
|
151
|
+
- **Read as text** — `.md`/`.adoc` keep a **string** value (the file's text),
|
|
152
|
+
which the `text`/`asciidoc` renderer parses to HTML. (This preserves 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.
|
|
168
|
+
|
|
169
|
+
## Requirements
|
|
170
|
+
|
|
171
|
+
- Node.js 18+
|
|
172
|
+
|
|
173
|
+
Dependencies (React, Vite, js-yaml, ignore, plus the renderer libraries
|
|
174
|
+
`react-pdf`, `marked`, `@asciidoctor/core`) install with `npm install` in this
|
|
175
|
+
directory; `npx yamlover` then runs `bin/yamlover.js`. DjVu.js is **vendored**
|
|
176
|
+
(`src/client/vendor/djvu.js`), not an npm dependency.
|
|
177
|
+
|
|
178
|
+
## Tests
|
|
179
|
+
|
|
180
|
+
Both sides are covered by [Vitest](https://vitest.dev) (it runs the TypeScript
|
|
181
|
+
directly, no build):
|
|
182
|
+
|
|
183
|
+
```console
|
|
184
|
+
$ npm test # run once
|
|
185
|
+
$ npm run test:watch
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- **Server** (`test/*.test.ts`, Node) — against the `examples/` fixtures: path
|
|
189
|
+
encoding round-trips (incl. keys with `/`), materialization and `$ref`/`$defs`
|
|
190
|
+
equivalence, one-level link markers (object/array/binary) in value and schema,
|
|
191
|
+
lazy binaries and the `!!binary` payload, the non-YAML → raw-text fallback,
|
|
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).
|
|
200
|
+
|
|
201
|
+
## Layout
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
bin/yamlover.js CLI entry — arg parsing + Vite middleware-mode server
|
|
205
|
+
src/server/ TypeScript port of the walker read side + JSON API
|
|
206
|
+
yamlover.ts materialize a logical tree (lazy leaves); toPlain/toSchema/buildTree; formatFromExt
|
|
207
|
+
gitignore.ts .gitignore predicate for surfaced stray files
|
|
208
|
+
api.ts /api/info, /api/tree, /api/json, /api/schema, /api/blob
|
|
209
|
+
src/client/ the React SPA (tree, node view, render, icons, paths)
|
|
210
|
+
renderers/ (type, format) registry + chapter/text/asciidoc/media/pdf/djvu
|
|
211
|
+
vendor/djvu.js prebuilt DjVu.js bundle (GPL-v2; see vendor/README.md)
|
|
212
|
+
test/ Vitest suite for the server logic
|
|
213
|
+
index.html SPA shell
|
|
214
|
+
```
|
package/bin/yamlover.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* yamlover — serve a directory of yamlover data as a browsable React SPA.
|
|
4
|
+
*
|
|
5
|
+
* npx yamlover [ROOT] [--port N]
|
|
6
|
+
*
|
|
7
|
+
* ROOT is the directory to browse (default: the current directory). It may be a
|
|
8
|
+
* yamlover node (with `.yamlover/`), a plain directory, or a single file — the
|
|
9
|
+
* same shapes `tools/walker` understands.
|
|
10
|
+
*
|
|
11
|
+
* The server runs Vite in middleware mode: the client (`src/client`) is served
|
|
12
|
+
* with HMR straight from source, and the server-side materializer
|
|
13
|
+
* (`src/server`) is loaded through Vite's `ssrLoadModule`, so there is no build
|
|
14
|
+
* step. API routes are handled before Vite; every other route falls back to the
|
|
15
|
+
* SPA's index.html (client-side routing on the JSON path).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createServer } from "vite";
|
|
19
|
+
import react from "@vitejs/plugin-react";
|
|
20
|
+
import { createServer as createHttpServer } from "node:http";
|
|
21
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
22
|
+
import { createRequire } from "node:module";
|
|
23
|
+
import { dirname, join, resolve } from "node:path";
|
|
24
|
+
import fs from "node:fs";
|
|
25
|
+
|
|
26
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const pkgRoot = resolve(__dirname, ".."); // tools/server
|
|
28
|
+
|
|
29
|
+
// Resolve react/react-dom as *this package* sees them, and alias Vite to those
|
|
30
|
+
// exact copies, so the SPA never picks up a stale React from a parent
|
|
31
|
+
// node_modules (which surfaces as "react-dom … does not provide an export named
|
|
32
|
+
// 'createRoot'" — createRoot is React 18+). Best-effort: if resolution fails we
|
|
33
|
+
// leave Vite to its defaults.
|
|
34
|
+
const reactAlias = {};
|
|
35
|
+
// Directories Vite is allowed to serve over `/@fs` (its file-system allowlist).
|
|
36
|
+
// Start with this package; below we add the node_modules that actually holds the
|
|
37
|
+
// heavy deps. Under `npx`, deps hoist to a `_npx/<hash>/node_modules` *outside*
|
|
38
|
+
// `pkgRoot`, and Vite's default workspace-root detection misses it — so a raw asset
|
|
39
|
+
// fetch (notably pdf.js's `pdf.worker.min.mjs`) gets denied and falls through to
|
|
40
|
+
// index.html, surfacing as the "non-JavaScript MIME type text/html" worker error.
|
|
41
|
+
const fsAllow = [pkgRoot];
|
|
42
|
+
try {
|
|
43
|
+
const req = createRequire(join(pkgRoot, "package.json"));
|
|
44
|
+
reactAlias["react"] = dirname(req.resolve("react/package.json"));
|
|
45
|
+
reactAlias["react-dom"] = dirname(req.resolve("react-dom/package.json"));
|
|
46
|
+
// Allow whichever node_modules each heavy dep resolves from (hoisted under npx,
|
|
47
|
+
// or nested in dev) so Vite serves their worker/asset files over `/@fs`.
|
|
48
|
+
for (const dep of ["pdfjs-dist", "react-pdf", "leaflet"]) {
|
|
49
|
+
try {
|
|
50
|
+
const nodeModules = dirname(dirname(req.resolve(`${dep}/package.json`)));
|
|
51
|
+
if (!fsAllow.includes(nodeModules)) fsAllow.push(nodeModules);
|
|
52
|
+
} catch {
|
|
53
|
+
/* dep not resolvable from here — skip */
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
/* fall back to default resolution */
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// --- argument parsing ----------------------------------------------------- //
|
|
61
|
+
let rootArg = null; // the ROOT path as typed (null when omitted)
|
|
62
|
+
let port = 5173;
|
|
63
|
+
let host = "0.0.0.0"; // all interfaces by default — reachable from the network
|
|
64
|
+
let gitignore = true; // hide .gitignore'd stray files by default
|
|
65
|
+
const argv = process.argv.slice(2);
|
|
66
|
+
for (let i = 0; i < argv.length; i++) {
|
|
67
|
+
const a = argv[i];
|
|
68
|
+
if (a === "--port" || a === "-p") port = parseInt(argv[++i], 10);
|
|
69
|
+
else if (a.startsWith("--port=")) port = parseInt(a.slice("--port=".length), 10);
|
|
70
|
+
else if (a === "--host") host = argv[++i];
|
|
71
|
+
else if (a.startsWith("--host=")) host = a.slice("--host=".length);
|
|
72
|
+
else if (a === "--no-gitignore") gitignore = false;
|
|
73
|
+
else if (a === "--help" || a === "-h") {
|
|
74
|
+
console.log("usage: npx yamlover [ROOT] [--port N] [--host ADDR] [--no-gitignore]");
|
|
75
|
+
process.exit(0);
|
|
76
|
+
} else if (!a.startsWith("-")) rootArg = a;
|
|
77
|
+
}
|
|
78
|
+
const dataRoot = resolve(process.cwd(), rootArg ?? ".");
|
|
79
|
+
if (!fs.existsSync(dataRoot)) {
|
|
80
|
+
console.error(`yamlover: no such path: ${dataRoot}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- server --------------------------------------------------------------- //
|
|
85
|
+
// `vite` and `handle` are filled in just below; the request handler closes over
|
|
86
|
+
// them and only reads them once requests start arriving, after assignment.
|
|
87
|
+
let vite;
|
|
88
|
+
let handle;
|
|
89
|
+
const indexHtmlPath = join(pkgRoot, "index.html");
|
|
90
|
+
|
|
91
|
+
const server = createHttpServer((req, res) => {
|
|
92
|
+
const url = new URL(req.url, "http://localhost");
|
|
93
|
+
if (url.pathname.startsWith("/api/")) {
|
|
94
|
+
handle(req, res, url);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
vite.middlewares(req, res, async () => {
|
|
98
|
+
// SPA fallback: serve the (transformed) index.html for any client route.
|
|
99
|
+
try {
|
|
100
|
+
let html = fs.readFileSync(indexHtmlPath, "utf-8");
|
|
101
|
+
html = await vite.transformIndexHtml(url.pathname, html);
|
|
102
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
103
|
+
res.end(html);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
vite.ssrFixStacktrace(e);
|
|
106
|
+
res.statusCode = 500;
|
|
107
|
+
res.end(e.message);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Run HMR over our own server (`hmr: { server }`) so Vite does not open a
|
|
113
|
+
// second WebSocket port of its own.
|
|
114
|
+
vite = await createServer({
|
|
115
|
+
root: pkgRoot,
|
|
116
|
+
plugins: [react()],
|
|
117
|
+
appType: "custom",
|
|
118
|
+
// Always use this package's own React: `dedupe` collapses react/react-dom to a
|
|
119
|
+
// single copy, and the aliases pin that copy to the one resolvable from here —
|
|
120
|
+
// so a stale react-dom in a *parent* node_modules (the classic "does not
|
|
121
|
+
// provide an export named 'createRoot'" failure) cannot shadow it.
|
|
122
|
+
resolve: { dedupe: ["react", "react-dom"], alias: reactAlias },
|
|
123
|
+
// Pre-bundle the heavy renderer deps up front. These are all lazy-loaded, so
|
|
124
|
+
// Vite would otherwise discover them (and their transitive CJS deps such as
|
|
125
|
+
// `warning` for react-pdf or `base64-js` for ag-psd) only on first use and could
|
|
126
|
+
// serve them un-interopped — the "does not provide an export named …" failure.
|
|
127
|
+
// Listing them forces a clean CJS→ESM bundle before any renderer mounts.
|
|
128
|
+
optimizeDeps: {
|
|
129
|
+
include: [
|
|
130
|
+
"react-pdf",
|
|
131
|
+
"marked",
|
|
132
|
+
"@asciidoctor/core",
|
|
133
|
+
"ag-psd",
|
|
134
|
+
"utif",
|
|
135
|
+
"heic2any",
|
|
136
|
+
// the office/map renderer deps (all CJS/UMD — they need the CJS→ESM interop
|
|
137
|
+
// a pre-bundle gives, or they are served raw and fail with "does not provide
|
|
138
|
+
// an export named 'default'", e.g. `import L from "leaflet"`)
|
|
139
|
+
"leaflet",
|
|
140
|
+
"xlsx",
|
|
141
|
+
"mammoth/mammoth.browser",
|
|
142
|
+
"@tmcw/togeojson",
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
// `allowedHosts: true` lifts Vite's Host-header allowlist so the SPA is
|
|
146
|
+
// reachable from the network (any hostname/IP), matching the 0.0.0.0 bind.
|
|
147
|
+
// `fs.allow` is widened (above) to the dirs holding the heavy deps so their
|
|
148
|
+
// `/@fs` assets — chiefly pdf.js's worker — are served, not 404'd to index.html.
|
|
149
|
+
server: { middlewareMode: true, allowedHosts: true, hmr: { server }, fs: { allow: fsAllow } },
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Load the server-side materializer. The engine-backed handler (engine-api.ts) supersedes the
|
|
153
|
+
// legacy loadEntity materializer (api.ts kept for ref). It reaches OUTSIDE this package
|
|
154
|
+
// (../../../engine, ../../../parser), which the npm tarball cannot carry — so at prepack we bundle
|
|
155
|
+
// that import graph into a self-contained `dist/server.js` (scripts/build.mjs). We pick the loader
|
|
156
|
+
// by whether the engine SOURCE is reachable: in the repo CHECKOUT it is, so we use Vite's
|
|
157
|
+
// `ssrLoadModule` to transpile the TS live (edits take effect with no rebuild — never a stale
|
|
158
|
+
// bundle); in the PUBLISHED package the source is absent, so we import the prebuilt bundle. Either
|
|
159
|
+
// way the client SPA is still served by Vite from src.
|
|
160
|
+
// The initial index runs as a BACKGROUND task — the server listens immediately (serving the
|
|
161
|
+
// previous on-disk index, or an empty tree on a cold start) while progress lands here and in
|
|
162
|
+
// the web UI (SSE task frames + GET /api/tasks).
|
|
163
|
+
const engineSrc = resolve(pkgRoot, "../engine/ts/src/index.ts"); // present only in the monorepo
|
|
164
|
+
const { createHandlers } = fs.existsSync(engineSrc)
|
|
165
|
+
? await vite.ssrLoadModule("/src/server/engine-api.ts") // repo: live TS, no rebuild needed
|
|
166
|
+
: await import(pathToFileURL(join(pkgRoot, "dist/server.js")).href); // published: prebuilt bundle
|
|
167
|
+
handle = createHandlers(dataRoot, {
|
|
168
|
+
gitignore,
|
|
169
|
+
watch: true, // re-index + push on external edits
|
|
170
|
+
log: (line) => console.log(`yamlover ${line}`),
|
|
171
|
+
});
|
|
172
|
+
handle.ready.catch((e) => console.error("yamlover: indexing failed:", e));
|
|
173
|
+
|
|
174
|
+
// Listen on `port`; if it is already in use, fall back to the next port (up to
|
|
175
|
+
// `MAX_PORT_TRIES`), so two instances — or a leftover one — don't collide.
|
|
176
|
+
const shown = host === "0.0.0.0" || host === "::" ? "localhost" : host;
|
|
177
|
+
const MAX_PORT_TRIES = 50;
|
|
178
|
+
|
|
179
|
+
function listenWithFallback(p, triesLeft) {
|
|
180
|
+
const onError = (err) => {
|
|
181
|
+
server.off("listening", onListening); // drop this attempt's success handler
|
|
182
|
+
if (err && err.code === "EADDRINUSE" && triesLeft > 0) {
|
|
183
|
+
console.log(`yamlover port ${p} in use — trying ${p + 1}…`);
|
|
184
|
+
listenWithFallback(p + 1, triesLeft - 1);
|
|
185
|
+
} else if (err && err.code === "EADDRINUSE") {
|
|
186
|
+
console.error(`yamlover: no free port found in ${port}–${p}`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
} else {
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const onListening = () => {
|
|
193
|
+
server.off("error", onError); // bound OK — stop intercepting listen errors
|
|
194
|
+
console.log(`yamlover serving ${dataRoot}`);
|
|
195
|
+
console.log(` http://${shown}:${p}/ (bound to ${host})`);
|
|
196
|
+
};
|
|
197
|
+
server.once("error", onError);
|
|
198
|
+
server.once("listening", onListening);
|
|
199
|
+
server.listen(p, host);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
listenWithFallback(port, MAX_PORT_TRIES);
|