@weborigami/origami 0.0.66-beta.1 → 0.0.66-beta.2
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/exports/PathTransform.js +3 -1
- package/exports/exports.js +3 -0
- package/package.json +7 -7
- package/src/builtins/@crawl.js +12 -24
- package/src/builtins/@debug.js +4 -2
- package/src/builtins/@inners.js +6 -9
- package/src/builtins/@mapFn.js +1 -1
- package/src/builtins/@post.js +45 -0
- package/src/builtins/@serve.js +0 -3
- package/src/builtins/@slash.js +1 -0
- package/src/builtins/@watch.js +12 -21
- package/src/common/CommandModulesTransform.js +2 -0
- package/src/common/ExplorableSiteTransform.js +8 -11
- package/src/common/FilterTree.js +2 -2
- package/src/common/GlobTree.js +14 -13
- package/src/misc/treeDot.js +3 -2
- package/src/server/constructResponse.js +10 -10
- package/src/server/parsePostData.js +35 -0
- package/src/server/server.js +14 -2
package/exports/PathTransform.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { trailingSlash } from "@weborigami/async-tree";
|
|
2
|
+
|
|
1
3
|
export default function PathTransform(Base) {
|
|
2
4
|
return class Path extends Base {
|
|
3
5
|
async get(key) {
|
|
@@ -6,7 +8,7 @@ export default function PathTransform(Base) {
|
|
|
6
8
|
// @ts-ignore
|
|
7
9
|
const path = this[PathTransform.pathKey]
|
|
8
10
|
? // @ts-ignore
|
|
9
|
-
`${this[PathTransform.pathKey]}
|
|
11
|
+
`${trailingSlash.add(this[PathTransform.pathKey])}${key}`
|
|
10
12
|
: key;
|
|
11
13
|
value[PathTransform.pathKey] = path;
|
|
12
14
|
}
|
package/exports/exports.js
CHANGED
|
@@ -72,6 +72,7 @@ export { default as paginateFn } from "../src/builtins/@paginateFn.js";
|
|
|
72
72
|
export { default as parent } from "../src/builtins/@parent.js";
|
|
73
73
|
export { default as perf } from "../src/builtins/@perf.js";
|
|
74
74
|
export { default as plain } from "../src/builtins/@plain.js";
|
|
75
|
+
export { default as post } from "../src/builtins/@post.js";
|
|
75
76
|
export { default as project } from "../src/builtins/@project.js";
|
|
76
77
|
export { default as redirect } from "../src/builtins/@redirect.js";
|
|
77
78
|
export { default as regexMatch } from "../src/builtins/@regexMatch.js";
|
|
@@ -84,6 +85,7 @@ export { default as setDeep } from "../src/builtins/@setDeep.js";
|
|
|
84
85
|
export { default as shell } from "../src/builtins/@shell.js";
|
|
85
86
|
export { default as shuffle } from "../src/builtins/@shuffle.js";
|
|
86
87
|
export { default as sitemap } from "../src/builtins/@sitemap.js";
|
|
88
|
+
export * from "../src/builtins/@slash.js";
|
|
87
89
|
export { default as slug } from "../src/builtins/@slug.js";
|
|
88
90
|
export { default as sort } from "../src/builtins/@sort.js";
|
|
89
91
|
export { default as sortFn } from "../src/builtins/@sortFn.js";
|
|
@@ -139,4 +141,5 @@ export { default as origamiHighlightDefinition } from "../src/misc/origamiHighli
|
|
|
139
141
|
export { default as treeDot } from "../src/misc/treeDot.js";
|
|
140
142
|
export { default as constructResponse } from "../src/server/constructResponse.js";
|
|
141
143
|
export * from "../src/server/mediaTypes.js";
|
|
144
|
+
export { default as parsePostData } from "../src/server/parsePostData.js";
|
|
142
145
|
export * from "../src/server/server.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/origami",
|
|
3
|
-
"version": "0.0.66-beta.
|
|
3
|
+
"version": "0.0.66-beta.2",
|
|
4
4
|
"description": "Web Origami language, CLI, framework, and server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
"main": "./exports/exports.js",
|
|
14
14
|
"types": "./index.ts",
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@types/node": "22.
|
|
17
|
-
"typescript": "5.
|
|
16
|
+
"@types/node": "22.7.4",
|
|
17
|
+
"typescript": "5.6.2"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@weborigami/async-tree": "0.0.66-beta.
|
|
21
|
-
"@weborigami/language": "0.0.66-beta.
|
|
22
|
-
"@weborigami/types": "0.0.66-beta.
|
|
20
|
+
"@weborigami/async-tree": "0.0.66-beta.2",
|
|
21
|
+
"@weborigami/language": "0.0.66-beta.2",
|
|
22
|
+
"@weborigami/types": "0.0.66-beta.2",
|
|
23
23
|
"exif-parser": "0.1.12",
|
|
24
24
|
"graphviz-wasm": "3.0.2",
|
|
25
25
|
"highlight.js": "11.10.0",
|
|
26
|
-
"marked": "14.1.
|
|
26
|
+
"marked": "14.1.2",
|
|
27
27
|
"marked-gfm-heading-id": "4.1.0",
|
|
28
28
|
"marked-highlight": "2.1.4",
|
|
29
29
|
"marked-smartypants": "1.1.8",
|
package/src/builtins/@crawl.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
deepMerge,
|
|
5
5
|
isPlainObject,
|
|
6
6
|
keysFromPath,
|
|
7
|
+
trailingSlash,
|
|
7
8
|
} from "@weborigami/async-tree";
|
|
8
9
|
import { InvokeFunctionsTransform, extname } from "@weborigami/language";
|
|
9
10
|
import * as utilities from "../common/utilities.js";
|
|
@@ -75,7 +76,7 @@ export default async function crawl(treelike, baseHref) {
|
|
|
75
76
|
for (const resourcePath of resourcePaths) {
|
|
76
77
|
const resourceKeys = adjustKeys(keysFromPath(resourcePath));
|
|
77
78
|
const fn = () => {
|
|
78
|
-
return traverse(tree, ...resourceKeys);
|
|
79
|
+
return Tree.traverse(tree, ...resourceKeys);
|
|
79
80
|
};
|
|
80
81
|
addValueToObject(resources, resourceKeys, fn);
|
|
81
82
|
}
|
|
@@ -100,18 +101,19 @@ export default async function crawl(treelike, baseHref) {
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
// For indexing and storage purposes, treat a path that ends in a trailing slash
|
|
103
|
-
//
|
|
104
|
+
// as if it ends in index.html.
|
|
104
105
|
function adjustKeys(keys) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
adjustedKeys[adjustedKeys.length - 1] = "index.html";
|
|
106
|
+
if (keys.length > 0 && !trailingSlash.has(keys.at(-1))) {
|
|
107
|
+
return keys;
|
|
108
108
|
}
|
|
109
|
+
const adjustedKeys = keys.slice();
|
|
110
|
+
adjustedKeys.push("index.html");
|
|
109
111
|
return adjustedKeys;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
function addValueToObject(object, keys, value) {
|
|
113
115
|
for (let i = 0, current = object; i < keys.length; i++) {
|
|
114
|
-
const key = keys[i];
|
|
116
|
+
const key = trailingSlash.remove(keys[i]);
|
|
115
117
|
if (i === keys.length - 1) {
|
|
116
118
|
// Write out value
|
|
117
119
|
if (isPlainObject(current[key])) {
|
|
@@ -150,7 +152,7 @@ async function* crawlPaths(tree, baseUrl) {
|
|
|
150
152
|
const promisesForPaths = {};
|
|
151
153
|
|
|
152
154
|
// Seed the promise dictionary with robots.txt and the root path.
|
|
153
|
-
const initialPaths = ["/robots.txt", ""];
|
|
155
|
+
const initialPaths = ["/robots.txt", "/"];
|
|
154
156
|
initialPaths.forEach((path) => {
|
|
155
157
|
promisesForPaths[path] = processPath(tree, path, baseUrl);
|
|
156
158
|
});
|
|
@@ -468,14 +470,13 @@ async function processPath(tree, path, baseUrl) {
|
|
|
468
470
|
}
|
|
469
471
|
|
|
470
472
|
// Convert path to keys
|
|
471
|
-
|
|
472
|
-
let keys = path === "" ? [""] : keysFromPath(path);
|
|
473
|
+
const keys = keysFromPath(path);
|
|
473
474
|
|
|
474
475
|
// Traverse tree to get value.
|
|
475
|
-
let value = await traverse(tree, ...keys);
|
|
476
|
+
let value = await Tree.traverse(tree, ...keys);
|
|
476
477
|
if (Tree.isAsyncTree(value)) {
|
|
477
478
|
// Path is actually a directory; see if it has an index.html
|
|
478
|
-
value = await traverse(value, "index.html");
|
|
479
|
+
value = await Tree.traverse(value, "index.html");
|
|
479
480
|
}
|
|
480
481
|
|
|
481
482
|
const adjustedKeys = adjustKeys(keys);
|
|
@@ -508,18 +509,5 @@ async function processPath(tree, path, baseUrl) {
|
|
|
508
509
|
};
|
|
509
510
|
}
|
|
510
511
|
|
|
511
|
-
async function traverse(tree, ...keys) {
|
|
512
|
-
if (tree.resolve && keys.length > 1) {
|
|
513
|
-
// Tree like SiteTree that supports resolve() method
|
|
514
|
-
const lastKey = keys.pop();
|
|
515
|
-
const path = keys.join("/");
|
|
516
|
-
const resolved = tree.resolve(path);
|
|
517
|
-
return resolved.get(lastKey);
|
|
518
|
-
} else {
|
|
519
|
-
// Regular async tree
|
|
520
|
-
return Tree.traverse(tree, ...keys);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
512
|
crawl.usage = `@crawl <tree>\tCrawl a tree`;
|
|
525
513
|
crawl.documentation = "https://weborigami.org/language/@crawl.html";
|
package/src/builtins/@debug.js
CHANGED
|
@@ -39,8 +39,10 @@ function DebugTransform(Base) {
|
|
|
39
39
|
const parent = this;
|
|
40
40
|
|
|
41
41
|
// Since this transform is for diagnostic purposes, cast any treelike
|
|
42
|
-
// result to a tree so we can debug the result too.
|
|
43
|
-
|
|
42
|
+
// result to a tree so we can debug the result too. (Don't do this for
|
|
43
|
+
// functions, as that can be undesirable, e.g., when writing functions
|
|
44
|
+
// that handle POST requests.)
|
|
45
|
+
if (Tree.isTreelike(value) && typeof value !== "function") {
|
|
44
46
|
value = Tree.from(value, { parent });
|
|
45
47
|
if (!isTransformApplied(DebugTransform, value)) {
|
|
46
48
|
value = transformObject(DebugTransform, value);
|
package/src/builtins/@inners.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { Tree } from "@weborigami/async-tree";
|
|
1
|
+
import { trailingSlash, Tree } from "@weborigami/async-tree";
|
|
2
2
|
import getTreeArgument from "../misc/getTreeArgument.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Return the
|
|
5
|
+
* Return the interior nodes of the tree. This relies on subtree keys having
|
|
6
|
+
* trailing slashes.
|
|
6
7
|
*
|
|
7
8
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
8
9
|
* @typedef {import("@weborigami/async-tree").Treelike} Treelike
|
|
@@ -19,12 +20,8 @@ export default async function inners(treelike) {
|
|
|
19
20
|
},
|
|
20
21
|
|
|
21
22
|
async keys() {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
if (await Tree.isKeyForSubtree(tree, key)) {
|
|
25
|
-
subtreeKeys.push(key);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
23
|
+
const keys = [...(await tree.keys())];
|
|
24
|
+
const subtreeKeys = keys.filter(trailingSlash.has);
|
|
28
25
|
return subtreeKeys;
|
|
29
26
|
},
|
|
30
27
|
};
|
|
@@ -32,5 +29,5 @@ export default async function inners(treelike) {
|
|
|
32
29
|
return result;
|
|
33
30
|
}
|
|
34
31
|
|
|
35
|
-
inners.usage = `@inners <tree>\tThe
|
|
32
|
+
inners.usage = `@inners <tree>\tThe interior nodes of the tree`;
|
|
36
33
|
inners.documentation = "https://weborigami.org/cli/builtins.html#inners";
|
package/src/builtins/@mapFn.js
CHANGED
|
@@ -91,7 +91,7 @@ export default function mapFnBuiltin(operation) {
|
|
|
91
91
|
);
|
|
92
92
|
return resultKey;
|
|
93
93
|
}
|
|
94
|
-
const keyFns = cachedKeyFunctions(scopedKeyFn);
|
|
94
|
+
const keyFns = cachedKeyFunctions(scopedKeyFn, deep);
|
|
95
95
|
extendedKeyFn = keyFns.key;
|
|
96
96
|
extendedInverseKeyFn = keyFns.inverseKey;
|
|
97
97
|
} else {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isStringLike,
|
|
3
|
+
isUnpackable,
|
|
4
|
+
toPlainValue,
|
|
5
|
+
toString,
|
|
6
|
+
Tree,
|
|
7
|
+
} from "@weborigami/async-tree";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @this {import("@weborigami/types").AsyncTree|null}
|
|
11
|
+
* @param {string} url
|
|
12
|
+
* @param {any} data
|
|
13
|
+
*/
|
|
14
|
+
export default async function post(url, data) {
|
|
15
|
+
let body;
|
|
16
|
+
let headers;
|
|
17
|
+
if (isUnpackable(data)) {
|
|
18
|
+
data = await data.unpack();
|
|
19
|
+
}
|
|
20
|
+
if (Tree.isTreelike(data)) {
|
|
21
|
+
const value = await toPlainValue(data);
|
|
22
|
+
body = JSON.stringify(value);
|
|
23
|
+
headers = {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
};
|
|
26
|
+
} else if (isStringLike(data)) {
|
|
27
|
+
body = toString(data);
|
|
28
|
+
headers = {
|
|
29
|
+
"Content-Type": "text/plain",
|
|
30
|
+
};
|
|
31
|
+
} else {
|
|
32
|
+
body = data;
|
|
33
|
+
}
|
|
34
|
+
const response = await fetch(url, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
body,
|
|
37
|
+
headers,
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Failed to POST to ${url}. Error ${response.status}: ${response.statusText}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return response.arrayBuffer();
|
|
45
|
+
}
|
package/src/builtins/@serve.js
CHANGED
|
@@ -26,9 +26,6 @@ export default async function serve(treelike, port) {
|
|
|
26
26
|
let tree;
|
|
27
27
|
if (treelike) {
|
|
28
28
|
tree = Tree.from(treelike, { parent: this });
|
|
29
|
-
|
|
30
|
-
// TODO: Instead of applying ExplorableSiteTransform, apply a transform
|
|
31
|
-
// that just maps the empty string to index.html.
|
|
32
29
|
if (!isTransformApplied(ExplorableSiteTransform, tree)) {
|
|
33
30
|
tree = transformObject(ExplorableSiteTransform, tree);
|
|
34
31
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { trailingSlash as default } from "@weborigami/async-tree";
|
package/src/builtins/@watch.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Tree } from "@weborigami/async-tree";
|
|
2
|
+
import { formatError } from "@weborigami/language";
|
|
2
3
|
import ConstantTree from "../common/ConstantTree.js";
|
|
3
4
|
import getTreeArgument from "../misc/getTreeArgument.js";
|
|
4
5
|
|
|
@@ -48,8 +49,8 @@ async function evaluateTree(parent, fn) {
|
|
|
48
49
|
let result;
|
|
49
50
|
try {
|
|
50
51
|
result = await fn.call(parent);
|
|
51
|
-
} catch (error) {
|
|
52
|
-
message =
|
|
52
|
+
} catch (/** @type {any} */ error) {
|
|
53
|
+
message = formatError(error);
|
|
53
54
|
}
|
|
54
55
|
tree = result ? Tree.from(result, { parent }) : undefined;
|
|
55
56
|
if (tree) {
|
|
@@ -64,29 +65,19 @@ async function evaluateTree(parent, fn) {
|
|
|
64
65
|
return tree;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
function messageForError(error) {
|
|
68
|
-
let message = "";
|
|
69
|
-
// Work up to the root cause, displaying intermediate messages as we go up.
|
|
70
|
-
while (error.cause) {
|
|
71
|
-
message += error.message + `\n`;
|
|
72
|
-
error = error.cause;
|
|
73
|
-
}
|
|
74
|
-
if (error.name) {
|
|
75
|
-
message += `${error.name}: `;
|
|
76
|
-
}
|
|
77
|
-
message += error.message;
|
|
78
|
-
return message;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
68
|
// Update an indirect pointer to a target.
|
|
82
69
|
function updateIndirectPointer(indirect, target) {
|
|
83
70
|
// Clean the pointer of any named properties or symbols that have been set
|
|
84
71
|
// directly on it.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
72
|
+
try {
|
|
73
|
+
for (const key of Object.getOwnPropertyNames(indirect)) {
|
|
74
|
+
delete indirect[key];
|
|
75
|
+
}
|
|
76
|
+
for (const key of Object.getOwnPropertySymbols(indirect)) {
|
|
77
|
+
delete indirect[key];
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// Ignore errors.
|
|
90
81
|
}
|
|
91
82
|
|
|
92
83
|
Object.setPrototypeOf(indirect, target);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trailingSlash } from "@weborigami/async-tree";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -18,6 +19,7 @@ export default function CommandsModulesTransform(Base) {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
// See if we have a JS module for the requested key.
|
|
22
|
+
key = trailingSlash.remove(key);
|
|
21
23
|
if (key === undefined || key.endsWith?.(".js")) {
|
|
22
24
|
return undefined;
|
|
23
25
|
}
|
|
@@ -25,11 +25,6 @@ import { isTransformApplied, transformObject } from "../common/utilities.js";
|
|
|
25
25
|
export default function ExplorableSiteTransform(Base) {
|
|
26
26
|
return class ExplorableSite extends Base {
|
|
27
27
|
async get(key) {
|
|
28
|
-
// The empty string key represents "index.html".
|
|
29
|
-
if (key === "") {
|
|
30
|
-
key = "index.html";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
28
|
// Ask the tree if it has the key.
|
|
34
29
|
let value = await super.get(key);
|
|
35
30
|
|
|
@@ -49,11 +44,6 @@ export default function ExplorableSiteTransform(Base) {
|
|
|
49
44
|
if (!isTransformApplied(ExplorableSiteTransform, value)) {
|
|
50
45
|
value = transformObject(ExplorableSiteTransform, value);
|
|
51
46
|
}
|
|
52
|
-
|
|
53
|
-
if (key.endsWith?.("/")) {
|
|
54
|
-
// Instead of return the tree directly, return an index for it.
|
|
55
|
-
value = await index.call(this, value);
|
|
56
|
-
}
|
|
57
47
|
} else if (value?.unpack) {
|
|
58
48
|
// If the value isn't a tree, but has a tree attached via an `unpack`
|
|
59
49
|
// method, wrap the unpack method to add this transform.
|
|
@@ -61,7 +51,8 @@ export default function ExplorableSiteTransform(Base) {
|
|
|
61
51
|
const parent = this;
|
|
62
52
|
value.unpack = async () => {
|
|
63
53
|
const content = await original();
|
|
64
|
-
|
|
54
|
+
// See function notes at @debug
|
|
55
|
+
if (!Tree.isTraversable(content) || typeof content === "function") {
|
|
65
56
|
return content;
|
|
66
57
|
}
|
|
67
58
|
/** @type {any} */
|
|
@@ -77,5 +68,11 @@ export default function ExplorableSiteTransform(Base) {
|
|
|
77
68
|
}
|
|
78
69
|
return value;
|
|
79
70
|
}
|
|
71
|
+
|
|
72
|
+
// If this value is given to the server, the server will call this pack()
|
|
73
|
+
// method. We respond with the index page.
|
|
74
|
+
async pack() {
|
|
75
|
+
return this.get("index.html");
|
|
76
|
+
}
|
|
80
77
|
};
|
|
81
78
|
}
|
package/src/common/FilterTree.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Tree } from "@weborigami/async-tree";
|
|
1
|
+
import { trailingSlash, Tree } from "@weborigami/async-tree";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
@@ -40,7 +40,7 @@ export default class FilterTree {
|
|
|
40
40
|
// must be a tree too.
|
|
41
41
|
const match =
|
|
42
42
|
(!isFilterValueTree && filterValue) ||
|
|
43
|
-
(isFilterValueTree &&
|
|
43
|
+
(isFilterValueTree && trailingSlash.has(key));
|
|
44
44
|
if (match) {
|
|
45
45
|
keys.add(key);
|
|
46
46
|
}
|
package/src/common/GlobTree.js
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DeepObjectTree,
|
|
3
|
-
ObjectTree,
|
|
4
|
-
Tree,
|
|
5
|
-
isPlainObject,
|
|
6
|
-
merge,
|
|
7
|
-
} from "@weborigami/async-tree";
|
|
1
|
+
import { ObjectTree, Tree, merge, trailingSlash } from "@weborigami/async-tree";
|
|
8
2
|
|
|
9
3
|
const globstar = "**";
|
|
10
4
|
|
|
@@ -14,15 +8,17 @@ const globstar = "**";
|
|
|
14
8
|
*/
|
|
15
9
|
export default class GlobTree {
|
|
16
10
|
constructor(globs) {
|
|
17
|
-
this.globs =
|
|
18
|
-
? new DeepObjectTree(globs)
|
|
19
|
-
: Tree.from(globs);
|
|
11
|
+
this.globs = Tree.from(globs, { deep: true });
|
|
20
12
|
}
|
|
21
13
|
|
|
22
14
|
async get(key) {
|
|
23
15
|
if (typeof key !== "string") {
|
|
24
16
|
return undefined;
|
|
25
17
|
}
|
|
18
|
+
|
|
19
|
+
// Remove trailing slash if it exists
|
|
20
|
+
key = trailingSlash.remove(key);
|
|
21
|
+
|
|
26
22
|
let value = await matchGlobs(this.globs, key);
|
|
27
23
|
if (Tree.isAsyncTree(value)) {
|
|
28
24
|
value = Reflect.construct(this.constructor, [value]);
|
|
@@ -35,8 +31,8 @@ export default class GlobTree {
|
|
|
35
31
|
}
|
|
36
32
|
}
|
|
37
33
|
|
|
34
|
+
// Convert the glob to a regular expression
|
|
38
35
|
function matchGlob(glob, text) {
|
|
39
|
-
// Convert the glob to a regular expression
|
|
40
36
|
const regexText = glob
|
|
41
37
|
// Escape special regex characters
|
|
42
38
|
.replace(/[+?^${}()|\[\]\\]/g, "\\$&")
|
|
@@ -49,10 +45,15 @@ function matchGlob(glob, text) {
|
|
|
49
45
|
|
|
50
46
|
async function matchGlobs(globs, text) {
|
|
51
47
|
let value;
|
|
52
|
-
for (
|
|
48
|
+
for (let glob of await globs.keys()) {
|
|
53
49
|
if (typeof glob !== "string") {
|
|
54
50
|
continue;
|
|
55
|
-
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Remove trailing slash if it exists
|
|
54
|
+
glob = trailingSlash.remove(glob);
|
|
55
|
+
|
|
56
|
+
if (glob !== globstar && matchGlob(glob, text)) {
|
|
56
57
|
value = await globs.get(glob);
|
|
57
58
|
if (value !== undefined) {
|
|
58
59
|
break;
|
package/src/misc/treeDot.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
isPlainObject,
|
|
4
4
|
isStringLike,
|
|
5
5
|
toString,
|
|
6
|
+
trailingSlash,
|
|
6
7
|
} from "@weborigami/async-tree";
|
|
7
8
|
import * as serialize from "../common/serialize.js";
|
|
8
9
|
import { keySymbol } from "../common/utilities.js";
|
|
@@ -49,7 +50,7 @@ async function statements(tree, nodePath, nodeLabel, options) {
|
|
|
49
50
|
// Draw edges and collect labels for the nodes they lead to.
|
|
50
51
|
let nodes = new Map();
|
|
51
52
|
for (const key of await tree.keys()) {
|
|
52
|
-
const destPath = nodePath ? `${nodePath}
|
|
53
|
+
const destPath = nodePath ? `${trailingSlash.add(nodePath)}${key}` : key;
|
|
53
54
|
const arc = ` "${nodePath}" -> "${destPath}" [label="${key}"];`;
|
|
54
55
|
result.push(arc);
|
|
55
56
|
|
|
@@ -163,7 +164,7 @@ async function statements(tree, nodePath, nodeLabel, options) {
|
|
|
163
164
|
const label = `label="${icon}${text}"`;
|
|
164
165
|
const color = node.isError ? `; color="red"` : "";
|
|
165
166
|
const fill = node.isError ? `; fillcolor="#FFF4F4"` : "";
|
|
166
|
-
const destPath = nodePath ? `${nodePath}
|
|
167
|
+
const destPath = nodePath ? `${trailingSlash.add(nodePath)}${key}` : key;
|
|
167
168
|
const url = createLinks ? `; URL="${destPath}"` : "";
|
|
168
169
|
result.push(` "${destPath}" [${label}${color}${fill}${url}];`);
|
|
169
170
|
}
|
|
@@ -32,16 +32,6 @@ export default async function constructResponse(request, resource) {
|
|
|
32
32
|
// Determine media type, what data we'll send, and encoding.
|
|
33
33
|
const url = new URL(request.url ?? "", `https://${request.headers.host}`);
|
|
34
34
|
|
|
35
|
-
let mediaType;
|
|
36
|
-
if (resource.mediaType) {
|
|
37
|
-
// Resource indicates its own media type.
|
|
38
|
-
mediaType = resource.mediaType;
|
|
39
|
-
} else {
|
|
40
|
-
// Infer expected media type from file extension on request URL.
|
|
41
|
-
const extension = extname(url.pathname).toLowerCase();
|
|
42
|
-
mediaType = extension ? mediaTypeForExtension[extension] : undefined;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
35
|
if (!url.pathname.endsWith("/") && Tree.isTreelike(resource)) {
|
|
46
36
|
// Treelike resource: redirect to its index page.
|
|
47
37
|
const Location = `${url.pathname}/`;
|
|
@@ -57,6 +47,16 @@ export default async function constructResponse(request, resource) {
|
|
|
57
47
|
resource = await resource.pack();
|
|
58
48
|
}
|
|
59
49
|
|
|
50
|
+
let mediaType;
|
|
51
|
+
if (resource.mediaType) {
|
|
52
|
+
// Resource indicates its own media type.
|
|
53
|
+
mediaType = resource.mediaType;
|
|
54
|
+
} else {
|
|
55
|
+
// Infer expected media type from file extension on request URL.
|
|
56
|
+
const extension = extname(url.pathname).toLowerCase();
|
|
57
|
+
mediaType = extension ? mediaTypeForExtension[extension] : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
60
|
if (
|
|
61
61
|
(mediaType === "application/json" || mediaType === "text/yaml") &&
|
|
62
62
|
!isStringLike(resource)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { toString } from "@weborigami/async-tree";
|
|
2
|
+
|
|
3
|
+
export default async function parsePostData(request) {
|
|
4
|
+
const data = await getPostData(request);
|
|
5
|
+
const type = request.headers["content-type"];
|
|
6
|
+
switch (type) {
|
|
7
|
+
case "application/json":
|
|
8
|
+
return JSON.parse(data);
|
|
9
|
+
|
|
10
|
+
case "application/x-www-form-urlencoded":
|
|
11
|
+
const params = new URLSearchParams(data);
|
|
12
|
+
return Object.fromEntries(params);
|
|
13
|
+
|
|
14
|
+
case "text/plain":
|
|
15
|
+
return toString(data);
|
|
16
|
+
|
|
17
|
+
default:
|
|
18
|
+
return data;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getPostData(request) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
let body = "";
|
|
25
|
+
request.on("data", (chunk) => {
|
|
26
|
+
body += chunk.toString();
|
|
27
|
+
});
|
|
28
|
+
request.on("end", () => {
|
|
29
|
+
resolve(body);
|
|
30
|
+
});
|
|
31
|
+
request.on("error", (error) => {
|
|
32
|
+
reject(error);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
package/src/server/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import { ObjectTree, Tree, keysFromPath } from "@weborigami/async-tree";
|
|
|
2
2
|
import { formatError } from "@weborigami/language";
|
|
3
3
|
import { ServerResponse } from "node:http";
|
|
4
4
|
import constructResponse from "./constructResponse.js";
|
|
5
|
+
import parsePostData from "./parsePostData.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Copy a constructed response to a ServerResponse. Return true if the response
|
|
@@ -81,13 +82,16 @@ export async function handleRequest(request, response, tree) {
|
|
|
81
82
|
? extendTreeScopeWithParams(tree, url)
|
|
82
83
|
: tree;
|
|
83
84
|
|
|
85
|
+
const data = request.method === "POST" ? await parsePostData(request) : null;
|
|
86
|
+
|
|
84
87
|
// Ask the tree for the resource with those keys.
|
|
85
88
|
let resource;
|
|
86
89
|
try {
|
|
87
90
|
resource = await Tree.traverse(extendedTree, ...keys);
|
|
88
91
|
// If resource is a function, invoke to get the object we want to return.
|
|
92
|
+
// For a POST request, pass the data to the function.
|
|
89
93
|
if (typeof resource === "function") {
|
|
90
|
-
resource = await resource();
|
|
94
|
+
resource = data ? await resource(data) : await resource();
|
|
91
95
|
}
|
|
92
96
|
} catch (/** @type {any} */ error) {
|
|
93
97
|
respondWithError(response, error);
|
|
@@ -111,8 +115,16 @@ function keysFromUrl(url) {
|
|
|
111
115
|
const parts = url.pathname.split(/\/!/);
|
|
112
116
|
|
|
113
117
|
// Split everything before the first command by slashes and decode those.
|
|
114
|
-
|
|
118
|
+
let path = parts.shift();
|
|
119
|
+
if (parts.length > 0) {
|
|
120
|
+
// HACK: Add back trailing slash that was removed by split
|
|
121
|
+
path += "/";
|
|
122
|
+
}
|
|
115
123
|
const pathKeys = keysFromPath(path).map((key) => decodeURIComponent(key));
|
|
124
|
+
if (parts.length > 0 && pathKeys.at(-1) === "") {
|
|
125
|
+
// HACK part 2: Remove empty string that was added for trailing slash
|
|
126
|
+
pathKeys.pop();
|
|
127
|
+
}
|
|
116
128
|
|
|
117
129
|
// If there are no commands, and the path ends with a trailing slash, the
|
|
118
130
|
// final key will be an empty string. Change that to "index.html".
|