@weborigami/origami 0.6.14 → 0.6.15
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/main.js +1 -0
- package/package.json +3 -3
- package/src/dev/debug2/debug2.js +43 -324
- package/src/dev/debug2/debugChild.js +15 -3
- package/src/dev/debug2/debugCommands.js +29 -0
- package/src/dev/debug2/debugParent.js +414 -0
- package/src/dev/debug2/debugTransform.js +29 -11
- package/src/dev/debug2/expressionTree.js +9 -4
- package/src/dev/debug2/oriEval.js +17 -0
- package/src/dev/dev.js +1 -0
- package/src/dev/help.yaml +6 -0
- package/src/server/server.js +15 -3
package/main.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as documentObject } from "./src/common/documentObject.js";
|
|
2
2
|
export * from "./src/common/serialize.js";
|
|
3
|
+
export { default as debugParent } from "./src/dev/debug2/debugParent.js";
|
|
3
4
|
export * as Dev from "./src/dev/dev.js";
|
|
4
5
|
export * as Origami from "./src/origami/origami.js";
|
|
5
6
|
export { default as origamiHighlightDefinition } from "./src/origami/origamiHighlightDefinition.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/origami",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.15",
|
|
4
4
|
"description": "Web Origami language, CLI, framework, and server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@hpcc-js/wasm-graphviz": "^1.21.0",
|
|
21
|
-
"@weborigami/async-tree": "0.6.
|
|
21
|
+
"@weborigami/async-tree": "0.6.15",
|
|
22
22
|
"@weborigami/json-feed-to-rss": "1.0.1",
|
|
23
|
-
"@weborigami/language": "0.6.
|
|
23
|
+
"@weborigami/language": "0.6.15",
|
|
24
24
|
"css-tree": "3.1.0",
|
|
25
25
|
"highlight.js": "11.11.1",
|
|
26
26
|
"jsdom": "28.1.0",
|
package/src/dev/debug2/debug2.js
CHANGED
|
@@ -1,39 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import http from "node:http";
|
|
4
|
-
import net from "node:net";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
|
|
7
|
-
const PUBLIC_HOST = "127.0.0.1";
|
|
8
|
-
const DEFAULT_PORT = 5000;
|
|
9
|
-
|
|
10
|
-
// Module that loads the server in the child process
|
|
11
|
-
const childModuleUrl = new URL("./debugChild.js", import.meta.url);
|
|
12
|
-
|
|
13
|
-
// The active child process and port
|
|
14
|
-
/** @typedef {import("node:child_process").ChildProcess} ChildProcess */
|
|
15
|
-
/** @typedef {{ process: ChildProcess, port: number | null }} ChildInfo */
|
|
16
|
-
/** @type {ChildInfo | null} */
|
|
17
|
-
let activeChild = null;
|
|
18
|
-
|
|
19
|
-
// The most recently started child (may not be ready yet)
|
|
20
|
-
/** @type {ChildInfo | null} */
|
|
21
|
-
let pendingChild = null;
|
|
1
|
+
import { execute } from "@weborigami/language";
|
|
2
|
+
import debugParent from "./debugParent.js";
|
|
22
3
|
|
|
23
4
|
/**
|
|
24
|
-
* Given an Origami
|
|
25
|
-
*
|
|
26
|
-
*
|
|
5
|
+
* Given an Origami expression, start a new debug server with that parent as the
|
|
6
|
+
* root of the resource tree.
|
|
7
|
+
*
|
|
8
|
+
* This function expects unevaluated arguments. This is what it allows it to
|
|
9
|
+
* extract the source code of the expression to be debugged. (If it were
|
|
10
|
+
* evaluated, the function will be called with the result of the expression.)
|
|
11
|
+
*
|
|
12
|
+
* The `options` argument can include:
|
|
13
|
+
* - `enableUnsafeEval`: if true, enables the `!eval` debug command in the child
|
|
14
|
+
* process; default is false
|
|
15
|
+
* - `debugFilesPath`: path to resources that will be added to the served tree
|
|
27
16
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* arrangement ensures the expression is evaluated in a clean Node context (not
|
|
31
|
-
* polluted by previous evaluations).
|
|
17
|
+
* @typedef {import("@weborigami/language").RuntimeState} RuntimeState
|
|
18
|
+
* @typedef {import("@weborigami/language").AnnotatedCode} AnnotatedCode
|
|
32
19
|
*
|
|
33
|
-
* @param {
|
|
34
|
-
* @param {
|
|
20
|
+
* @param {AnnotatedCode} code
|
|
21
|
+
* @param {any | RuntimeState} options
|
|
22
|
+
* @param {RuntimeState} state
|
|
35
23
|
*/
|
|
36
|
-
export default async function debug2(code, state) {
|
|
24
|
+
export default async function debug2(code, options, state) {
|
|
25
|
+
if (state === undefined) {
|
|
26
|
+
// Options were omitted; shift arguments
|
|
27
|
+
state = options;
|
|
28
|
+
options = [];
|
|
29
|
+
}
|
|
30
|
+
|
|
37
31
|
if (
|
|
38
32
|
!(code instanceof Array) ||
|
|
39
33
|
code.source === undefined ||
|
|
@@ -43,6 +37,9 @@ export default async function debug2(code, state) {
|
|
|
43
37
|
"Dev.debug2 expects an Origami expression to evaluate: `debug2 <expression>`",
|
|
44
38
|
);
|
|
45
39
|
}
|
|
40
|
+
|
|
41
|
+
const expression = code.source;
|
|
42
|
+
|
|
46
43
|
const { parent } = state;
|
|
47
44
|
// @ts-ignore
|
|
48
45
|
const parentPath = parent?.path;
|
|
@@ -50,301 +47,23 @@ export default async function debug2(code, state) {
|
|
|
50
47
|
throw new Error("Dev.debug2 couldn't work out the parent path.");
|
|
51
48
|
}
|
|
52
49
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
tree.watch();
|
|
60
|
-
tree.addEventListener?.("change", (event) => {
|
|
61
|
-
// @ts-ignore
|
|
62
|
-
const { filePath } = event.options;
|
|
63
|
-
if (isJavaScriptFile(filePath)) {
|
|
64
|
-
// Need to restart the child process
|
|
65
|
-
console.log("JavaScript file changed, restarting server…");
|
|
66
|
-
startChild(serverOptions);
|
|
67
|
-
} else if (isPackageJsonFile(filePath)) {
|
|
68
|
-
// Need to restart the child process
|
|
69
|
-
console.log("package.json changed, restarting server…");
|
|
70
|
-
startChild(serverOptions);
|
|
71
|
-
} else {
|
|
72
|
-
// Just have the child reevaluate the expression
|
|
73
|
-
console.log("File changed, reloading site…");
|
|
74
|
-
activeChild?.process.send({ type: "REEVALUATE" });
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const port = await findOpenPort(PUBLIC_HOST);
|
|
50
|
+
// Need to evaluate options object
|
|
51
|
+
if (options.length > 0) {
|
|
52
|
+
options = await execute(options, state);
|
|
53
|
+
} else {
|
|
54
|
+
options = {};
|
|
55
|
+
}
|
|
79
56
|
|
|
80
|
-
//
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
57
|
+
// @ts-ignore
|
|
58
|
+
const enableUnsafeEval = options.enableUnsafeEval ?? false;
|
|
59
|
+
const debugFilesPath = options.debugFilesPath ?? "";
|
|
60
|
+
|
|
61
|
+
await debugParent({
|
|
62
|
+
debugFilesPath,
|
|
63
|
+
enableUnsafeEval,
|
|
64
|
+
expression,
|
|
65
|
+
parentPath,
|
|
87
66
|
});
|
|
88
67
|
}
|
|
89
68
|
debug2.needsState = true;
|
|
90
69
|
debug2.unevaluatedArgs = true;
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Give a child process a chance to finish any in-flight requests before we kill
|
|
94
|
-
* it.
|
|
95
|
-
*
|
|
96
|
-
* @param {ChildProcess} childProcess
|
|
97
|
-
*/
|
|
98
|
-
async function drainAndStopChild(childProcess) {
|
|
99
|
-
if (childProcess.killed) {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Ask it to drain first.
|
|
104
|
-
try {
|
|
105
|
-
childProcess.send({ type: "DRAIN" });
|
|
106
|
-
} catch {
|
|
107
|
-
// ignore
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const drained = new Promise((resolve) => {
|
|
111
|
-
const onMessage = (msg) => {
|
|
112
|
-
if (msg && typeof msg === "object" && msg.type === "DRAINED") {
|
|
113
|
-
cleanup(resolve);
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
const onExit = () => cleanup(resolve);
|
|
117
|
-
|
|
118
|
-
function cleanup(done) {
|
|
119
|
-
childProcess.off("message", onMessage);
|
|
120
|
-
childProcess.off("exit", onExit);
|
|
121
|
-
done();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
childProcess.on("message", onMessage);
|
|
125
|
-
childProcess.on("exit", onExit);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Give it a short grace window to finish in-flight work.
|
|
129
|
-
const GRACE_MS = 1500;
|
|
130
|
-
await Promise.race([
|
|
131
|
-
drained,
|
|
132
|
-
new Promise((r) => setTimeout(r, GRACE_MS).unref()),
|
|
133
|
-
]);
|
|
134
|
-
|
|
135
|
-
if (!childProcess.killed) {
|
|
136
|
-
childProcess.kill("SIGTERM");
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Final escalation.
|
|
140
|
-
setTimeout(() => {
|
|
141
|
-
// Child should have exited by now, but if not kill it
|
|
142
|
-
if (!childProcess.killed) {
|
|
143
|
-
childProcess.kill("SIGKILL");
|
|
144
|
-
}
|
|
145
|
-
}, GRACE_MS).unref();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Return the first open port number on or after the given port number.
|
|
149
|
-
async function findOpenPort(host, startPort = DEFAULT_PORT) {
|
|
150
|
-
for (let port = startPort; port <= 65535; port++) {
|
|
151
|
-
const open = await isPortAvailable(host, port);
|
|
152
|
-
if (open) {
|
|
153
|
-
return port;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
throw new Error(`No open port found on or after ${startPort}`);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function isJavaScriptFile(filePath) {
|
|
161
|
-
const extname = path.extname(filePath).toLowerCase();
|
|
162
|
-
const jsExtensions = [".cjs", ".js", ".mjs", ".ts"];
|
|
163
|
-
return jsExtensions.includes(extname);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function isPackageJsonFile(filePath) {
|
|
167
|
-
return path.basename(filePath).toLowerCase() === "package.json";
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Check whether a TCP port can be bound.
|
|
172
|
-
*
|
|
173
|
-
* @param {string} host
|
|
174
|
-
* @param {number} port
|
|
175
|
-
* @returns {Promise<boolean>}
|
|
176
|
-
*/
|
|
177
|
-
async function isPortAvailable(host, port) {
|
|
178
|
-
return new Promise((resolve) => {
|
|
179
|
-
const server = net.createServer();
|
|
180
|
-
|
|
181
|
-
server.unref();
|
|
182
|
-
|
|
183
|
-
server.once("error", (/** @type {any} */ error) => {
|
|
184
|
-
// Port is unavailable or cannot be bound on this host.
|
|
185
|
-
if (
|
|
186
|
-
error.code === "EADDRINUSE" ||
|
|
187
|
-
error.code === "EACCES" ||
|
|
188
|
-
error.code === "EADDRNOTAVAIL"
|
|
189
|
-
) {
|
|
190
|
-
resolve(false);
|
|
191
|
-
} else {
|
|
192
|
-
resolve(false);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
server.once("listening", () => {
|
|
197
|
-
server.close(() => resolve(true));
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
server.listen(port, host);
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Proxy incoming requests to the active child server, or return a 503 if not
|
|
206
|
-
* ready.
|
|
207
|
-
*
|
|
208
|
-
* @param {import("node:http").IncomingMessage} request
|
|
209
|
-
* @param {import("node:http").ServerResponse} response
|
|
210
|
-
*/
|
|
211
|
-
function proxyRequest(request, response) {
|
|
212
|
-
if (!activeChild) {
|
|
213
|
-
response.statusCode = 503;
|
|
214
|
-
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
215
|
-
response.end("Dev server is starting…\n");
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const { port } = activeChild;
|
|
220
|
-
|
|
221
|
-
// Minimal hop-by-hop header stripping
|
|
222
|
-
const headers = { ...request.headers };
|
|
223
|
-
delete headers.connection;
|
|
224
|
-
delete headers["proxy-connection"];
|
|
225
|
-
delete headers["keep-alive"];
|
|
226
|
-
delete headers.te;
|
|
227
|
-
delete headers.trailer;
|
|
228
|
-
delete headers["transfer-encoding"];
|
|
229
|
-
delete headers.upgrade;
|
|
230
|
-
|
|
231
|
-
const upstreamRequest = http.request(
|
|
232
|
-
{
|
|
233
|
-
host: PUBLIC_HOST,
|
|
234
|
-
port,
|
|
235
|
-
method: request.method,
|
|
236
|
-
path: request.url,
|
|
237
|
-
headers,
|
|
238
|
-
},
|
|
239
|
-
(upstreamResponse) => {
|
|
240
|
-
response.writeHead(
|
|
241
|
-
upstreamResponse.statusCode ?? 502,
|
|
242
|
-
upstreamResponse.statusMessage,
|
|
243
|
-
upstreamResponse.headers,
|
|
244
|
-
);
|
|
245
|
-
upstreamResponse.pipe(response);
|
|
246
|
-
},
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
upstreamRequest.on("error", (err) => {
|
|
250
|
-
// Stop piping the request body
|
|
251
|
-
request.unpipe(upstreamRequest);
|
|
252
|
-
upstreamRequest.destroy();
|
|
253
|
-
|
|
254
|
-
// Only send error response if headers haven't been sent yet
|
|
255
|
-
if (!response.headersSent) {
|
|
256
|
-
response.statusCode = 502;
|
|
257
|
-
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
258
|
-
response.end(`Upstream error: ${err.message}\n`);
|
|
259
|
-
} else {
|
|
260
|
-
// Headers already sent, can't send error message - just close
|
|
261
|
-
response.destroy();
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// Also handle errors on the incoming request
|
|
266
|
-
request.on("error", () => {
|
|
267
|
-
upstreamRequest.destroy();
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
request.pipe(upstreamRequest);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Start a new child process.
|
|
275
|
-
*
|
|
276
|
-
* This will be a pending process until it sends a READY message, at which point
|
|
277
|
-
* it becomes active and any previous active child is drained and stopped.
|
|
278
|
-
*/
|
|
279
|
-
function startChild(serverOptions) {
|
|
280
|
-
const { expression, parent } = serverOptions;
|
|
281
|
-
|
|
282
|
-
// Start the child process, passing parent path via an environment variable.
|
|
283
|
-
/** @type {ChildProcess} */
|
|
284
|
-
let childProcess;
|
|
285
|
-
try {
|
|
286
|
-
childProcess = fork(childModuleUrl, [], {
|
|
287
|
-
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
|
288
|
-
env: {
|
|
289
|
-
...process.env,
|
|
290
|
-
ORIGAMI_EXPRESSION: expression,
|
|
291
|
-
ORIGAMI_PARENT: parent,
|
|
292
|
-
},
|
|
293
|
-
});
|
|
294
|
-
} catch (error) {
|
|
295
|
-
throw new Error("Dev.debug2: failed to start child server:", {
|
|
296
|
-
cause: error,
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// This becomes the pending child immediately
|
|
301
|
-
pendingChild = { process: childProcess, port: null };
|
|
302
|
-
|
|
303
|
-
// Listen for messages from the child about its status
|
|
304
|
-
childProcess.on("message", (/** @type {any} */ message) => {
|
|
305
|
-
if (!message || typeof message !== "object") {
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (message.type === "READY" && typeof message.port === "number") {
|
|
310
|
-
// Only promote to active if this is still the pending child
|
|
311
|
-
if (pendingChild?.process === childProcess) {
|
|
312
|
-
const previousChild = activeChild;
|
|
313
|
-
|
|
314
|
-
activeChild = pendingChild;
|
|
315
|
-
pendingChild.port = message.port;
|
|
316
|
-
pendingChild = null;
|
|
317
|
-
|
|
318
|
-
// Drain previous child in background (don't wait)
|
|
319
|
-
if (previousChild?.process && previousChild.process !== childProcess) {
|
|
320
|
-
drainAndStopChild(previousChild.process).catch((err) =>
|
|
321
|
-
console.error("[drain]", err),
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
} else {
|
|
325
|
-
// This child was superseded by a newer one, kill it
|
|
326
|
-
// console.log("Child process superseded by newer one, killing it...");
|
|
327
|
-
childProcess.kill("SIGTERM");
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (message.type === "FATAL") {
|
|
332
|
-
// Child couldn't start (import error, etc.)
|
|
333
|
-
// Keep previous active child if any; otherwise we'll serve 500/503.
|
|
334
|
-
console.error("[child fatal]", message.error ?? message);
|
|
335
|
-
if (pendingChild?.process === childProcess) {
|
|
336
|
-
pendingChild = null;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
childProcess.on("exit", (code, signal) => {
|
|
342
|
-
if (activeChild?.process === childProcess) {
|
|
343
|
-
// Active child died unexpectedly.
|
|
344
|
-
activeChild = null;
|
|
345
|
-
}
|
|
346
|
-
if (pendingChild?.process === childProcess) {
|
|
347
|
-
pendingChild = null;
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
}
|
|
@@ -3,7 +3,7 @@ import { requestListener } from "../../server/server.js";
|
|
|
3
3
|
import expressionTree from "./expressionTree.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* The
|
|
6
|
+
* The debug parent runs this module in a child process, passing in a parent
|
|
7
7
|
* path in an environment variable.
|
|
8
8
|
*
|
|
9
9
|
* This module starts an HTTP server that will serve resources from that tree.
|
|
@@ -22,6 +22,13 @@ function fail(message) {
|
|
|
22
22
|
process.exit(1);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** @type {string} */
|
|
26
|
+
const debugFilesPath = process.env.ORIGAMI_DEBUG_FILES_PATH ?? "";
|
|
27
|
+
|
|
28
|
+
/** @type {boolean} */
|
|
29
|
+
// @ts-ignore
|
|
30
|
+
const enableUnsafeEval = process.env.ORIGAMI_ENABLE_UNSAFE_EVAL === "1";
|
|
31
|
+
|
|
25
32
|
/** @type {string} */
|
|
26
33
|
// @ts-ignore
|
|
27
34
|
const expression = process.env.ORIGAMI_EXPRESSION;
|
|
@@ -31,7 +38,7 @@ if (expression === undefined) {
|
|
|
31
38
|
|
|
32
39
|
/** @type {string} */
|
|
33
40
|
// @ts-ignore
|
|
34
|
-
const parentPath = process.env.
|
|
41
|
+
const parentPath = process.env.ORIGAMI_PARENT_PATH;
|
|
35
42
|
if (parentPath === undefined) {
|
|
36
43
|
fail("Missing Origami parent");
|
|
37
44
|
}
|
|
@@ -88,7 +95,12 @@ function beginDrain() {
|
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
async function evaluateExpression() {
|
|
91
|
-
const tree = await expressionTree(
|
|
98
|
+
const tree = await expressionTree({
|
|
99
|
+
debugFilesPath,
|
|
100
|
+
expression,
|
|
101
|
+
parentPath,
|
|
102
|
+
enableUnsafeEval,
|
|
103
|
+
});
|
|
92
104
|
if (!tree) {
|
|
93
105
|
fail("Dev.debug2: expression did not evaluate to a maplike resource tree");
|
|
94
106
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Subset of commands made available via debugTransform
|
|
2
|
+
|
|
3
|
+
import { Tree } from "@weborigami/async-tree";
|
|
4
|
+
|
|
5
|
+
import index from "../../origami/indexPage.js";
|
|
6
|
+
import yaml from "../../origami/yaml.js";
|
|
7
|
+
import explore from "../explore.js";
|
|
8
|
+
import svg from "../svg.js";
|
|
9
|
+
import version from "../version.js";
|
|
10
|
+
import oriEval from "./oriEval.js";
|
|
11
|
+
|
|
12
|
+
export default function debugCommands(enableUnsafeEval = false) {
|
|
13
|
+
return Object.assign(
|
|
14
|
+
{
|
|
15
|
+
keys: Tree.keys,
|
|
16
|
+
json: Tree.json,
|
|
17
|
+
index,
|
|
18
|
+
yaml,
|
|
19
|
+
explore,
|
|
20
|
+
svg,
|
|
21
|
+
version,
|
|
22
|
+
},
|
|
23
|
+
enableUnsafeEval
|
|
24
|
+
? {
|
|
25
|
+
eval: oriEval,
|
|
26
|
+
}
|
|
27
|
+
: {},
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { OrigamiFileMap } from "@weborigami/language";
|
|
2
|
+
import { fork } from "node:child_process";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import net from "node:net";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
const PUBLIC_HOST = "127.0.0.1";
|
|
9
|
+
const DEFAULT_PORT = 5000;
|
|
10
|
+
|
|
11
|
+
// Module that loads the server in the child process
|
|
12
|
+
const childModuleUrl = new URL("./debugChild.js", import.meta.url);
|
|
13
|
+
|
|
14
|
+
// The public-facing server that proxies to the child process
|
|
15
|
+
let publicServer;
|
|
16
|
+
let publicOrigin;
|
|
17
|
+
|
|
18
|
+
// The tree of files in the parent path, which we watch for changes
|
|
19
|
+
let tree;
|
|
20
|
+
|
|
21
|
+
// The active child process and port
|
|
22
|
+
/** @typedef {import("node:child_process").ChildProcess} ChildProcess */
|
|
23
|
+
/** @typedef {{ process: ChildProcess, port: number | null }} ChildInfo */
|
|
24
|
+
/** @type {ChildInfo | null} */
|
|
25
|
+
let activeChild = null;
|
|
26
|
+
|
|
27
|
+
// The most recently started child (may not be ready yet)
|
|
28
|
+
/** @type {ChildInfo | null} */
|
|
29
|
+
let pendingChild = null;
|
|
30
|
+
|
|
31
|
+
// Used to communicate errors to caller
|
|
32
|
+
let emitter = null;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Start a new debug parent server for the given Origami expression and runtime
|
|
36
|
+
* state.
|
|
37
|
+
*
|
|
38
|
+
* This function will start a child server that evaluates the given expression
|
|
39
|
+
* with the given parent path. This arrangement ensures the expression is
|
|
40
|
+
* evaluated in a clean Node context (not polluted by previous evaluations). The
|
|
41
|
+
* parent server proxies requests to the child server.
|
|
42
|
+
*
|
|
43
|
+
* The debug parent monitors the parent tree for changes, and restarts the child
|
|
44
|
+
* whenever files in the parent tree change.
|
|
45
|
+
*
|
|
46
|
+
* Supported `options`:
|
|
47
|
+
* - `debugFilesPath`: path to resources that will be added to the served tree
|
|
48
|
+
* - `enableUnsafeEval`: if true, enables the `!eval` debug command in the child
|
|
49
|
+
* process; default is false
|
|
50
|
+
* - `expression` (required): the Origami expression to evaluate in the child
|
|
51
|
+
* process
|
|
52
|
+
* - `parentPath` (required): the path to the parent tree used for evaluation
|
|
53
|
+
*
|
|
54
|
+
* The returned `emitter` is an EventEmitter that emits "error" events when the
|
|
55
|
+
* child server encounters an Origami error while handling a request.
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} options
|
|
58
|
+
* @param {string} options.debugFilesPath
|
|
59
|
+
* @param {boolean} options.enableUnsafeEval
|
|
60
|
+
* @param {string} options.expression
|
|
61
|
+
* @param {string} options.parentPath
|
|
62
|
+
*/
|
|
63
|
+
export default async function debugParent(options) {
|
|
64
|
+
const { parentPath } = options;
|
|
65
|
+
|
|
66
|
+
tree = new OrigamiFileMap(parentPath);
|
|
67
|
+
tree.watch();
|
|
68
|
+
tree.addEventListener?.("change", (event) => {
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
const { filePath } = event.options;
|
|
71
|
+
if (isJavaScriptFile(filePath)) {
|
|
72
|
+
// Need to restart the child process
|
|
73
|
+
console.log("JavaScript file changed, restarting server…");
|
|
74
|
+
startChild(options);
|
|
75
|
+
} else if (isPackageJsonFile(filePath)) {
|
|
76
|
+
// Need to restart the child process
|
|
77
|
+
console.log("package.json changed, restarting server…");
|
|
78
|
+
startChild(options);
|
|
79
|
+
} else {
|
|
80
|
+
// Just have the child reevaluate the expression
|
|
81
|
+
console.log("File changed, reloading site…");
|
|
82
|
+
activeChild?.process.send({ type: "REEVALUATE" });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const port = await findOpenPort();
|
|
87
|
+
publicOrigin = `http://${PUBLIC_HOST}:${port}`;
|
|
88
|
+
|
|
89
|
+
publicServer = http.createServer(proxyRequest);
|
|
90
|
+
publicServer.listen(port, PUBLIC_HOST, () => {
|
|
91
|
+
startChild(options);
|
|
92
|
+
console.log(`Server running at ${publicOrigin}. Press Ctrl+C to stop.`);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
emitter = Object.assign(new EventEmitter(), {
|
|
96
|
+
close,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return emitter;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Gracefully stop the parent server and any active child server, giving the
|
|
104
|
+
* child a chance to finish any in-flight requests before exiting.
|
|
105
|
+
*/
|
|
106
|
+
async function close() {
|
|
107
|
+
// Stop accepting new connections and force-close any keep-alive connections
|
|
108
|
+
// so the close callback fires promptly.
|
|
109
|
+
const closed = new Promise((resolve) => publicServer.close(resolve));
|
|
110
|
+
publicServer.closeAllConnections();
|
|
111
|
+
await closed;
|
|
112
|
+
publicServer = null;
|
|
113
|
+
emitter = null;
|
|
114
|
+
|
|
115
|
+
// Drain and stop any children concurrently
|
|
116
|
+
const children = [pendingChild?.process, activeChild?.process].filter(
|
|
117
|
+
/** @returns {child is ChildProcess} */
|
|
118
|
+
(child) => child !== undefined,
|
|
119
|
+
);
|
|
120
|
+
pendingChild = null;
|
|
121
|
+
activeChild = null;
|
|
122
|
+
await Promise.all(children.map(drainAndStopChild));
|
|
123
|
+
|
|
124
|
+
// Stop watching for file changes
|
|
125
|
+
tree.unwatch();
|
|
126
|
+
tree = null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Give a child process a chance to finish any in-flight requests before we kill
|
|
131
|
+
* it.
|
|
132
|
+
*
|
|
133
|
+
* @param {ChildProcess} childProcess
|
|
134
|
+
*/
|
|
135
|
+
async function drainAndStopChild(childProcess) {
|
|
136
|
+
if (childProcess.killed) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Ask it to drain first.
|
|
141
|
+
try {
|
|
142
|
+
childProcess.send({ type: "DRAIN" });
|
|
143
|
+
} catch {
|
|
144
|
+
// ignore
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const drained = new Promise((resolve) => {
|
|
148
|
+
const onMessage = (msg) => {
|
|
149
|
+
if (msg && typeof msg === "object" && msg.type === "DRAINED") {
|
|
150
|
+
cleanup(resolve);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const onExit = () => cleanup(resolve);
|
|
154
|
+
|
|
155
|
+
function cleanup(done) {
|
|
156
|
+
childProcess.off("message", onMessage);
|
|
157
|
+
childProcess.off("exit", onExit);
|
|
158
|
+
done();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
childProcess.on("message", onMessage);
|
|
162
|
+
childProcess.on("exit", onExit);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Give it a short grace window to finish in-flight work.
|
|
166
|
+
const GRACE_MS = 1500;
|
|
167
|
+
await Promise.race([
|
|
168
|
+
drained,
|
|
169
|
+
new Promise((r) => setTimeout(r, GRACE_MS).unref()),
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
if (!childProcess.killed) {
|
|
173
|
+
childProcess.kill("SIGTERM");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Final escalation.
|
|
177
|
+
setTimeout(() => {
|
|
178
|
+
// Child should have exited by now, but if not kill it
|
|
179
|
+
if (!childProcess.killed) {
|
|
180
|
+
childProcess.kill("SIGKILL");
|
|
181
|
+
}
|
|
182
|
+
}, GRACE_MS).unref();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Return the first open port number on or after the given port number.
|
|
186
|
+
async function findOpenPort(startPort = DEFAULT_PORT) {
|
|
187
|
+
for (let port = startPort; port <= 65535; port++) {
|
|
188
|
+
if (await isPortAvailable(port)) {
|
|
189
|
+
return port;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
throw new Error(`No open port found on or after ${startPort}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isJavaScriptFile(filePath) {
|
|
197
|
+
const extname = path.extname(filePath).toLowerCase();
|
|
198
|
+
const jsExtensions = [".cjs", ".js", ".mjs", ".ts"];
|
|
199
|
+
return jsExtensions.includes(extname);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isPackageJsonFile(filePath) {
|
|
203
|
+
return path.basename(filePath).toLowerCase() === "package.json";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check whether a port is available on both IPv4 and IPv6 loopback addresses
|
|
208
|
+
* by attempting TCP connections. On macOS, IPv4 and IPv6 port spaces are
|
|
209
|
+
* independent (IPV6_V6ONLY=1 by default), so a server bound to [::]:PORT is
|
|
210
|
+
* invisible to a 127.0.0.1 bind check. Using connect probes on both loopbacks
|
|
211
|
+
* catches servers regardless of which protocol family they listen on. Any
|
|
212
|
+
* connection error (ECONNREFUSED, EADDRNOTAVAIL, etc.) means nothing is
|
|
213
|
+
* listening there, so the function is safe on systems without IPv6.
|
|
214
|
+
*
|
|
215
|
+
* @param {number} port
|
|
216
|
+
* @returns {Promise<boolean>}
|
|
217
|
+
*/
|
|
218
|
+
async function isPortAvailable(port) {
|
|
219
|
+
const [v4, v6] = await Promise.all([
|
|
220
|
+
isPortListening("127.0.0.1", port),
|
|
221
|
+
isPortListening("::1", port),
|
|
222
|
+
]);
|
|
223
|
+
return !v4 && !v6;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @param {string} host
|
|
228
|
+
* @param {number} port
|
|
229
|
+
* @returns {Promise<boolean>}
|
|
230
|
+
*/
|
|
231
|
+
function isPortListening(host, port) {
|
|
232
|
+
return new Promise((resolve) => {
|
|
233
|
+
const socket = net.createConnection(port, host);
|
|
234
|
+
socket.once("connect", () => {
|
|
235
|
+
socket.destroy();
|
|
236
|
+
resolve(true);
|
|
237
|
+
});
|
|
238
|
+
socket.once("error", () => {
|
|
239
|
+
socket.destroy();
|
|
240
|
+
resolve(false);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Proxy incoming requests to the active child server, or return a 503 if not
|
|
247
|
+
* ready.
|
|
248
|
+
*
|
|
249
|
+
* @param {import("node:http").IncomingMessage} request
|
|
250
|
+
* @param {import("node:http").ServerResponse} response
|
|
251
|
+
*/
|
|
252
|
+
function proxyRequest(request, response) {
|
|
253
|
+
if (!activeChild) {
|
|
254
|
+
response.statusCode = 503;
|
|
255
|
+
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
256
|
+
response.end("Dev server is starting…\n");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const { port } = activeChild;
|
|
261
|
+
|
|
262
|
+
// Minimal hop-by-hop header stripping
|
|
263
|
+
const headers = { ...request.headers };
|
|
264
|
+
delete headers.connection;
|
|
265
|
+
delete headers["proxy-connection"];
|
|
266
|
+
delete headers["keep-alive"];
|
|
267
|
+
delete headers.te;
|
|
268
|
+
delete headers.trailer;
|
|
269
|
+
delete headers["transfer-encoding"];
|
|
270
|
+
delete headers.upgrade;
|
|
271
|
+
|
|
272
|
+
const upstreamRequest = http.request(
|
|
273
|
+
{
|
|
274
|
+
host: PUBLIC_HOST,
|
|
275
|
+
port,
|
|
276
|
+
method: request.method,
|
|
277
|
+
path: request.url,
|
|
278
|
+
headers,
|
|
279
|
+
},
|
|
280
|
+
(upstreamResponse) => {
|
|
281
|
+
const { statusCode } = upstreamResponse;
|
|
282
|
+
response.writeHead(
|
|
283
|
+
statusCode ?? 502,
|
|
284
|
+
upstreamResponse.statusMessage,
|
|
285
|
+
upstreamResponse.headers,
|
|
286
|
+
);
|
|
287
|
+
upstreamResponse.pipe(response);
|
|
288
|
+
|
|
289
|
+
// Let caller know about the Origami error messages
|
|
290
|
+
if (statusCode !== undefined && statusCode >= 500 && emitter) {
|
|
291
|
+
const rawHeader = upstreamResponse.headers["x-error-details"];
|
|
292
|
+
const raw = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
|
|
293
|
+
const message = raw ? decodeURIComponent(raw) : undefined;
|
|
294
|
+
if (message) {
|
|
295
|
+
emitter.emit("origami-error", message);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
upstreamRequest.on("error", (err) => {
|
|
302
|
+
// Stop piping the request body
|
|
303
|
+
request.unpipe(upstreamRequest);
|
|
304
|
+
upstreamRequest.destroy();
|
|
305
|
+
|
|
306
|
+
// Only send error response if headers haven't been sent yet
|
|
307
|
+
if (!response.headersSent) {
|
|
308
|
+
response.statusCode = 502;
|
|
309
|
+
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
310
|
+
response.end(`Upstream error: ${err.message}\n`);
|
|
311
|
+
} else {
|
|
312
|
+
// Headers already sent, can't send error message - just close
|
|
313
|
+
response.destroy();
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Also handle errors on the incoming request
|
|
318
|
+
request.on("error", () => {
|
|
319
|
+
upstreamRequest.destroy();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
request.pipe(upstreamRequest);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Start a new child process.
|
|
327
|
+
*
|
|
328
|
+
* This will be a pending process until it sends a READY message, at which point
|
|
329
|
+
* it becomes active and any previous active child is drained and stopped.
|
|
330
|
+
*/
|
|
331
|
+
function startChild(options) {
|
|
332
|
+
const { debugFilesPath, enableUnsafeEval, expression, parentPath } = options;
|
|
333
|
+
|
|
334
|
+
// Start the child process, passing parent path via an environment variable.
|
|
335
|
+
/** @type {ChildProcess} */
|
|
336
|
+
let childProcess;
|
|
337
|
+
try {
|
|
338
|
+
childProcess = fork(childModuleUrl, [], {
|
|
339
|
+
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
|
340
|
+
env: {
|
|
341
|
+
...process.env,
|
|
342
|
+
ORIGAMI_DEBUG_FILES_PATH: debugFilesPath,
|
|
343
|
+
ORIGAMI_ENABLE_UNSAFE_EVAL: enableUnsafeEval ? "1" : "0",
|
|
344
|
+
ORIGAMI_EXPRESSION: expression,
|
|
345
|
+
ORIGAMI_PARENT_PATH: parentPath,
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
} catch (error) {
|
|
349
|
+
throw new Error("Dev.debug2: failed to start child server:", {
|
|
350
|
+
cause: error,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// This becomes the pending child immediately
|
|
355
|
+
pendingChild = { process: childProcess, port: null };
|
|
356
|
+
|
|
357
|
+
// Listen for messages from the child about its status
|
|
358
|
+
childProcess.on("message", (/** @type {any} */ message) => {
|
|
359
|
+
if (!message || typeof message !== "object") {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (message.type === "READY" && typeof message.port === "number") {
|
|
364
|
+
// Only promote to active if this is still the pending child
|
|
365
|
+
if (pendingChild?.process === childProcess) {
|
|
366
|
+
const previousChild = activeChild;
|
|
367
|
+
|
|
368
|
+
activeChild = pendingChild;
|
|
369
|
+
pendingChild.port = message.port;
|
|
370
|
+
pendingChild = null;
|
|
371
|
+
|
|
372
|
+
// Drain previous child in background (don't wait)
|
|
373
|
+
if (previousChild?.process && previousChild.process !== childProcess) {
|
|
374
|
+
drainAndStopChild(previousChild.process).catch((err) =>
|
|
375
|
+
console.error("[drain]", err),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!emitter) {
|
|
380
|
+
console.warn(
|
|
381
|
+
"Dev.debug2: child server is ready but parent emitter is gone, cannot emit ready event",
|
|
382
|
+
);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
emitter.emit("ready", {
|
|
386
|
+
origin: publicOrigin,
|
|
387
|
+
});
|
|
388
|
+
} else {
|
|
389
|
+
// This child was superseded by a newer one, kill it
|
|
390
|
+
// console.log("Child process superseded by newer one, killing it...");
|
|
391
|
+
childProcess.kill("SIGTERM");
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (message.type === "FATAL") {
|
|
396
|
+
// Child couldn't start (import error, etc.)
|
|
397
|
+
// Keep previous active child if any; otherwise we'll serve 500/503.
|
|
398
|
+
console.error("[child fatal]", message.error ?? message);
|
|
399
|
+
if (pendingChild?.process === childProcess) {
|
|
400
|
+
pendingChild = null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
childProcess.on("exit", (code, signal) => {
|
|
406
|
+
if (activeChild?.process === childProcess) {
|
|
407
|
+
// Active child died unexpectedly.
|
|
408
|
+
activeChild = null;
|
|
409
|
+
}
|
|
410
|
+
if (pendingChild?.process === childProcess) {
|
|
411
|
+
pendingChild = null;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
@@ -9,9 +9,10 @@ import {
|
|
|
9
9
|
scope,
|
|
10
10
|
trailingSlash,
|
|
11
11
|
} from "@weborigami/async-tree";
|
|
12
|
-
import {
|
|
12
|
+
import { OrigamiFileMap } from "@weborigami/language";
|
|
13
13
|
import indexPage from "../../origami/indexPage.js";
|
|
14
14
|
import yaml from "../../origami/yaml.js";
|
|
15
|
+
import debugCommands from "./debugCommands.js";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Transform the given map-based tree to add debugging resources:
|
|
@@ -23,9 +24,19 @@ import yaml from "../../origami/yaml.js";
|
|
|
23
24
|
* Also transform a simple object result to YAML for viewing.
|
|
24
25
|
*
|
|
25
26
|
* @param {import("@weborigami/async-tree").Maplike} maplike
|
|
27
|
+
* @param {string} debugFilesPath
|
|
28
|
+
* @param {boolean} enableUnsafeEval
|
|
26
29
|
*/
|
|
27
|
-
export default function debugTransform(
|
|
30
|
+
export default function debugTransform(
|
|
31
|
+
maplike,
|
|
32
|
+
debugFilesPath = "",
|
|
33
|
+
enableUnsafeEval = false,
|
|
34
|
+
) {
|
|
28
35
|
const source = Tree.from(maplike, { deep: true });
|
|
36
|
+
const commands = debugCommands(enableUnsafeEval);
|
|
37
|
+
|
|
38
|
+
const debugFiles = debugFilesPath ? new OrigamiFileMap(debugFilesPath) : null;
|
|
39
|
+
|
|
29
40
|
return Object.assign(new AsyncMap(), {
|
|
30
41
|
description: "debug resources",
|
|
31
42
|
|
|
@@ -33,15 +44,22 @@ export default function debugTransform(maplike) {
|
|
|
33
44
|
// Ask the tree if it has the key.
|
|
34
45
|
let value = await source.get(key);
|
|
35
46
|
|
|
47
|
+
if (value === undefined && debugFiles) {
|
|
48
|
+
// Try the debug files
|
|
49
|
+
value = await debugFiles.get(key);
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
if (value === undefined) {
|
|
37
|
-
//
|
|
38
|
-
if (key === "
|
|
53
|
+
// Try the defaults and commands
|
|
54
|
+
if (key === "_debugger/") {
|
|
55
|
+
return debugFiles;
|
|
56
|
+
} else if (key === "index.html") {
|
|
39
57
|
// Generate an index page for this site
|
|
40
58
|
value = await indexPage(source);
|
|
41
59
|
} else if (key === ".keys.json") {
|
|
42
60
|
value = await jsonKeys.stringify(source);
|
|
43
61
|
} else if (typeof key === "string" && key.startsWith("!")) {
|
|
44
|
-
value = await invokeOrigamiCommand(source, key);
|
|
62
|
+
value = await invokeOrigamiCommand(commands, source, key);
|
|
45
63
|
}
|
|
46
64
|
}
|
|
47
65
|
|
|
@@ -60,8 +78,9 @@ export default function debugTransform(maplike) {
|
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
if (Tree.isMap(value)) {
|
|
63
|
-
// Ensure this transform is applied to any map result
|
|
64
|
-
|
|
81
|
+
// Ensure this transform is applied to any map result.
|
|
82
|
+
// Note: debugFilesPath only needed at top level.
|
|
83
|
+
value = debugTransform(value, "", enableUnsafeEval);
|
|
65
84
|
} else if (value?.unpack) {
|
|
66
85
|
// If the value isn't a tree, but has a tree attached via an `unpack`
|
|
67
86
|
// method, wrap the unpack method to add this transform.
|
|
@@ -95,13 +114,12 @@ export default function debugTransform(maplike) {
|
|
|
95
114
|
});
|
|
96
115
|
}
|
|
97
116
|
|
|
98
|
-
async function invokeOrigamiCommand(tree, key) {
|
|
117
|
+
async function invokeOrigamiCommand(commands, tree, key) {
|
|
99
118
|
// Key is an Origami command; invoke it.
|
|
100
|
-
const globals = await projectGlobals(tree);
|
|
101
119
|
const commandName = trailingSlash.remove(key.slice(1).trim());
|
|
102
120
|
|
|
103
|
-
// Look for
|
|
104
|
-
const command =
|
|
121
|
+
// Look for the indicated command
|
|
122
|
+
const command = commands[commandName];
|
|
105
123
|
let value;
|
|
106
124
|
if (command) {
|
|
107
125
|
value = await command(tree);
|
|
@@ -14,10 +14,15 @@ let version = 0;
|
|
|
14
14
|
* Evaluate the given expression using the indicated parent path to produce a
|
|
15
15
|
* resource tree, then transform that tree with debug resources and return it.
|
|
16
16
|
*
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {string}
|
|
17
|
+
* @param {Object} options
|
|
18
|
+
* @param {string} options.debugFilesPath
|
|
19
|
+
* @param {boolean} options.enableUnsafeEval
|
|
20
|
+
* @param {string} options.expression
|
|
21
|
+
* @param {string} options.parentPath
|
|
19
22
|
*/
|
|
20
|
-
export default async function expressionTree(
|
|
23
|
+
export default async function expressionTree(options) {
|
|
24
|
+
const { debugFilesPath, expression, parentPath, enableUnsafeEval } = options;
|
|
25
|
+
|
|
21
26
|
const parent = new OrigamiFileMap(parentPath);
|
|
22
27
|
const globals = await projectGlobals(parent);
|
|
23
28
|
|
|
@@ -42,7 +47,7 @@ export default async function expressionTree(expression, parentPath) {
|
|
|
42
47
|
setParent(maplike, parent);
|
|
43
48
|
|
|
44
49
|
// Add debugging resources
|
|
45
|
-
const tree = debugTransform(maplike);
|
|
50
|
+
const tree = debugTransform(maplike, debugFilesPath, enableUnsafeEval);
|
|
46
51
|
|
|
47
52
|
/** @type {any} */ (tree).version = version++;
|
|
48
53
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import ori from "../../origami/ori.js";
|
|
2
|
+
|
|
3
|
+
let lastExpression;
|
|
4
|
+
let lastResult;
|
|
5
|
+
|
|
6
|
+
export default async function oriEval(parent) {
|
|
7
|
+
return async (encodedExpression) => {
|
|
8
|
+
console.log(encodedExpression);
|
|
9
|
+
const expression = decodeURIComponent(encodedExpression);
|
|
10
|
+
if (expression === lastExpression) {
|
|
11
|
+
return lastResult;
|
|
12
|
+
}
|
|
13
|
+
lastExpression = expression;
|
|
14
|
+
lastResult = await ori(expression, { parent });
|
|
15
|
+
return lastResult;
|
|
16
|
+
};
|
|
17
|
+
}
|
package/src/dev/dev.js
CHANGED
|
@@ -12,6 +12,7 @@ export { default as audit } from "./crawler/audit.js";
|
|
|
12
12
|
export { default as crawl } from "./crawler/crawl.js";
|
|
13
13
|
export { default as debug } from "./debug.js";
|
|
14
14
|
export { default as debug2 } from "./debug2/debug2.js";
|
|
15
|
+
export { default as eval } from "./debug2/oriEval.js";
|
|
15
16
|
export { default as explore } from "./explore.js";
|
|
16
17
|
export { default as help } from "./help.js";
|
|
17
18
|
export { default as log } from "./log.js";
|
package/src/dev/help.yaml
CHANGED
|
@@ -211,6 +211,9 @@ Tree:
|
|
|
211
211
|
deepValues:
|
|
212
212
|
args: (tree)
|
|
213
213
|
description: The in-order leaf values of the tree
|
|
214
|
+
deflatePaths:
|
|
215
|
+
args: (tree)
|
|
216
|
+
description: Convert a tree to a mapping of paths to values
|
|
214
217
|
delete:
|
|
215
218
|
args: (map, key)
|
|
216
219
|
description: Delete the value for the key from map
|
|
@@ -244,6 +247,9 @@ Tree:
|
|
|
244
247
|
indent:
|
|
245
248
|
args: "`…`"
|
|
246
249
|
description: Tagged template literal for normalizing indentation
|
|
250
|
+
inflatePaths:
|
|
251
|
+
args: (map)
|
|
252
|
+
description: Convert mapping of paths to values into a tree
|
|
247
253
|
inners:
|
|
248
254
|
args: (tree)
|
|
249
255
|
description: The tree's interior nodes
|
package/src/server/server.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
TraverseError,
|
|
3
|
+
Tree,
|
|
4
|
+
keysFromPath,
|
|
5
|
+
trailingSlash,
|
|
6
|
+
} from "@weborigami/async-tree";
|
|
2
7
|
import { formatError } from "@weborigami/language";
|
|
3
8
|
import { ServerResponse } from "node:http";
|
|
4
9
|
import constructResponse from "./constructResponse.js";
|
|
@@ -138,9 +143,16 @@ ${message}
|
|
|
138
143
|
</body>
|
|
139
144
|
</html>
|
|
140
145
|
`;
|
|
141
|
-
response.writeHead(500, {
|
|
146
|
+
response.writeHead(500, {
|
|
147
|
+
"Content-Type": "text/html",
|
|
148
|
+
"x-error-details": encodeURIComponent(message),
|
|
149
|
+
});
|
|
142
150
|
response.end(html, "utf-8");
|
|
143
|
-
|
|
151
|
+
|
|
152
|
+
// Don't log traverse errors for requests like favicon.ico, com.chrome.devtools.json, etc.
|
|
153
|
+
if (!(error instanceof TraverseError)) {
|
|
154
|
+
console.error(message);
|
|
155
|
+
}
|
|
144
156
|
}
|
|
145
157
|
|
|
146
158
|
// Asynchronous tree router as Express middleware.
|