@weborigami/origami 0.6.17 → 0.7.0-beta.1
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/package.json +13 -13
- package/src/cli/cli.js +10 -5
- package/src/common/serialize.js +3 -0
- package/src/dev/OriCommandTransform.js +3 -3
- package/src/dev/changes.js +15 -52
- package/src/dev/debug2/debug2.js +0 -34
- package/src/dev/debug2/debugChild.js +73 -43
- package/src/dev/debug2/debugCommands.js +1 -0
- package/src/dev/debug2/debugParent.js +30 -42
- package/src/dev/debug2/debugTransform.js +32 -20
- package/src/dev/debug2/expressionTree.js +10 -6
- package/src/dev/debug2/oriEval.js +2 -2
- package/src/dev/dev.js +1 -1
- package/src/dev/explore.js +1 -0
- package/src/dev/help.yaml +17 -11
- package/src/dev/syscache.js +56 -0
- package/src/handlers/xml_handler.js +1 -1
- package/src/origami/{domNodeToObject.js → domObject.js} +16 -4
- package/src/origami/fetch.js +1 -22
- package/src/origami/htmlDom.js +21 -0
- package/src/origami/htmlParse.js +6 -13
- package/src/origami/once.js +4 -1
- package/src/origami/ori.js +10 -11
- package/src/origami/origami.js +4 -1
- package/src/origami/volatile.js +1 -0
- package/src/origami/xmlDom.js +32 -0
- package/src/origami/xmlParse.js +6 -24
- package/src/server/constructResponse.js +47 -12
- package/src/server/server.js +69 -42
- package/src/dev/changes2.js +0 -38
- package/src/origami/project.js +0 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/origami",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0-beta.1",
|
|
4
4
|
"description": "Web Origami language, CLI, framework, and server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -13,24 +13,24 @@
|
|
|
13
13
|
"main": "./main.js",
|
|
14
14
|
"types": "./index.ts",
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@types/node": "25.
|
|
17
|
-
"typescript": "
|
|
16
|
+
"@types/node": "25.9.1",
|
|
17
|
+
"typescript": "6.0.3"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@hpcc-js/wasm-graphviz": "
|
|
21
|
-
"@weborigami/async-tree": "0.
|
|
20
|
+
"@hpcc-js/wasm-graphviz": "1.21.7",
|
|
21
|
+
"@weborigami/async-tree": "0.7.0-beta.1",
|
|
22
22
|
"@weborigami/json-feed-to-rss": "1.0.1",
|
|
23
|
-
"@weborigami/language": "0.
|
|
24
|
-
"css-tree": "3.1
|
|
23
|
+
"@weborigami/language": "0.7.0-beta.1",
|
|
24
|
+
"css-tree": "3.2.1",
|
|
25
25
|
"highlight.js": "11.11.1",
|
|
26
|
-
"jsdom": "
|
|
27
|
-
"marked": "
|
|
28
|
-
"marked-gfm-heading-id": "4.1.
|
|
29
|
-
"marked-highlight": "2.2.
|
|
30
|
-
"marked-smartypants": "1.1.
|
|
26
|
+
"jsdom": "29.1.1",
|
|
27
|
+
"marked": "18.0.4",
|
|
28
|
+
"marked-gfm-heading-id": "4.1.4",
|
|
29
|
+
"marked-highlight": "2.2.4",
|
|
30
|
+
"marked-smartypants": "1.1.12",
|
|
31
31
|
"sharp": "0.34.5",
|
|
32
32
|
"whatwg-mimetype": "5.0.0",
|
|
33
|
-
"yaml": "2.
|
|
33
|
+
"yaml": "2.9.0"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"test": "node --test --test-reporter=spec",
|
package/src/cli/cli.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Tree } from "@weborigami/async-tree";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
activeProjectRoot,
|
|
6
|
+
formatError,
|
|
7
|
+
projectRootFromPath,
|
|
8
|
+
} from "@weborigami/language";
|
|
5
9
|
import path from "node:path";
|
|
6
10
|
import process, { stdout } from "node:process";
|
|
7
11
|
import help from "../dev/help.js";
|
|
@@ -12,10 +16,6 @@ const TypedArray = Object.getPrototypeOf(Uint8Array);
|
|
|
12
16
|
async function main(...args) {
|
|
13
17
|
const expression = args.join(" ");
|
|
14
18
|
|
|
15
|
-
// Find the project root.
|
|
16
|
-
const currentDirectory = process.cwd();
|
|
17
|
-
const projectRoot = await projectRootFromPath(currentDirectory);
|
|
18
|
-
|
|
19
19
|
// If no arguments were passed, show usage.
|
|
20
20
|
if (!expression) {
|
|
21
21
|
const usage = await help();
|
|
@@ -23,6 +23,11 @@ async function main(...args) {
|
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// Find the project root.
|
|
27
|
+
const currentDirectory = process.cwd();
|
|
28
|
+
const projectRoot = await projectRootFromPath(currentDirectory);
|
|
29
|
+
activeProjectRoot.set(projectRoot);
|
|
30
|
+
|
|
26
31
|
// Traverse from the project root to the current directory.
|
|
27
32
|
const relative = path.relative(projectRoot.path, currentDirectory);
|
|
28
33
|
const parent = await Tree.traversePath(projectRoot, relative);
|
package/src/common/serialize.js
CHANGED
|
@@ -43,6 +43,9 @@ export async function toJson(object) {
|
|
|
43
43
|
* @returns {Promise<string>}
|
|
44
44
|
*/
|
|
45
45
|
export async function toYaml(object) {
|
|
46
|
+
// TODO: The toPlainValue will remove trailing slashes from keys, which should
|
|
47
|
+
// only happen for maps that support trailing slash keys. For maps that don't,
|
|
48
|
+
// we should preserve trailing slashes.
|
|
46
49
|
const serializable = await toPlainValue(object, reduceToMap);
|
|
47
50
|
return YAML.stringify(serializable);
|
|
48
51
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isUnpackable, scope, trailingSlash } from "@weborigami/async-tree";
|
|
2
|
-
import {
|
|
2
|
+
import { getGlobalsForTree } from "@weborigami/language";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Add support for commands prefixed with `!`.
|
|
@@ -28,11 +28,11 @@ export default function OriCommandTransform(Base) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// Key is an Origami command; invoke it.
|
|
31
|
-
const globals =
|
|
31
|
+
const globals = getGlobalsForTree(this);
|
|
32
32
|
const commandName = trailingSlash.remove(key.slice(1).trim());
|
|
33
33
|
|
|
34
34
|
// Look for command as a global or Dev command
|
|
35
|
-
const command = globals[commandName] ?? globals
|
|
35
|
+
const command = globals?.[commandName] ?? globals?.Dev?.[commandName];
|
|
36
36
|
if (command) {
|
|
37
37
|
value = await command(this);
|
|
38
38
|
} else {
|
package/src/dev/changes.js
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
args,
|
|
3
|
-
isStringlike,
|
|
4
|
-
toString,
|
|
5
|
-
trailingSlash,
|
|
6
|
-
Tree,
|
|
7
|
-
} from "@weborigami/async-tree";
|
|
1
|
+
import { args, isStringlike, toString, Tree } from "@weborigami/async-tree";
|
|
8
2
|
|
|
9
3
|
/**
|
|
10
4
|
* Given an old tree and a new tree, return a tree of changes indicated
|
|
@@ -25,51 +19,20 @@ export default async function changes(oldMaplike, newMaplike) {
|
|
|
25
19
|
position: 2,
|
|
26
20
|
});
|
|
27
21
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const oldKeysNormalized = oldKeys.map(trailingSlash.remove);
|
|
32
|
-
const newKeysNormalized = newKeys.map(trailingSlash.remove);
|
|
33
|
-
|
|
34
|
-
let result;
|
|
35
|
-
|
|
36
|
-
for (const oldKey of oldKeys) {
|
|
37
|
-
const oldNormalized = trailingSlash.remove(oldKey);
|
|
38
|
-
if (!newKeysNormalized.includes(oldNormalized)) {
|
|
39
|
-
result ??= {};
|
|
40
|
-
result[oldKey] = "deleted";
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const oldValue = await oldTree.get(oldKey);
|
|
45
|
-
const newValue = await newTree.get(oldKey);
|
|
46
|
-
|
|
47
|
-
if (Tree.isMap(oldValue) && Tree.isMap(newValue)) {
|
|
48
|
-
const treeChanges = await changes(oldValue, newValue);
|
|
49
|
-
if (treeChanges && Object.keys(treeChanges).length > 0) {
|
|
50
|
-
result ??= {};
|
|
51
|
-
result[oldKey] = treeChanges;
|
|
52
|
-
}
|
|
53
|
-
} else if (isStringlike(oldValue) && isStringlike(newValue)) {
|
|
54
|
-
const oldText = toString(oldValue);
|
|
55
|
-
const newText = toString(newValue);
|
|
56
|
-
if (oldText !== newText) {
|
|
57
|
-
result ??= {};
|
|
58
|
-
result[oldKey] = "changed";
|
|
59
|
-
}
|
|
60
|
-
} else {
|
|
61
|
-
result ??= {};
|
|
62
|
-
result[oldKey] = "changed";
|
|
63
|
-
}
|
|
64
|
-
}
|
|
22
|
+
const combination = await Tree.combine(oldTree, newTree, compare);
|
|
23
|
+
return combination;
|
|
24
|
+
}
|
|
65
25
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
26
|
+
function compare(oldValue, newValue) {
|
|
27
|
+
if (oldValue !== undefined && newValue === undefined) {
|
|
28
|
+
return "deleted";
|
|
29
|
+
} else if (oldValue === undefined && newValue !== undefined) {
|
|
30
|
+
return "added";
|
|
31
|
+
} else if (isStringlike(oldValue) && isStringlike(newValue)) {
|
|
32
|
+
const oldText = toString(oldValue);
|
|
33
|
+
const newText = toString(newValue);
|
|
34
|
+
return oldText === newText ? undefined : "changed";
|
|
35
|
+
} else {
|
|
36
|
+
return oldValue === newValue ? undefined : "changed";
|
|
72
37
|
}
|
|
73
|
-
|
|
74
|
-
return result;
|
|
75
38
|
}
|
package/src/dev/debug2/debug2.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { OrigamiFileMap } from "@weborigami/language";
|
|
2
|
-
import path from "node:path";
|
|
3
1
|
import debugParent from "./debugParent.js";
|
|
4
2
|
|
|
5
3
|
/**
|
|
@@ -42,39 +40,7 @@ export default async function debug2(code, state) {
|
|
|
42
40
|
parentPath,
|
|
43
41
|
});
|
|
44
42
|
|
|
45
|
-
// Watch the parent files for changes
|
|
46
|
-
const tree = new OrigamiFileMap(parentPath);
|
|
47
|
-
tree.watch();
|
|
48
|
-
tree.addEventListener?.("change", async (event) => {
|
|
49
|
-
// @ts-ignore
|
|
50
|
-
const { filePath } = event.options;
|
|
51
|
-
if (isJavaScriptFile(filePath)) {
|
|
52
|
-
// Need to restart the child process
|
|
53
|
-
console.log("JavaScript file changed, restarting server…");
|
|
54
|
-
await server.restart();
|
|
55
|
-
} else if (path.basename(filePath) === "package.json") {
|
|
56
|
-
// Need to restart the child process
|
|
57
|
-
console.log("package.json changed, restarting server…");
|
|
58
|
-
await server.restart();
|
|
59
|
-
} else {
|
|
60
|
-
// Just have the child reevaluate the expression
|
|
61
|
-
console.log("File changed, reloading site…");
|
|
62
|
-
await server.reevaluate();
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// When server closes, stop watching for file changes
|
|
67
|
-
server.on("close", () => {
|
|
68
|
-
tree.unwatch();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
43
|
console.log(`Server running at ${server.origin}. Press Ctrl+C to stop.`);
|
|
72
44
|
}
|
|
73
45
|
debug2.needsState = true;
|
|
74
46
|
debug2.unevaluatedArgs = true;
|
|
75
|
-
|
|
76
|
-
function isJavaScriptFile(filePath) {
|
|
77
|
-
const extname = path.extname(filePath).toLowerCase();
|
|
78
|
-
const jsExtensions = [".cjs", ".js", ".mjs", ".ts"];
|
|
79
|
-
return jsExtensions.includes(extname);
|
|
80
|
-
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { AsyncMap, Tree } from "@weborigami/async-tree";
|
|
2
|
+
import {
|
|
3
|
+
activeProjectRoot,
|
|
4
|
+
projectRootFromPath,
|
|
5
|
+
systemCache,
|
|
6
|
+
} from "@weborigami/language";
|
|
1
7
|
import http from "node:http";
|
|
8
|
+
import path from "node:path";
|
|
2
9
|
import { requestListener } from "../../server/server.js";
|
|
3
10
|
import expressionTree from "./expressionTree.js";
|
|
4
11
|
|
|
@@ -36,13 +43,26 @@ if (parentPath === undefined) {
|
|
|
36
43
|
fail("Missing Origami parent");
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
const
|
|
46
|
+
const projectRoot = await projectRootFromPath(parentPath);
|
|
47
|
+
activeProjectRoot.set(projectRoot);
|
|
48
|
+
projectRoot.watch();
|
|
40
49
|
|
|
41
|
-
//
|
|
42
|
-
|
|
50
|
+
// Traverse from the project root to the indicated parent.
|
|
51
|
+
const relative = path.relative(projectRoot.path, parentPath);
|
|
52
|
+
const parent = await Tree.traversePath(projectRoot, relative);
|
|
43
53
|
|
|
44
|
-
//
|
|
45
|
-
|
|
54
|
+
// Notify parent if a file changes
|
|
55
|
+
projectRoot.addEventListener("change", async (/** @type {any} */ event) => {
|
|
56
|
+
const { filePath } = event.options;
|
|
57
|
+
if (filePath) {
|
|
58
|
+
process.send?.({ type: "VALUE_CHANGE", path: filePath });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const quiet = process.env.ORIGAMI_QUIET === "1";
|
|
63
|
+
|
|
64
|
+
// Get a handle to the tree produced by evaluating the expression
|
|
65
|
+
const treeHandle = await handleToEvaluatedExpression(expression, parent);
|
|
46
66
|
|
|
47
67
|
// Serve the tree of resources
|
|
48
68
|
const listener = requestListener(treeHandle, { quiet });
|
|
@@ -59,20 +79,26 @@ server.on("connection", (socket) => {
|
|
|
59
79
|
server.keepAliveTimeout = 1000;
|
|
60
80
|
server.headersTimeout = 5000;
|
|
61
81
|
|
|
62
|
-
//
|
|
63
|
-
let
|
|
82
|
+
// Closing state
|
|
83
|
+
let closing = false;
|
|
64
84
|
let serverClosed = false;
|
|
65
85
|
|
|
66
|
-
function
|
|
67
|
-
if (
|
|
68
|
-
|
|
86
|
+
function beginClose() {
|
|
87
|
+
if (closing) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
closing = true;
|
|
69
92
|
|
|
70
93
|
// Stop accepting new connections.
|
|
71
94
|
server.close(() => {
|
|
72
95
|
serverClosed = true;
|
|
73
|
-
|
|
96
|
+
maybeFinishClose();
|
|
74
97
|
});
|
|
75
98
|
|
|
99
|
+
// Stop watching files
|
|
100
|
+
projectRoot.unwatch();
|
|
101
|
+
|
|
76
102
|
// Give in-flight requests a moment, then force-close remaining sockets.
|
|
77
103
|
const GRACE_MS = 1200;
|
|
78
104
|
setTimeout(() => {
|
|
@@ -81,7 +107,7 @@ function beginDrain() {
|
|
|
81
107
|
socket.destroy();
|
|
82
108
|
}
|
|
83
109
|
// socket "close" events will shrink the set; check again soon.
|
|
84
|
-
setTimeout(
|
|
110
|
+
setTimeout(maybeFinishClose, 50).unref();
|
|
85
111
|
}, GRACE_MS).unref();
|
|
86
112
|
|
|
87
113
|
// Absolute last resort: don’t hang forever.
|
|
@@ -89,50 +115,54 @@ function beginDrain() {
|
|
|
89
115
|
setTimeout(() => process.exit(0), HARD_MS).unref();
|
|
90
116
|
}
|
|
91
117
|
|
|
92
|
-
async function
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
118
|
+
async function handleToEvaluatedExpression(expression, parent) {
|
|
119
|
+
const handle = Object.assign(new AsyncMap(), {
|
|
120
|
+
async get(key) {
|
|
121
|
+
const tree = await this.getTree();
|
|
122
|
+
return tree.get(key);
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async getTree() {
|
|
126
|
+
const tree = await systemCache.getOrInsertComputedAsync(
|
|
127
|
+
"_debug",
|
|
128
|
+
async () =>
|
|
129
|
+
expressionTree({
|
|
130
|
+
expression,
|
|
131
|
+
parent,
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
return tree;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
async keys() {
|
|
138
|
+
const tree = await this.getTree();
|
|
139
|
+
return tree.keys();
|
|
140
|
+
},
|
|
96
141
|
});
|
|
97
|
-
if (!tree) {
|
|
98
|
-
fail("Dev.debug2: expression did not evaluate to a maplike resource tree");
|
|
99
|
-
}
|
|
100
|
-
Object.setPrototypeOf(treeHandle, tree);
|
|
101
142
|
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
for (const key of Object.getOwnPropertyNames(treeHandle)) {
|
|
106
|
-
delete treeHandle[key];
|
|
107
|
-
}
|
|
108
|
-
for (const key of Object.getOwnPropertySymbols(treeHandle)) {
|
|
109
|
-
delete treeHandle[key];
|
|
110
|
-
}
|
|
111
|
-
} catch {
|
|
112
|
-
// Ignore errors.
|
|
113
|
-
}
|
|
143
|
+
// Trigger initial expression evaluation but don't wait for it. This lets some
|
|
144
|
+
// evaluation happen while the user launches/refreshes their browser.
|
|
145
|
+
handle.getTree();
|
|
114
146
|
|
|
115
|
-
|
|
147
|
+
return handle;
|
|
116
148
|
}
|
|
117
149
|
|
|
118
|
-
function
|
|
119
|
-
if (!
|
|
150
|
+
function maybeFinishClose() {
|
|
151
|
+
if (!closing) return;
|
|
120
152
|
if (serverClosed && sockets.size === 0) {
|
|
121
|
-
process.send?.({ type: "
|
|
153
|
+
process.send?.({ type: "CLOSED" });
|
|
122
154
|
process.exit(0);
|
|
123
155
|
}
|
|
124
156
|
}
|
|
125
157
|
|
|
126
|
-
//
|
|
158
|
+
// Close when instructed by parent, or if parent dies.
|
|
127
159
|
process.on("message", async (/** @type {any} */ message) => {
|
|
128
|
-
if (message?.type === "
|
|
129
|
-
|
|
130
|
-
} else if (message?.type === "REEVALUATE") {
|
|
131
|
-
await evaluateExpression();
|
|
160
|
+
if (message?.type === "CLOSE") {
|
|
161
|
+
beginClose();
|
|
132
162
|
}
|
|
133
163
|
});
|
|
134
|
-
process.on("SIGTERM",
|
|
135
|
-
process.on("SIGINT",
|
|
164
|
+
process.on("SIGTERM", beginClose);
|
|
165
|
+
process.on("SIGINT", beginClose);
|
|
136
166
|
|
|
137
167
|
process.on("disconnect", () => {
|
|
138
168
|
// Parent process died, exit immediately
|
|
@@ -8,4 +8,5 @@ export { default as index } from "../../origami/indexPage.js";
|
|
|
8
8
|
export { default as yaml } from "../../origami/yaml.js";
|
|
9
9
|
export { default as explore } from "../explore.js";
|
|
10
10
|
export { default as svg } from "../svg.js";
|
|
11
|
+
export { default as syscache } from "../syscache.js";
|
|
11
12
|
export { default as version } from "../version.js";
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { fork } from "node:child_process";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import http from "node:http";
|
|
4
|
+
import path from "node:path";
|
|
4
5
|
import { findOpenPort } from "../../common/findOpenPort.js";
|
|
5
6
|
|
|
6
|
-
const PUBLIC_HOST = "127.0.0.1";
|
|
7
|
-
|
|
8
7
|
// Module that loads the server in the child process
|
|
9
8
|
const childModuleUrl = new URL("./debugChild.js", import.meta.url);
|
|
10
9
|
|
|
@@ -58,20 +57,18 @@ export default async function debugParent(options) {
|
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
const port = options.port ?? (await findOpenPort());
|
|
61
|
-
publicOrigin = `http
|
|
60
|
+
publicOrigin = `http://localhost:${port}`;
|
|
62
61
|
|
|
63
62
|
publicServer = http.createServer(proxyRequest);
|
|
64
|
-
await new Promise((resolve) =>
|
|
65
|
-
publicServer.listen(port, PUBLIC_HOST, resolve),
|
|
66
|
-
);
|
|
63
|
+
await new Promise((resolve) => publicServer.listen(port, undefined, resolve));
|
|
67
64
|
await startChild(options);
|
|
68
65
|
|
|
69
66
|
emitter = Object.assign(new EventEmitter(), {
|
|
70
67
|
close,
|
|
71
68
|
origin: publicOrigin,
|
|
72
|
-
reevaluate,
|
|
73
69
|
restart: () => startChild(options),
|
|
74
70
|
});
|
|
71
|
+
|
|
75
72
|
return emitter;
|
|
76
73
|
}
|
|
77
74
|
|
|
@@ -112,16 +109,16 @@ async function drainAndStopChild(childProcess) {
|
|
|
112
109
|
return;
|
|
113
110
|
}
|
|
114
111
|
|
|
115
|
-
// Ask it to
|
|
112
|
+
// Ask it to close first.
|
|
116
113
|
try {
|
|
117
|
-
childProcess.send({ type: "
|
|
114
|
+
childProcess.send({ type: "CLOSE" });
|
|
118
115
|
} catch {
|
|
119
116
|
// ignore
|
|
120
117
|
}
|
|
121
118
|
|
|
122
|
-
const
|
|
119
|
+
const closed = new Promise((resolve) => {
|
|
123
120
|
const onMessage = (msg) => {
|
|
124
|
-
if (msg && typeof msg === "object" && msg.type === "
|
|
121
|
+
if (msg && typeof msg === "object" && msg.type === "CLOSED") {
|
|
125
122
|
cleanup(resolve);
|
|
126
123
|
}
|
|
127
124
|
};
|
|
@@ -140,7 +137,7 @@ async function drainAndStopChild(childProcess) {
|
|
|
140
137
|
// Give it a short grace window to finish in-flight work.
|
|
141
138
|
const GRACE_MS = 1500;
|
|
142
139
|
await Promise.race([
|
|
143
|
-
|
|
140
|
+
closed,
|
|
144
141
|
new Promise((r) => setTimeout(r, GRACE_MS).unref()),
|
|
145
142
|
]);
|
|
146
143
|
|
|
@@ -157,6 +154,20 @@ async function drainAndStopChild(childProcess) {
|
|
|
157
154
|
}, GRACE_MS).unref();
|
|
158
155
|
}
|
|
159
156
|
|
|
157
|
+
function isJavaScriptFile(filePath) {
|
|
158
|
+
const extname = path.extname(filePath).toLowerCase();
|
|
159
|
+
const jsExtensions = [".cjs", ".js", ".mjs", ".ts"];
|
|
160
|
+
return jsExtensions.includes(extname);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function onFileChange(filePath) {
|
|
164
|
+
if (isJavaScriptFile(filePath)) {
|
|
165
|
+
// Need to restart the child process
|
|
166
|
+
console.log("JavaScript file changed, restarting server…");
|
|
167
|
+
await emitter.restart();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
160
171
|
/**
|
|
161
172
|
* Proxy incoming requests to the active child server, or return a 503 if not
|
|
162
173
|
* ready.
|
|
@@ -186,7 +197,7 @@ function proxyRequest(request, response) {
|
|
|
186
197
|
|
|
187
198
|
const upstreamRequest = http.request(
|
|
188
199
|
{
|
|
189
|
-
host:
|
|
200
|
+
host: "localhost",
|
|
190
201
|
port,
|
|
191
202
|
method: request.method,
|
|
192
203
|
path: request.url,
|
|
@@ -237,30 +248,6 @@ function proxyRequest(request, response) {
|
|
|
237
248
|
request.pipe(upstreamRequest);
|
|
238
249
|
}
|
|
239
250
|
|
|
240
|
-
async function reevaluate() {
|
|
241
|
-
if (!activeChild) {
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const child = activeChild;
|
|
246
|
-
|
|
247
|
-
// Wait for the next EVALUATED message from the child
|
|
248
|
-
const evaluated = /** @type {Promise<void>} */ (
|
|
249
|
-
new Promise((resolve) => {
|
|
250
|
-
const onMessage = (/** @type {any} */ msg) => {
|
|
251
|
-
if (msg && typeof msg === "object" && msg.type === "EVALUATED") {
|
|
252
|
-
child.process.off("message", onMessage);
|
|
253
|
-
resolve();
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
child.process.on("message", onMessage);
|
|
257
|
-
})
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
child.process.send({ type: "REEVALUATE" });
|
|
261
|
-
await evaluated;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
251
|
/**
|
|
265
252
|
* Start a new child process.
|
|
266
253
|
*
|
|
@@ -332,11 +319,12 @@ function startChild(options) {
|
|
|
332
319
|
// console.log("Child process superseded by newer one, killing it...");
|
|
333
320
|
childProcess.kill("SIGTERM");
|
|
334
321
|
}
|
|
335
|
-
} else if (
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
322
|
+
} else if (
|
|
323
|
+
message.type === "VALUE_CHANGE" &&
|
|
324
|
+
typeof message.path === "string"
|
|
325
|
+
) {
|
|
326
|
+
// REVIEW: We don't await this -- is that a problem?
|
|
327
|
+
onFileChange(message.path);
|
|
340
328
|
} else if (message.type === "FATAL") {
|
|
341
329
|
// Child couldn't start (import error, etc.)
|
|
342
330
|
// Keep previous active child if any; otherwise we'll serve 500/503.
|
|
@@ -23,27 +23,10 @@ import * as debugCommands from "./debugCommands.js";
|
|
|
23
23
|
* Also transform a simple object result to YAML for viewing.
|
|
24
24
|
*
|
|
25
25
|
* @typedef {import("@weborigami/async-tree").Maplike} Maplike
|
|
26
|
-
* @typedef {import("@weborigami/async-tree").Packed} Packed
|
|
27
26
|
*
|
|
28
|
-
* @param {Maplike
|
|
27
|
+
* @param {Maplike} input
|
|
29
28
|
*/
|
|
30
29
|
export default function debugTransform(input) {
|
|
31
|
-
if (isUnpackable(input)) {
|
|
32
|
-
// If the value isn't a tree, but has a tree attached via an `unpack`
|
|
33
|
-
// method, destructively wrap the unpack method to add this transform.
|
|
34
|
-
const original = input.unpack.bind(input);
|
|
35
|
-
input.unpack = async () => {
|
|
36
|
-
const content = await original();
|
|
37
|
-
if (!Tree.isTraversable(content) || typeof content === "function") {
|
|
38
|
-
return content;
|
|
39
|
-
}
|
|
40
|
-
/** @type {any} */
|
|
41
|
-
let tree = Tree.from(content);
|
|
42
|
-
return debugTransform(tree);
|
|
43
|
-
};
|
|
44
|
-
return input;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
30
|
const source = Tree.from(input, { deep: true });
|
|
48
31
|
|
|
49
32
|
return Object.assign(new AsyncMap(), {
|
|
@@ -85,8 +68,10 @@ export default function debugTransform(input) {
|
|
|
85
68
|
|
|
86
69
|
// Ensure this transform is applied to any map result, or any object with
|
|
87
70
|
// an unpack method that returns a map.
|
|
88
|
-
if (Tree.isMap(value)
|
|
71
|
+
if (Tree.isMap(value)) {
|
|
89
72
|
value = debugTransform(value);
|
|
73
|
+
} else if (value?.unpack) {
|
|
74
|
+
value = debugPackedValue(value);
|
|
90
75
|
}
|
|
91
76
|
|
|
92
77
|
return value;
|
|
@@ -99,15 +84,42 @@ export default function debugTransform(input) {
|
|
|
99
84
|
// If this value is given to the server, the server will call this pack()
|
|
100
85
|
// method. We respond with the index page.
|
|
101
86
|
async pack() {
|
|
102
|
-
|
|
87
|
+
// @ts-ignore
|
|
88
|
+
return source.pack?.() ?? this.get("index.html");
|
|
103
89
|
},
|
|
104
90
|
|
|
91
|
+
// @ts-ignore
|
|
92
|
+
parent: source.parent,
|
|
93
|
+
|
|
105
94
|
source,
|
|
106
95
|
|
|
107
96
|
trailingSlashKeys: true,
|
|
108
97
|
});
|
|
109
98
|
}
|
|
110
99
|
|
|
100
|
+
/**
|
|
101
|
+
* If the value isn't a tree, but has a tree attached via an `unpack` method,
|
|
102
|
+
* destructively wrap the unpack method to add this transform.
|
|
103
|
+
*
|
|
104
|
+
* @typedef {import("@weborigami/async-tree").Packed} Packed
|
|
105
|
+
* @param {Packed} packed
|
|
106
|
+
*/
|
|
107
|
+
function debugPackedValue(packed) {
|
|
108
|
+
if (isUnpackable(packed)) {
|
|
109
|
+
const original = packed.unpack.bind(packed);
|
|
110
|
+
packed.unpack = async () => {
|
|
111
|
+
const content = await original();
|
|
112
|
+
if (!Tree.isTraversable(content) || typeof content === "function") {
|
|
113
|
+
return content;
|
|
114
|
+
}
|
|
115
|
+
/** @type {any} */
|
|
116
|
+
let tree = Tree.from(content);
|
|
117
|
+
return debugTransform(tree);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return packed;
|
|
121
|
+
}
|
|
122
|
+
|
|
111
123
|
async function invokeOrigamiCommand(tree, key) {
|
|
112
124
|
// Key is an Origami command; invoke it.
|
|
113
125
|
const commandName = trailingSlash.remove(key.slice(1).trim());
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
setParent,
|
|
5
5
|
Tree,
|
|
6
6
|
} from "@weborigami/async-tree";
|
|
7
|
-
import { evaluate,
|
|
7
|
+
import { evaluate, getGlobalsForTree } from "@weborigami/language";
|
|
8
8
|
import debugTransform from "./debugTransform.js";
|
|
9
9
|
|
|
10
10
|
// So we can distinguish different trees in the debugger
|
|
@@ -16,18 +16,22 @@ let version = 0;
|
|
|
16
16
|
*
|
|
17
17
|
* @param {Object} options
|
|
18
18
|
* @param {string} options.expression
|
|
19
|
-
* @param {
|
|
19
|
+
* @param {import("@weborigami/language").OrigamiFileMap} options.parent
|
|
20
20
|
*/
|
|
21
21
|
export default async function expressionTree(options) {
|
|
22
|
-
const { expression,
|
|
22
|
+
const { expression, parent } = options;
|
|
23
23
|
|
|
24
|
-
const
|
|
25
|
-
|
|
24
|
+
const globals = getGlobalsForTree(parent);
|
|
25
|
+
|
|
26
|
+
const source = {
|
|
27
|
+
text: expression,
|
|
28
|
+
cachePath: "_tree",
|
|
29
|
+
};
|
|
26
30
|
|
|
27
31
|
let maplike;
|
|
28
32
|
try {
|
|
29
33
|
// Evaluate the expression
|
|
30
|
-
maplike = await evaluate(
|
|
34
|
+
maplike = await evaluate(source, { globals, mode: "shell", parent });
|
|
31
35
|
if (isUnpackable(maplike)) {
|
|
32
36
|
maplike = await maplike.unpack();
|
|
33
37
|
}
|