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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/bin/yamlover.js +202 -0
  4. package/dist/server.js +4681 -0
  5. package/index.html +12 -0
  6. package/package.json +72 -0
  7. package/src/client/App.tsx +372 -0
  8. package/src/client/NodeView.tsx +422 -0
  9. package/src/client/TaskStrip.tsx +34 -0
  10. package/src/client/Tree.tsx +97 -0
  11. package/src/client/api.ts +186 -0
  12. package/src/client/icons.ts +91 -0
  13. package/src/client/links.tsx +108 -0
  14. package/src/client/live.ts +42 -0
  15. package/src/client/main.tsx +10 -0
  16. package/src/client/paste-html.ts +228 -0
  17. package/src/client/paste-links.ts +42 -0
  18. package/src/client/paths.ts +109 -0
  19. package/src/client/render.tsx +326 -0
  20. package/src/client/renderers/annotate.tsx +507 -0
  21. package/src/client/renderers/asciidoc.tsx +35 -0
  22. package/src/client/renderers/chapter.tsx +138 -0
  23. package/src/client/renderers/csv.tsx +233 -0
  24. package/src/client/renderers/decoded.tsx +72 -0
  25. package/src/client/renderers/djvu.tsx +97 -0
  26. package/src/client/renderers/doc.tsx +40 -0
  27. package/src/client/renderers/docx.tsx +49 -0
  28. package/src/client/renderers/epub.tsx +147 -0
  29. package/src/client/renderers/explorer.tsx +209 -0
  30. package/src/client/renderers/fb2.tsx +149 -0
  31. package/src/client/renderers/headings.ts +69 -0
  32. package/src/client/renderers/heic.tsx +23 -0
  33. package/src/client/renderers/imagemap.tsx +157 -0
  34. package/src/client/renderers/kml.ts +46 -0
  35. package/src/client/renderers/latex.tsx +36 -0
  36. package/src/client/renderers/map.tsx +205 -0
  37. package/src/client/renderers/marklower.tsx +119 -0
  38. package/src/client/renderers/markup.tsx +64 -0
  39. package/src/client/renderers/media.tsx +19 -0
  40. package/src/client/renderers/panzoom.ts +101 -0
  41. package/src/client/renderers/pdf.tsx +176 -0
  42. package/src/client/renderers/plaintext.tsx +120 -0
  43. package/src/client/renderers/plantuml.tsx +82 -0
  44. package/src/client/renderers/psd.tsx +25 -0
  45. package/src/client/renderers/registry.tsx +389 -0
  46. package/src/client/renderers/rtf.tsx +210 -0
  47. package/src/client/renderers/spreadsheet.tsx +105 -0
  48. package/src/client/renderers/tag.tsx +113 -0
  49. package/src/client/renderers/text.tsx +41 -0
  50. package/src/client/renderers/tiff.tsx +33 -0
  51. package/src/client/styles.css +1115 -0
  52. package/src/client/vendor/README.md +30 -0
  53. package/src/client/vendor/djvu.js +15535 -0
  54. package/src/client/vite-env.d.ts +31 -0
  55. package/src/server/api.ts +147 -0
  56. package/src/server/engine-api.ts +1442 -0
  57. package/src/server/gitignore.ts +81 -0
  58. package/src/server/node-kind.ts +48 -0
  59. package/src/server/tasks.ts +83 -0
  60. 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
+ ```
@@ -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);