@diy-accounting-uk/diya-gl 1.2.42 → 1.2.43
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 +11 -1
- package/bin/diya-gl-view.js +9 -0
- package/bin/diya-gl.js +2 -1
- package/dist/app/bin/link.js +6 -2
- package/dist/app/bin/view.js +108 -0
- package/dist/app/data/releases.json +26 -0
- package/dist/app/lib/diya-gl-view-server.js +203 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -38,11 +38,12 @@ diya-gl recalc --package bst --data my-book --years se-2025-2026 --output-dir ou
|
|
|
38
38
|
diya-gl read-workbook --file my-workbook.xlsx --output-dir out
|
|
39
39
|
diya-gl write-workbook --data my-book --output-dir out
|
|
40
40
|
diya-gl link my-book
|
|
41
|
+
diya-gl view my-book
|
|
41
42
|
diya-gl mcp
|
|
42
43
|
```
|
|
43
44
|
|
|
44
45
|
Each subcommand is also its own command, if you only want one on your `PATH`:
|
|
45
|
-
`diya-gl-recalc`, `diya-gl-read-workbook`, `diya-gl-write-workbook`, `diya-gl-link`, `diya-gl-mcp`.
|
|
46
|
+
`diya-gl-recalc`, `diya-gl-read-workbook`, `diya-gl-write-workbook`, `diya-gl-link`, `diya-gl-view`, `diya-gl-mcp`.
|
|
46
47
|
|
|
47
48
|
- **recalc** takes a diya-gl book (`--data`) or a populated workbook (`--source-dir`,
|
|
48
49
|
`--mode saved|recalculate`) and writes `report.json`: every figure, report section and
|
|
@@ -57,6 +58,15 @@ Each subcommand is also its own command, if you only want one on your `PATH`:
|
|
|
57
58
|
opens it, the whole book carried in the fragment so nothing is uploaded. `--base <url>`
|
|
58
59
|
replaces the origin. The fragment length goes to stderr, with a warning past 64 KB (about
|
|
59
60
|
2,000 lines), where chat, email and QR codes start to cut links off.
|
|
61
|
+
- **view** takes the same book sources as **link**, serves the full diya-gl pages on
|
|
62
|
+
`127.0.0.1` (`--port <number>`, default a free port) and opens the book in your default
|
|
63
|
+
browser (`--no-open` prints the address only). The pages are not in this package: the local
|
|
64
|
+
server fetches each file from https://diya-gl.co.uk on first use and caches it under the
|
|
65
|
+
system temporary directory, one directory per package version, so the first run needs the
|
|
66
|
+
network and later runs of the same version reuse the cache. `--origin <url>` fetches from
|
|
67
|
+
another site. The book stays in the browser (the part of the address after `#` is never sent
|
|
68
|
+
anywhere). The server stays up until Ctrl-C. The workbook templates come from
|
|
69
|
+
spreadsheets.diyaccounting.co.uk the first time a page action needs one.
|
|
60
70
|
- **mcp** runs a stdio [MCP](https://modelcontextprotocol.io) server with four tools:
|
|
61
71
|
`extract_book`, `report`, `edit_lines`, `save_workbook`. Point an MCP client at
|
|
62
72
|
`diya-gl-mcp` (or `diya-gl mcp`) with no arguments.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// Copyright (C) 2006-2026 DIY Accounting Limited
|
|
4
|
+
//
|
|
5
|
+
// diya-gl-view.js — open a book in the full diya-gl pages, served locally
|
|
6
|
+
// from this package.
|
|
7
|
+
|
|
8
|
+
import { runDistBin } from "./run-dist-bin.js";
|
|
9
|
+
runDistBin("app/bin/view.js", process.argv.slice(2));
|
package/bin/diya-gl.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
// Copyright (C) 2006-2026 DIY Accounting Limited
|
|
4
4
|
//
|
|
5
|
-
// diya-gl.js — the dispatcher: diya-gl <recalc|read-workbook|write-workbook|link|mcp> [args].
|
|
5
|
+
// diya-gl.js — the dispatcher: diya-gl <recalc|read-workbook|write-workbook|link|view|mcp> [args].
|
|
6
6
|
// Each subcommand is also its own bin (diya-gl-recalc, and so on), for a
|
|
7
7
|
// caller that wants one command on its PATH without the others.
|
|
8
8
|
|
|
@@ -17,6 +17,7 @@ const SUBCOMMANDS = {
|
|
|
17
17
|
"read-workbook": "app/bin/export.js",
|
|
18
18
|
"write-workbook": "app/bin/write-workbook.js",
|
|
19
19
|
"link": "app/bin/link.js",
|
|
20
|
+
"view": "app/bin/view.js",
|
|
20
21
|
"mcp": "app/bin/diya-gl-mcp.js",
|
|
21
22
|
};
|
|
22
23
|
|
package/dist/app/bin/link.js
CHANGED
|
@@ -55,13 +55,17 @@ async function readBookText(source) {
|
|
|
55
55
|
return { toml: canonicalBookToml(book), lines: canonicalLinesJsonl(lines), book };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
export async function
|
|
58
|
+
export async function readBookFragment(source) {
|
|
59
59
|
const { toml, lines, book } = await readBookText(source);
|
|
60
60
|
const declared = book.entityInformation && book.entityInformation["diya-gl:product"];
|
|
61
61
|
const product = productIdOf(declared);
|
|
62
62
|
if (!product)
|
|
63
63
|
throw new Error(`The book declares no product with a page: entityInformation."diya-gl:product" is ${JSON.stringify(declared)}.`);
|
|
64
|
-
|
|
64
|
+
return { product, ...encodeBookFragment({ toml, lines }) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function bookLink(source, base = DEFAULT_BASE) {
|
|
68
|
+
const { product, fragment, length, warning } = await readBookFragment(source);
|
|
65
69
|
return { url: `${base}/${product}.html#${FRAGMENT_KEY}=${fragment}`, length, warning };
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// Copyright (C) 2006-2026 DIY Accounting Limited
|
|
4
|
+
//
|
|
5
|
+
// view.js — open a book in the full diya-gl pages, served on 127.0.0.1 by a
|
|
6
|
+
// caching proxy that fetches each page file from diya-gl.co.uk on first use.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// node app/bin/view.js examples/basic-taxi-driver
|
|
10
|
+
// node app/bin/view.js my-book-diya-gl.zip --port 8080 --no-open
|
|
11
|
+
//
|
|
12
|
+
// The book is any source link.js reads: a directory holding book.toml and
|
|
13
|
+
// lines.jsonl, or a diya-gl zip, JSON or workbook file. The server stays up
|
|
14
|
+
// until Ctrl-C. Fetched files are cached under the system temporary directory,
|
|
15
|
+
// one directory per package version and origin host.
|
|
16
|
+
|
|
17
|
+
import { spawn } from "child_process";
|
|
18
|
+
import { existsSync, readFileSync } from "fs";
|
|
19
|
+
import { tmpdir } from "os";
|
|
20
|
+
import { dirname, join, resolve } from "path";
|
|
21
|
+
import { fileURLToPath } from "url";
|
|
22
|
+
import { readBookFragment } from "./link.js";
|
|
23
|
+
import { FRAGMENT_KEY } from "../lib/diya-gl-link.js";
|
|
24
|
+
import { DEFAULT_ORIGIN, startViewServer } from "../lib/diya-gl-view-server.js";
|
|
25
|
+
|
|
26
|
+
const USAGE =
|
|
27
|
+
"Usage: diya-gl view <book directory | diya-gl zip | diya-gl JSON file | workbook> [--port <number>] [--origin <url>] [--no-open]";
|
|
28
|
+
|
|
29
|
+
// The nearest package.json above this file: the package's own in dist/, the
|
|
30
|
+
// repository's in a checkout. Both carry the version the cache is keyed on.
|
|
31
|
+
function packageVersion() {
|
|
32
|
+
let directory = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
for (;;) {
|
|
34
|
+
const candidate = join(directory, "package.json");
|
|
35
|
+
if (existsSync(candidate)) return JSON.parse(readFileSync(candidate, "utf8")).version;
|
|
36
|
+
const parent = dirname(directory);
|
|
37
|
+
if (parent === directory) throw new Error("No package.json found above " + import.meta.url);
|
|
38
|
+
directory = parent;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseArgs(argv) {
|
|
43
|
+
const args = argv.slice(2);
|
|
44
|
+
let port = 0;
|
|
45
|
+
let origin = DEFAULT_ORIGIN;
|
|
46
|
+
let open = true;
|
|
47
|
+
const positional = [];
|
|
48
|
+
for (let i = 0; i < args.length; i++) {
|
|
49
|
+
if (args[i] === "--port") {
|
|
50
|
+
const value = args[++i];
|
|
51
|
+
port = Number(value);
|
|
52
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535)
|
|
53
|
+
throw new Error(`--port needs a number from 0 to 65535, got "${value}". ${USAGE}`);
|
|
54
|
+
} else if (args[i] === "--origin") {
|
|
55
|
+
origin = args[++i];
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = new URL(origin);
|
|
59
|
+
} catch {
|
|
60
|
+
throw new Error(`--origin needs an http or https URL, got "${origin}". ${USAGE}`);
|
|
61
|
+
}
|
|
62
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
63
|
+
throw new Error(`--origin needs an http or https URL, got "${origin}". ${USAGE}`);
|
|
64
|
+
} else if (args[i] === "--no-open") {
|
|
65
|
+
open = false;
|
|
66
|
+
} else {
|
|
67
|
+
positional.push(args[i]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (positional.length !== 1) throw new Error(USAGE);
|
|
71
|
+
return { source: positional[0], port, origin, open };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function openInBrowser(url) {
|
|
75
|
+
const [command, args] =
|
|
76
|
+
process.platform === "darwin"
|
|
77
|
+
? ["open", [url]]
|
|
78
|
+
: process.platform === "win32"
|
|
79
|
+
? ["cmd", ["/c", "start", "", url]]
|
|
80
|
+
: ["xdg-open", [url]];
|
|
81
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
82
|
+
child.on("error", (error) => console.error(`Could not open a browser (${error.message}). Open the address above.`));
|
|
83
|
+
child.unref();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function main() {
|
|
87
|
+
const { source, port, origin, open } = parseArgs(process.argv);
|
|
88
|
+
const { product, fragment, length, warning } = await readBookFragment(source);
|
|
89
|
+
const cacheDir = join(tmpdir(), "diya-gl-view", packageVersion(), new URL(origin).host.replace(/[^A-Za-z0-9.-]/g, "_"));
|
|
90
|
+
const running = await startViewServer({ cacheDir, origin, port });
|
|
91
|
+
const url = `http://${running.host}:${running.port}/${product}.html#${FRAGMENT_KEY}=${fragment}`;
|
|
92
|
+
console.log(url);
|
|
93
|
+
console.error(`Fragment length: ${length} characters`);
|
|
94
|
+
if (warning) console.error(`Warning: ${warning}`);
|
|
95
|
+
console.error(`Fetching pages from ${running.origin} on first use.`);
|
|
96
|
+
console.error(`Cache directory: ${running.cacheDir}`);
|
|
97
|
+
console.error("Serving on 127.0.0.1 until you press Ctrl-C.");
|
|
98
|
+
if (open) openInBrowser(url);
|
|
99
|
+
|
|
100
|
+
process.on("SIGINT", () => {
|
|
101
|
+
running.close().then(() => process.exit(0));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
main().catch((error) => {
|
|
106
|
+
console.error(error.message);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
});
|
|
@@ -1325,6 +1325,32 @@
|
|
|
1325
1325
|
"scorecard": "101605 passed, 0 warnings, 0 failed"
|
|
1326
1326
|
}
|
|
1327
1327
|
}
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
"tag": "diya-gl-v1.2.42",
|
|
1331
|
+
"date": "2026-10-03",
|
|
1332
|
+
"formatVersion": "diya-gl/1",
|
|
1333
|
+
"engineVersion": "1.2.40+57de8f55b",
|
|
1334
|
+
"taxDataHash": "89424bd36c0d",
|
|
1335
|
+
"reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
|
|
1336
|
+
"templates": {
|
|
1337
|
+
"bst": {
|
|
1338
|
+
"hash": "0d003e10e00c",
|
|
1339
|
+
"scorecard": "812 passed, 0 warnings, 0 failed"
|
|
1340
|
+
},
|
|
1341
|
+
"taxi": {
|
|
1342
|
+
"hash": "1ff69654d64d",
|
|
1343
|
+
"scorecard": "700 passed, 0 warnings, 0 failed"
|
|
1344
|
+
},
|
|
1345
|
+
"se": {
|
|
1346
|
+
"hash": "064605ae76ed",
|
|
1347
|
+
"scorecard": "8014 passed, 0 warnings, 0 failed"
|
|
1348
|
+
},
|
|
1349
|
+
"ltd": {
|
|
1350
|
+
"hash": "5bb7be9b6d97",
|
|
1351
|
+
"scorecard": "101605 passed, 0 warnings, 0 failed"
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1328
1354
|
}
|
|
1329
1355
|
]
|
|
1330
1356
|
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2006-2026 DIY Accounting Limited
|
|
3
|
+
//
|
|
4
|
+
// diya-gl-view-server.js — a read-only caching proxy on 127.0.0.1 for the
|
|
5
|
+
// diya-gl pages. A GET or HEAD for /<path> is answered from the cache
|
|
6
|
+
// directory when it holds the file; otherwise the file is fetched from the
|
|
7
|
+
// origin site, stored with its content type, and served. A non-2xx answer
|
|
8
|
+
// from the origin passes through with its status and is not stored.
|
|
9
|
+
//
|
|
10
|
+
// The pages are not part of the npm package: they are fetched from the site
|
|
11
|
+
// on first use. The service worker script is answered 404, so the browser
|
|
12
|
+
// never layers a second cache over this one.
|
|
13
|
+
//
|
|
14
|
+
// The workbook templates are fetched from the template source (the same site
|
|
15
|
+
// the engine's own template loader uses) and streamed back unchanged.
|
|
16
|
+
|
|
17
|
+
import { createServer } from "http";
|
|
18
|
+
import { mkdir, readFile, realpath, rename, writeFile } from "fs/promises";
|
|
19
|
+
import { extname, resolve, sep } from "path";
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_ORIGIN = "https://diya-gl.co.uk";
|
|
22
|
+
export const DEFAULT_TEMPLATE_SOURCE = "https://spreadsheets.diyaccounting.co.uk/diya-gl/assets";
|
|
23
|
+
const TEMPLATE_URL_PREFIX = "/assets/templates/";
|
|
24
|
+
// A template is "meta.toml" or "<product>/<file>" with a workbook or TOML extension; nothing else
|
|
25
|
+
// is proxied, so the upstream URL is always the template source plus one of these names.
|
|
26
|
+
const TEMPLATE_NAME = /^(?:[a-z]+\/)?[A-Za-z0-9][A-Za-z0-9._-]*\.(?:xlsx|toml)$/;
|
|
27
|
+
|
|
28
|
+
const CONTENT_TYPES = {
|
|
29
|
+
".html": "text/html; charset=utf-8",
|
|
30
|
+
".js": "text/javascript; charset=utf-8",
|
|
31
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
32
|
+
".css": "text/css; charset=utf-8",
|
|
33
|
+
".json": "application/json; charset=utf-8",
|
|
34
|
+
".jsonl": "application/x-ndjson; charset=utf-8",
|
|
35
|
+
".toml": "text/plain; charset=utf-8",
|
|
36
|
+
".txt": "text/plain; charset=utf-8",
|
|
37
|
+
".svg": "image/svg+xml",
|
|
38
|
+
".png": "image/png",
|
|
39
|
+
".ico": "image/x-icon",
|
|
40
|
+
".webmanifest": "application/manifest+json",
|
|
41
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
42
|
+
".zip": "application/zip",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function send(response, status, body, contentType = "text/plain; charset=utf-8") {
|
|
46
|
+
response.writeHead(status, { "Content-Type": contentType, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
|
|
47
|
+
response.end(body);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const SERVICE_WORKER_PATH = "/sw.js";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The cache entry paths for a URL path, or null when the path would leave the
|
|
54
|
+
* cache directory. The body and its content type are stored side by side.
|
|
55
|
+
*/
|
|
56
|
+
function cacheEntryFor(cacheReal, urlPath) {
|
|
57
|
+
let decoded;
|
|
58
|
+
try {
|
|
59
|
+
decoded = decodeURIComponent(urlPath);
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (decoded.includes("\0") || decoded.includes("\\")) return null;
|
|
64
|
+
const segments = decoded.split("/").filter((segment) => segment !== "");
|
|
65
|
+
if (segments.some((segment) => segment === "." || segment === "..")) return null;
|
|
66
|
+
if (decoded.endsWith("/") || segments.length === 0) segments.push("index.html");
|
|
67
|
+
const body = resolve(cacheReal, "body", ...segments);
|
|
68
|
+
if (!body.startsWith(resolve(cacheReal, "body") + sep)) return null;
|
|
69
|
+
return { body, type: resolve(cacheReal, "type", ...segments) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function readIfPresent(path, cacheReal) {
|
|
73
|
+
try {
|
|
74
|
+
const real = await realpath(path);
|
|
75
|
+
if (!real.startsWith(cacheReal + sep)) return null;
|
|
76
|
+
return await readFile(real);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EISDIR") return null;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function writeAtomically(path, data) {
|
|
84
|
+
await mkdir(resolve(path, ".."), { recursive: true });
|
|
85
|
+
const temporary = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
86
|
+
await writeFile(temporary, data);
|
|
87
|
+
await rename(temporary, path);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function serveFromOrigin(request, response, { origin, cacheReal, urlPath, search }) {
|
|
91
|
+
const entry = cacheEntryFor(cacheReal, urlPath);
|
|
92
|
+
if (!entry) {
|
|
93
|
+
send(response, 404, "Not found\n");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const cachedBody = await readIfPresent(entry.body, cacheReal);
|
|
97
|
+
const cachedType = cachedBody ? await readIfPresent(entry.type, cacheReal) : null;
|
|
98
|
+
if (cachedBody && cachedType) {
|
|
99
|
+
response.writeHead(200, {
|
|
100
|
+
"Content-Type": cachedType.toString("utf8"),
|
|
101
|
+
"Cache-Control": "no-store",
|
|
102
|
+
"X-Content-Type-Options": "nosniff",
|
|
103
|
+
});
|
|
104
|
+
response.end(request.method === "HEAD" ? undefined : cachedBody);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const url = `${origin}${urlPath}${search}`;
|
|
108
|
+
let upstream;
|
|
109
|
+
try {
|
|
110
|
+
upstream = await fetch(url);
|
|
111
|
+
} catch (cause) {
|
|
112
|
+
send(response, 502, `${url} could not be fetched: ${cause.message}\n`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const body = Buffer.from(await upstream.arrayBuffer());
|
|
116
|
+
const contentType = upstream.headers.get("content-type") ?? "application/octet-stream";
|
|
117
|
+
if (!upstream.ok) {
|
|
118
|
+
send(response, upstream.status, request.method === "HEAD" ? undefined : body, contentType);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
await writeAtomically(entry.body, body);
|
|
122
|
+
await writeAtomically(entry.type, contentType);
|
|
123
|
+
send(response, 200, request.method === "HEAD" ? undefined : body, contentType);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function proxyTemplate(response, templateSource, urlPath) {
|
|
127
|
+
const requested = urlPath.slice(TEMPLATE_URL_PREFIX.length);
|
|
128
|
+
const base = `${templateSource.replace(/\/$/, "")}/templates/`;
|
|
129
|
+
const url = TEMPLATE_NAME.test(requested) ? new URL(requested, base) : null;
|
|
130
|
+
if (!url || url.origin !== new URL(base).origin || !url.href.startsWith(base)) {
|
|
131
|
+
send(response, 404, "Not found\n");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
let upstream;
|
|
135
|
+
try {
|
|
136
|
+
upstream = await fetch(url);
|
|
137
|
+
} catch (cause) {
|
|
138
|
+
send(response, 502, `The workbook template could not be fetched from ${url.href}: ${cause.message}\n`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (!upstream.ok) {
|
|
142
|
+
send(response, upstream.status === 404 ? 404 : 502, `${url.href} returned ${upstream.status}\n`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
send(response, 200, Buffer.from(await upstream.arrayBuffer()), CONTENT_TYPES[extname(url.pathname)] ?? "application/octet-stream");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param {Object} options
|
|
150
|
+
* @param {string} options.cacheDir - the directory fetched files are stored in; created when absent
|
|
151
|
+
* @param {string} [options.origin] - the site pages are fetched from
|
|
152
|
+
* @param {number} [options.port] - 0 or absent lets the OS choose a free port
|
|
153
|
+
* @param {string} [options.templateSource] - where /assets/templates/ requests are fetched from
|
|
154
|
+
* @returns {Promise<{server: import("http").Server, port: number, host: string, origin: string, cacheDir: string, close: () => Promise<void>}>}
|
|
155
|
+
*/
|
|
156
|
+
export async function startViewServer({ cacheDir, origin = DEFAULT_ORIGIN, port = 0, templateSource = DEFAULT_TEMPLATE_SOURCE }) {
|
|
157
|
+
await mkdir(cacheDir, { recursive: true });
|
|
158
|
+
const cacheReal = await realpath(cacheDir);
|
|
159
|
+
const host = "127.0.0.1";
|
|
160
|
+
const originBase = origin.replace(/\/$/, "");
|
|
161
|
+
|
|
162
|
+
const server = createServer(async (request, response) => {
|
|
163
|
+
try {
|
|
164
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
165
|
+
send(response, 405, "Method not allowed\n");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// The path as sent: new URL() would collapse dot segments before the escape check sees them.
|
|
169
|
+
const queryAt = request.url.indexOf("?");
|
|
170
|
+
const urlPath = queryAt === -1 ? request.url : request.url.slice(0, queryAt);
|
|
171
|
+
const search = queryAt === -1 ? "" : request.url.slice(queryAt);
|
|
172
|
+
if (!urlPath.startsWith("/") || urlPath === SERVICE_WORKER_PATH) {
|
|
173
|
+
send(response, 404, "Not found\n");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (urlPath.startsWith(TEMPLATE_URL_PREFIX)) {
|
|
177
|
+
await proxyTemplate(response, templateSource, urlPath);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
await serveFromOrigin(request, response, { origin: originBase, cacheReal, urlPath, search });
|
|
181
|
+
} catch (error) {
|
|
182
|
+
send(response, 500, `${error.message}\n`);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await new Promise((resolveListening, rejectListening) => {
|
|
187
|
+
server.once("error", rejectListening);
|
|
188
|
+
server.listen(port, host, resolveListening);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
server,
|
|
193
|
+
host,
|
|
194
|
+
origin: originBase,
|
|
195
|
+
cacheDir: cacheReal,
|
|
196
|
+
port: server.address().port,
|
|
197
|
+
close: () =>
|
|
198
|
+
new Promise((resolveClosed) => {
|
|
199
|
+
server.close(() => resolveClosed());
|
|
200
|
+
server.closeAllConnections();
|
|
201
|
+
}),
|
|
202
|
+
};
|
|
203
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diy-accounting-uk/diya-gl",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.43",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Recalculate, read and write DIYA-GL books from the command line",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"diya-gl-read-workbook": "bin/diya-gl-read-workbook.js",
|
|
18
18
|
"diya-gl-write-workbook": "bin/diya-gl-write-workbook.js",
|
|
19
19
|
"diya-gl-link": "bin/diya-gl-link.js",
|
|
20
|
+
"diya-gl-view": "bin/diya-gl-view.js",
|
|
20
21
|
"diya-gl-mcp": "bin/diya-gl-mcp.js"
|
|
21
22
|
},
|
|
22
23
|
"files": ["bin/", "dist/", "README.md", "NOTICE"],
|