@weborigami/origami 0.0.41 → 0.0.42
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/exports.js +32 -34
- package/package.json +4 -4
- package/src/builtins/@arrows.js +1 -1
- package/src/builtins/@builtins.js +2 -5
- package/src/builtins/@cache.js +1 -1
- package/src/builtins/{@tree/concat.js → @concat.js} +3 -3
- package/src/builtins/@copy.js +2 -2
- package/src/builtins/{@tree/count.js → @count.js} +3 -3
- package/src/builtins/@crawl.js +1 -1
- package/src/builtins/@debug.js +1 -1
- package/src/builtins/{@tree/defineds.js → @defineds.js} +2 -2
- package/src/builtins/@document.js +1 -1
- package/src/builtins/{@tree/exceptions.js → @exceptions.js} +4 -4
- package/src/builtins/@explore.js +2 -5
- package/src/builtins/@files.js +1 -1
- package/src/builtins/@filter.js +1 -1
- package/src/builtins/{@tree/first.js → @first.js} +3 -3
- package/src/builtins/{@tree/fn.js → @fnTree.js} +7 -7
- package/src/builtins/@globs.js +1 -1
- package/src/builtins/{@tree/groupBy.js → @groupBy.js} +6 -6
- package/src/builtins/@help.js +1 -1
- package/src/builtins/@http.js +1 -1
- package/src/builtins/@https.js +1 -1
- package/src/builtins/@if.js +1 -1
- package/src/builtins/@image/format.js +36 -2
- package/src/builtins/@image/resize.js +30 -2
- package/src/builtins/@index.js +1 -1
- package/src/builtins/@inherited.js +1 -1
- package/src/builtins/@inline.js +1 -1
- package/src/builtins/{@tree/inners.js → @inners.js} +3 -3
- package/src/builtins/@invoke.js +1 -1
- package/src/builtins/{@tree/isAsyncTree.js → @isAsyncTree.js} +1 -1
- package/src/builtins/@json.js +1 -1
- package/src/builtins/{@tree/keys.js → @keys.js} +3 -3
- package/src/builtins/{@tree/keysJson.js → @keysJson.js} +3 -3
- package/src/builtins/@loaders/ori.js +18 -18
- package/src/builtins/@map.js +13 -1
- package/src/builtins/@match.js +1 -1
- package/src/builtins/{@tree/merge.js → @merge.js} +2 -2
- package/src/builtins/{@tree/mergeDeep.js → @mergeDeep.js} +3 -3
- package/src/builtins/@once.js +1 -1
- package/src/builtins/@ori.js +1 -1
- package/src/builtins/@pack.js +1 -1
- package/src/builtins/{@tree/parent.js → @parent.js} +3 -3
- package/src/builtins/{@tree/paths.js → @paths.js} +3 -3
- package/src/builtins/@perf.js +18 -0
- package/src/builtins/{@tree/plain.js → @plain.js} +3 -3
- package/src/builtins/@project.js +1 -1
- package/src/builtins/@redirect.js +8 -0
- package/src/builtins/{@tree/reverse.js → @reverse.js} +3 -3
- package/src/builtins/@rss.js +1 -1
- package/src/builtins/@scope/extend.js +6 -6
- package/src/builtins/@scope/get.js +1 -1
- package/src/builtins/@scope/set.js +4 -4
- package/src/builtins/@serve.js +1 -1
- package/src/builtins/{@tree/setDeep.js → @setDeep.js} +1 -1
- package/src/builtins/{@tree/shuffle.js → @shuffle.js} +5 -5
- package/src/builtins/{@tree/sitemap.js → @sitemap.js} +5 -5
- package/src/builtins/{@tree/sort.js → @sort.js} +4 -4
- package/src/builtins/{@tree/sortBy.js → @sortBy.js} +6 -6
- package/src/builtins/{@tree/static.js → @static.js} +5 -5
- package/src/builtins/@svg.js +2 -2
- package/src/builtins/{@tree/table.js → @table.js} +3 -3
- package/src/builtins/{@tree/take.js → @take.js} +3 -3
- package/src/builtins/{@tree/from.js → @tree.js} +3 -3
- package/src/builtins/@treeHttp.js +1 -1
- package/src/builtins/@treeHttps.js +1 -1
- package/src/builtins/@unpack.js +1 -1
- package/src/builtins/{@tree/values.js → @values.js} +3 -3
- package/src/builtins/{@tree/valuesDeep.js → @valuesDeep.js} +3 -3
- package/src/builtins/@watch.js +1 -1
- package/src/builtins/@with.js +1 -1
- package/src/builtins/@yaml.js +1 -1
- package/src/builtins/{@tree/map.d.ts → map.d.ts} +1 -1
- package/src/cli/cli.js +2 -15
- package/src/common/ExplorableSiteTransform.js +6 -4
- package/src/misc/assertScopeIsDefined.js +2 -2
- package/src/misc/getTreeArgument.js +16 -6
- package/src/{builtins/@tree/dot.js → misc/treeDot.js} +3 -7
- package/src/server/constructResponse.js +126 -0
- package/src/server/mediaTypes.js +0 -16
- package/src/server/server.js +62 -131
- package/src/builtins/@tree/flowSvg.js +0 -55
- package/src/builtins/@tree/fromJson.js +0 -6
- package/src/builtins/@tree/fromYaml.js +0 -24
- package/src/builtins/@tree/nextKey.js +0 -29
- package/src/builtins/@tree/previousKey.js +0 -29
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Scope } from "@weborigami/language";
|
|
2
|
-
import getTreeArgument from "
|
|
2
|
+
import getTreeArgument from "../misc/getTreeArgument.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Given a tree, take the first n items from it.
|
|
@@ -11,7 +11,7 @@ import getTreeArgument from "../../misc/getTreeArgument.js";
|
|
|
11
11
|
* @param {number} n
|
|
12
12
|
*/
|
|
13
13
|
export default async function take(treelike, n) {
|
|
14
|
-
const tree = await getTreeArgument(this, arguments, treelike);
|
|
14
|
+
const tree = await getTreeArgument(this, arguments, treelike, "@take");
|
|
15
15
|
|
|
16
16
|
/** @type {AsyncTree} */
|
|
17
17
|
let takeTree = {
|
|
@@ -30,5 +30,5 @@ export default async function take(treelike, n) {
|
|
|
30
30
|
return takeTree;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
take.usage =
|
|
33
|
+
take.usage = `@take tree, n\tReturn the first n items from tree`;
|
|
34
34
|
take.documentation = "https://weborigami.org/cli/builtins.html#take";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Scope } from "@weborigami/language";
|
|
2
|
-
import getTreeArgument from "
|
|
2
|
+
import getTreeArgument from "../misc/getTreeArgument.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Cast the indicated treelike to a tree.
|
|
@@ -10,10 +10,10 @@ import getTreeArgument from "../../misc/getTreeArgument.js";
|
|
|
10
10
|
* @param {Treelike} [treelike]
|
|
11
11
|
*/
|
|
12
12
|
export default async function tree(treelike) {
|
|
13
|
-
let tree = await getTreeArgument(this, arguments, treelike);
|
|
13
|
+
let tree = await getTreeArgument(this, arguments, treelike, "@tree");
|
|
14
14
|
tree = Scope.treeWithScope(tree, this);
|
|
15
15
|
return tree;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
tree.usage =
|
|
18
|
+
tree.usage = `@tree <treelike>\tConvert JSON, YAML, function, or plain object to a tree`;
|
|
19
19
|
tree.documentation = "https://weborigami.org/cli/builtins.html#tree";
|
|
@@ -11,7 +11,7 @@ import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
|
|
|
11
11
|
* @param {...string|Symbol} keys
|
|
12
12
|
*/
|
|
13
13
|
export default function treeHttp(host, ...keys) {
|
|
14
|
-
assertScopeIsDefined(this);
|
|
14
|
+
assertScopeIsDefined(this, "treeHttp");
|
|
15
15
|
return ops.treeHttp.call(this, host, ...keys);
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -11,7 +11,7 @@ import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
|
|
|
11
11
|
* @param {...string|Symbol} keys
|
|
12
12
|
*/
|
|
13
13
|
export default function treeHttps(host, ...keys) {
|
|
14
|
-
assertScopeIsDefined(this);
|
|
14
|
+
assertScopeIsDefined(this, "treeHttps");
|
|
15
15
|
return ops.treeHttps.call(this, host, ...keys);
|
|
16
16
|
}
|
|
17
17
|
|
package/src/builtins/@unpack.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Tree } from "@weborigami/async-tree";
|
|
2
|
-
import getTreeArgument from "
|
|
2
|
+
import getTreeArgument from "../misc/getTreeArgument.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Return the interior nodes of the tree.
|
|
@@ -10,9 +10,9 @@ import getTreeArgument from "../../misc/getTreeArgument.js";
|
|
|
10
10
|
* @param {Treelike} [treelike]
|
|
11
11
|
*/
|
|
12
12
|
export default async function values(treelike) {
|
|
13
|
-
const tree = await getTreeArgument(this, arguments, treelike);
|
|
13
|
+
const tree = await getTreeArgument(this, arguments, treelike, "@values");
|
|
14
14
|
return Tree.values(tree);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
values.usage =
|
|
17
|
+
values.usage = `@values <tree>\tThe top-level values in the tree`;
|
|
18
18
|
values.documentation = "https://weborigami.org/cli/builtins.html#values";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Tree } from "@weborigami/async-tree";
|
|
2
|
-
import getTreeArgument from "
|
|
2
|
+
import getTreeArgument from "../misc/getTreeArgument.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Return the in-order exterior values of a tree as a flat array.
|
|
@@ -10,10 +10,10 @@ import getTreeArgument from "../../misc/getTreeArgument.js";
|
|
|
10
10
|
* @param {Treelike} [treelike]
|
|
11
11
|
*/
|
|
12
12
|
export default async function valuesDeep(treelike) {
|
|
13
|
-
const tree = await getTreeArgument(this, arguments, treelike);
|
|
13
|
+
const tree = await getTreeArgument(this, arguments, treelike, "@valuesDeep");
|
|
14
14
|
return Tree.mapReduce(tree, null, async (values) => values.flat());
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
valuesDeep.usage =
|
|
17
|
+
valuesDeep.usage = `@valuesDeep <tree>\tThe in-order tree values as a flat array`;
|
|
18
18
|
valuesDeep.documentation =
|
|
19
19
|
"https://weborigami.org/cli/builtins.html#valuesDeep";
|
package/src/builtins/@watch.js
CHANGED
|
@@ -16,7 +16,7 @@ import getTreeArgument from "../misc/getTreeArgument.js";
|
|
|
16
16
|
*/
|
|
17
17
|
export default async function watch(treelike, fn) {
|
|
18
18
|
/** @type {any} */
|
|
19
|
-
const container = await getTreeArgument(this, arguments, treelike);
|
|
19
|
+
const container = await getTreeArgument(this, arguments, treelike, "@watch");
|
|
20
20
|
|
|
21
21
|
// Watch the indicated tree.
|
|
22
22
|
await /** @type {any} */ (container).watch?.();
|
package/src/builtins/@with.js
CHANGED
|
@@ -14,7 +14,7 @@ import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
|
|
|
14
14
|
* @param {Invocable} invocable
|
|
15
15
|
*/
|
|
16
16
|
export default function withTree(treelike, invocable) {
|
|
17
|
-
assertScopeIsDefined(this);
|
|
17
|
+
assertScopeIsDefined(this, "with");
|
|
18
18
|
const tree = Tree.from(treelike);
|
|
19
19
|
const fn = toFunction(invocable);
|
|
20
20
|
const scope = new Scope(tree, this);
|
package/src/builtins/@yaml.js
CHANGED
|
@@ -10,7 +10,7 @@ import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
|
|
|
10
10
|
* @param {any} [obj]
|
|
11
11
|
*/
|
|
12
12
|
export default async function toYaml(obj) {
|
|
13
|
-
assertScopeIsDefined(this);
|
|
13
|
+
assertScopeIsDefined(this, "yaml");
|
|
14
14
|
// A fragment of the logic from getTreeArgument.js
|
|
15
15
|
if (arguments.length > 0 && obj === undefined) {
|
|
16
16
|
throw new Error(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { KeyFn, Treelike, ValueKeyFn } from "@weborigami/async-tree";
|
|
2
2
|
import { AsyncTree } from "@weborigami/types";
|
|
3
|
-
import { TreelikeTransform } from "
|
|
3
|
+
import { TreelikeTransform } from "../../index.ts";
|
|
4
4
|
|
|
5
5
|
type TreeMapOptions = {
|
|
6
6
|
deep?: boolean;
|
package/src/cli/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { ObjectTree, Tree } from "@weborigami/async-tree";
|
|
4
|
-
import { Scope } from "@weborigami/language";
|
|
4
|
+
import { Scope, formatError } from "@weborigami/language";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import process, { stdout } from "node:process";
|
|
7
7
|
import ori from "../builtins/@ori.js";
|
|
@@ -75,19 +75,6 @@ while (args[0] === "") {
|
|
|
75
75
|
try {
|
|
76
76
|
await main(...args);
|
|
77
77
|
} catch (/** @type {any} */ error) {
|
|
78
|
-
|
|
79
|
-
if (!error.cause && !error.stack) {
|
|
80
|
-
console.error(error.message);
|
|
81
|
-
} else {
|
|
82
|
-
while (error.cause) {
|
|
83
|
-
console.error(error.message);
|
|
84
|
-
error = error.cause;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (error.stack) {
|
|
88
|
-
// Display stack trace for root cause, under the theory that that's the most
|
|
89
|
-
// useful place to look for the problem.
|
|
90
|
-
console.error(error.stack);
|
|
91
|
-
}
|
|
78
|
+
console.error(formatError(error));
|
|
92
79
|
process.exitCode = 1;
|
|
93
80
|
}
|
|
@@ -37,10 +37,12 @@ export default function ExplorableSiteTransform(Base) {
|
|
|
37
37
|
if (value === undefined) {
|
|
38
38
|
// The tree doesn't have the key; try the defaults.
|
|
39
39
|
const scope = Scope.getScope(this);
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
if (scope) {
|
|
41
|
+
if (key === "index.html") {
|
|
42
|
+
value = await index.call(scope, this);
|
|
43
|
+
} else if (key === ".keys.json") {
|
|
44
|
+
value = await keysJson.stringify(this);
|
|
45
|
+
}
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export default function assertScopeIsDefined(scope) {
|
|
1
|
+
export default function assertScopeIsDefined(scope, methodName) {
|
|
2
2
|
if (scope === undefined) {
|
|
3
3
|
throw new Error(
|
|
4
|
-
|
|
4
|
+
`${methodName} must be called with a scope. If you don't want to pass a scope, invoke with: ${methodName}.call(null)`
|
|
5
5
|
);
|
|
6
6
|
}
|
|
7
7
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Tree } from "@weborigami/async-tree";
|
|
2
|
+
import { isTreelike } from "@weborigami/async-tree/src/Tree.js";
|
|
2
3
|
import assertScopeIsDefined from "./assertScopeIsDefined.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -16,26 +17,35 @@ import assertScopeIsDefined from "./assertScopeIsDefined.js";
|
|
|
16
17
|
* @param {AsyncTree|null} scope
|
|
17
18
|
* @param {IArguments} args
|
|
18
19
|
* @param {Treelike|undefined} treelike
|
|
20
|
+
* @param {string} methodName
|
|
19
21
|
* @returns {Promise<AsyncTree>}
|
|
20
22
|
*/
|
|
21
|
-
export default async function getTreeArgument(
|
|
23
|
+
export default async function getTreeArgument(
|
|
24
|
+
scope,
|
|
25
|
+
args,
|
|
26
|
+
treelike,
|
|
27
|
+
methodName
|
|
28
|
+
) {
|
|
22
29
|
assertScopeIsDefined(scope);
|
|
23
30
|
|
|
24
31
|
if (treelike !== undefined) {
|
|
25
|
-
|
|
32
|
+
if (isTreelike(treelike)) {
|
|
33
|
+
return Tree.from(treelike);
|
|
34
|
+
}
|
|
35
|
+
throw new Error(
|
|
36
|
+
`${methodName}: The first argument must be a tree, like an array, object, or files.`
|
|
37
|
+
);
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
if (args.length === 0) {
|
|
29
41
|
if (!scope) {
|
|
30
42
|
// Should never happen because assertScopeIsDefined throws an exception.
|
|
31
43
|
throw new Error(
|
|
32
|
-
|
|
44
|
+
`${methodName} was called with no tree argument and no scope.`
|
|
33
45
|
);
|
|
34
46
|
}
|
|
35
47
|
return scope.get("@current");
|
|
36
48
|
}
|
|
37
49
|
|
|
38
|
-
throw new Error(
|
|
39
|
-
"An Origami tree function was called with an initial argument, but its value is undefined."
|
|
40
|
-
);
|
|
50
|
+
throw new Error(`${methodName}: The first argument was undefined.`);
|
|
41
51
|
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { Tree, isPlainObject, isStringLike } from "@weborigami/async-tree";
|
|
2
2
|
import { extname } from "@weborigami/language";
|
|
3
|
-
import * as serialize from "
|
|
4
|
-
import {
|
|
5
|
-
hasNonPrintableCharacters,
|
|
6
|
-
keySymbol,
|
|
7
|
-
} from "../../common/utilities.js";
|
|
8
|
-
import getTreeArgument from "../../misc/getTreeArgument.js";
|
|
3
|
+
import * as serialize from "../common/serialize.js";
|
|
4
|
+
import { hasNonPrintableCharacters, keySymbol } from "../common/utilities.js";
|
|
9
5
|
|
|
10
6
|
/**
|
|
11
7
|
* Render a tree in DOT format.
|
|
@@ -19,7 +15,7 @@ import getTreeArgument from "../../misc/getTreeArgument.js";
|
|
|
19
15
|
* @param {PlainObject} [options]
|
|
20
16
|
*/
|
|
21
17
|
export default async function dot(treelike, options = {}) {
|
|
22
|
-
const tree =
|
|
18
|
+
const tree = Tree.from(treelike);
|
|
23
19
|
const rootLabel = tree[keySymbol] ?? "";
|
|
24
20
|
const treeArcs = await statements(tree, "", rootLabel, options);
|
|
25
21
|
return `digraph g {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SiteTree,
|
|
3
|
+
Tree,
|
|
4
|
+
isPlainObject,
|
|
5
|
+
isStringLike,
|
|
6
|
+
} from "@weborigami/async-tree";
|
|
7
|
+
import { extname } from "@weborigami/language";
|
|
8
|
+
import * as serialize from "../common/serialize.js";
|
|
9
|
+
import { toString } from "../common/utilities.js";
|
|
10
|
+
import { mediaTypeForExtension } from "./mediaTypes.js";
|
|
11
|
+
|
|
12
|
+
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Given a resource that was returned from a route, construct an appropriate
|
|
16
|
+
* HTTP Response indicating what should be sent to the client. Return null
|
|
17
|
+
* if the resource is not a valid response.
|
|
18
|
+
*
|
|
19
|
+
* @param {import("node:http").IncomingMessage} request
|
|
20
|
+
* @param {any} resource
|
|
21
|
+
* @returns {Promise<Response|null>}
|
|
22
|
+
*/
|
|
23
|
+
export default async function constructResponse(request, resource) {
|
|
24
|
+
if (resource instanceof Response) {
|
|
25
|
+
// Already a Response, return as is.
|
|
26
|
+
return resource;
|
|
27
|
+
} else if (!resource) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Determine media type, what data we'll send, and encoding.
|
|
32
|
+
const url = new URL(request.url ?? "", `https://${request.headers.host}`);
|
|
33
|
+
const extension = extname(url.pathname).toLowerCase();
|
|
34
|
+
let mediaType = extension ? mediaTypeForExtension[extension] : undefined;
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
mediaType === undefined &&
|
|
38
|
+
!url.pathname.endsWith("/") &&
|
|
39
|
+
(Tree.isAsyncTree(resource) ||
|
|
40
|
+
isPlainObject(resource) ||
|
|
41
|
+
resource instanceof Array)
|
|
42
|
+
) {
|
|
43
|
+
// Redirect to an index page for the result.
|
|
44
|
+
const Location = `${request.url}/`;
|
|
45
|
+
return new Response("ok", {
|
|
46
|
+
headers: {
|
|
47
|
+
Location,
|
|
48
|
+
},
|
|
49
|
+
status: 307,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If the request is for a JSON or YAML result, and the resource we got
|
|
54
|
+
// isn't yet a string or Buffer, convert the resource to JSON or YAML now.
|
|
55
|
+
if (
|
|
56
|
+
(mediaType === "application/json" || mediaType === "text/yaml") &&
|
|
57
|
+
!isStringLike(resource)
|
|
58
|
+
) {
|
|
59
|
+
const tree = Tree.from(resource);
|
|
60
|
+
resource =
|
|
61
|
+
mediaType === "text/yaml"
|
|
62
|
+
? await serialize.toYaml(tree)
|
|
63
|
+
: await serialize.toJson(tree);
|
|
64
|
+
} else if (
|
|
65
|
+
mediaType === undefined &&
|
|
66
|
+
(isPlainObject(resource) || resource instanceof Array)
|
|
67
|
+
) {
|
|
68
|
+
// The resource is data, try showing it as YAML.
|
|
69
|
+
const tree = Tree.from(resource);
|
|
70
|
+
resource = await serialize.toYaml(tree);
|
|
71
|
+
mediaType = "text/yaml";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let body;
|
|
75
|
+
if (mediaType) {
|
|
76
|
+
body = SiteTree.mediaTypeIsText(mediaType) ? toString(resource) : resource;
|
|
77
|
+
} else {
|
|
78
|
+
body = textOrObject(resource);
|
|
79
|
+
// Infer media type.
|
|
80
|
+
mediaType =
|
|
81
|
+
typeof body !== "string"
|
|
82
|
+
? "application/octet-stream"
|
|
83
|
+
: body.trimStart().startsWith("<")
|
|
84
|
+
? "text/html"
|
|
85
|
+
: "text/plain";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Assume text is encoded in UTF-8.
|
|
89
|
+
if (SiteTree.mediaTypeIsText(mediaType)) {
|
|
90
|
+
mediaType += "; charset=utf-8";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// If we didn't get back some kind of data that response.write() accepts,
|
|
94
|
+
// assume it was an error.
|
|
95
|
+
const validResponse = typeof body === "string" || body instanceof TypedArray;
|
|
96
|
+
if (!validResponse) {
|
|
97
|
+
const typeName = body?.constructor?.name ?? typeof body;
|
|
98
|
+
console.error(
|
|
99
|
+
`A served tree must return a string or a TypedArray (such as a Buffer) but returned an instance of ${typeName}.`
|
|
100
|
+
);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return new Response(body, {
|
|
105
|
+
headers: {
|
|
106
|
+
"Content-Type": mediaType,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Convert to a string if we can, but leave objects that convert to something
|
|
113
|
+
* like "[object Object]" alone.
|
|
114
|
+
*
|
|
115
|
+
* @param {any} object
|
|
116
|
+
*/
|
|
117
|
+
function textOrObject(object) {
|
|
118
|
+
if (object instanceof ArrayBuffer) {
|
|
119
|
+
// Convert to Buffer.
|
|
120
|
+
return Buffer.from(object);
|
|
121
|
+
} else if (object instanceof TypedArray) {
|
|
122
|
+
// Return typed arrays as is.
|
|
123
|
+
return object;
|
|
124
|
+
}
|
|
125
|
+
return toString(object);
|
|
126
|
+
}
|
package/src/server/mediaTypes.js
CHANGED
|
@@ -79,19 +79,3 @@ export const mediaTypeForExtension = {
|
|
|
79
79
|
".yaml": "text/yaml", // Not official
|
|
80
80
|
".zip": "application/zip",
|
|
81
81
|
};
|
|
82
|
-
|
|
83
|
-
export const mediaTypeIsText = {
|
|
84
|
-
"application/json": true,
|
|
85
|
-
"application/ld+json": true,
|
|
86
|
-
"application/vnd.oasis.opendocument.text": true,
|
|
87
|
-
"application/x-httpd-php": true,
|
|
88
|
-
"application/x-sh": true,
|
|
89
|
-
"application/xhtml+xml": true,
|
|
90
|
-
"text/css": true,
|
|
91
|
-
"text/csv": true,
|
|
92
|
-
"text/html": true,
|
|
93
|
-
"text/javascript": true,
|
|
94
|
-
"text/plain": true,
|
|
95
|
-
"text/yaml": true,
|
|
96
|
-
"text/yml": true,
|
|
97
|
-
};
|
package/src/server/server.js
CHANGED
|
@@ -1,16 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
isStringLike,
|
|
6
|
-
keysFromPath,
|
|
7
|
-
} from "@weborigami/async-tree";
|
|
8
|
-
import { Scope, extname } from "@weborigami/language";
|
|
9
|
-
import * as serialize from "../common/serialize.js";
|
|
10
|
-
import { toString } from "../common/utilities.js";
|
|
11
|
-
import { mediaTypeForExtension, mediaTypeIsText } from "./mediaTypes.js";
|
|
1
|
+
import { ObjectTree, Tree, keysFromPath } from "@weborigami/async-tree";
|
|
2
|
+
import { Scope, formatError } from "@weborigami/language";
|
|
3
|
+
import { ServerResponse } from "node:http";
|
|
4
|
+
import constructResponse from "./constructResponse.js";
|
|
12
5
|
|
|
13
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Copy a constructed response to a ServerResponse. Return true if the response
|
|
8
|
+
* was successfully copied, and false if there was a problem.
|
|
9
|
+
*
|
|
10
|
+
* @param {Response} constructed
|
|
11
|
+
* @param {ServerResponse} response
|
|
12
|
+
*/
|
|
13
|
+
async function copyResponse(constructed, response) {
|
|
14
|
+
response.statusCode = constructed.status;
|
|
15
|
+
response.statusMessage = constructed.statusText;
|
|
16
|
+
|
|
17
|
+
for (const [key, value] of constructed.headers) {
|
|
18
|
+
response.setHeader(key, value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (constructed.body) {
|
|
22
|
+
try {
|
|
23
|
+
// Write the response body to the ServerResponse.
|
|
24
|
+
const reader = constructed.body.getReader();
|
|
25
|
+
let { done, value } = await reader.read();
|
|
26
|
+
while (!done) {
|
|
27
|
+
response.write(value);
|
|
28
|
+
({ done, value } = await reader.read());
|
|
29
|
+
}
|
|
30
|
+
response.end();
|
|
31
|
+
} catch (/** @type {any} */ error) {
|
|
32
|
+
console.error(error.message);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
14
39
|
|
|
15
40
|
// Extend the tree's scope with the URL's search parameters.
|
|
16
41
|
function extendTreeScopeWithParams(tree, url) {
|
|
@@ -38,21 +63,16 @@ function extendTreeScopeWithParams(tree, url) {
|
|
|
38
63
|
return extendedTree;
|
|
39
64
|
}
|
|
40
65
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
next();
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Handle a client request.
|
|
68
|
+
*
|
|
69
|
+
* @param {import("node:http").IncomingMessage} request
|
|
70
|
+
* @param {ServerResponse} response
|
|
71
|
+
* @param {import("@weborigami/types").AsyncTree} tree
|
|
72
|
+
*/
|
|
53
73
|
export async function handleRequest(request, response, tree) {
|
|
54
74
|
// For parsing purposes, we assume HTTPS -- it doesn't affect parsing.
|
|
55
|
-
const url = new URL(request.url, `https://${request.headers.host}`);
|
|
75
|
+
const url = new URL(request.url ?? "", `https://${request.headers.host}`);
|
|
56
76
|
const keys = keysFromUrl(url);
|
|
57
77
|
|
|
58
78
|
const extendedTree =
|
|
@@ -73,93 +93,15 @@ export async function handleRequest(request, response, tree) {
|
|
|
73
93
|
return true;
|
|
74
94
|
}
|
|
75
95
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Determine media type, what data we'll send, and encoding.
|
|
83
|
-
const extension = extname(url.pathname).toLowerCase();
|
|
84
|
-
mediaType = extension ? mediaTypeForExtension[extension] : undefined;
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
mediaType === undefined &&
|
|
88
|
-
!request.url.endsWith("/") &&
|
|
89
|
-
(Tree.isAsyncTree(resource) ||
|
|
90
|
-
isPlainObject(resource) ||
|
|
91
|
-
resource instanceof Array)
|
|
92
|
-
) {
|
|
93
|
-
// Redirect to an index page for the result.
|
|
94
|
-
// Redirect to the root of the tree.
|
|
95
|
-
const Location = `${request.url}/`;
|
|
96
|
-
response.writeHead(307, { Location });
|
|
97
|
-
response.end("ok");
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// If the request is for a JSON or YAML result, and the resource we got
|
|
102
|
-
// isn't yet a string or Buffer, convert the resource to JSON or YAML now.
|
|
103
|
-
if (
|
|
104
|
-
(mediaType === "application/json" || mediaType === "text/yaml") &&
|
|
105
|
-
!isStringLike(resource)
|
|
106
|
-
) {
|
|
107
|
-
const tree = Tree.from(resource);
|
|
108
|
-
resource =
|
|
109
|
-
mediaType === "text/yaml"
|
|
110
|
-
? await serialize.toYaml(tree)
|
|
111
|
-
: await serialize.toJson(tree);
|
|
112
|
-
} else if (
|
|
113
|
-
mediaType === undefined &&
|
|
114
|
-
(isPlainObject(resource) || resource instanceof Array)
|
|
115
|
-
) {
|
|
116
|
-
// The resource is data, try showing it as YAML.
|
|
117
|
-
const tree = Tree.from(resource);
|
|
118
|
-
resource = await serialize.toYaml(tree);
|
|
119
|
-
mediaType = "text/yaml";
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
let data;
|
|
123
|
-
if (mediaType) {
|
|
124
|
-
data = mediaTypeIsText[mediaType] ? toString(resource) : resource;
|
|
125
|
-
} else {
|
|
126
|
-
data = textOrObject(resource);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (!mediaType) {
|
|
130
|
-
// Can't identify media type; infer default type.
|
|
131
|
-
mediaType =
|
|
132
|
-
typeof data !== "string"
|
|
133
|
-
? "application/octet-stream"
|
|
134
|
-
: data.trimStart().startsWith("<")
|
|
135
|
-
? "text/html"
|
|
136
|
-
: "text/plain";
|
|
137
|
-
}
|
|
138
|
-
const encoding = mediaTypeIsText[mediaType] ? "utf-8" : undefined;
|
|
139
|
-
|
|
140
|
-
// If we didn't get back some kind of data that response.write() accepts,
|
|
141
|
-
// assume it was an error.
|
|
142
|
-
const validResponse = typeof data === "string" || data instanceof TypedArray;
|
|
143
|
-
|
|
144
|
-
if (!validResponse) {
|
|
145
|
-
const typeName = data?.constructor?.name ?? typeof data;
|
|
146
|
-
console.error(
|
|
147
|
-
`A served tree must return a string or a TypedArray (such as a Buffer) but returned an instance of ${typeName}.`
|
|
148
|
-
);
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
response.writeHead(200, {
|
|
153
|
-
"Content-Type": mediaType,
|
|
154
|
-
});
|
|
155
|
-
try {
|
|
156
|
-
response.end(data, encoding);
|
|
157
|
-
} catch (/** @type {any} */ error) {
|
|
158
|
-
console.error(error.message);
|
|
96
|
+
// Construct the response.
|
|
97
|
+
const constructed = await constructResponse(request, resource);
|
|
98
|
+
if (!constructed) {
|
|
159
99
|
return false;
|
|
160
100
|
}
|
|
161
101
|
|
|
162
|
-
return true
|
|
102
|
+
// Copy the construct response to the ServerResponse and return true if
|
|
103
|
+
// the response was valid.
|
|
104
|
+
return copyResponse(constructed, response);
|
|
163
105
|
}
|
|
164
106
|
|
|
165
107
|
function keysFromUrl(url) {
|
|
@@ -211,16 +153,7 @@ export function requestListener(treelike) {
|
|
|
211
153
|
* the console.
|
|
212
154
|
*/
|
|
213
155
|
function respondWithError(response, error) {
|
|
214
|
-
let message =
|
|
215
|
-
// Work up to the root cause, displaying intermediate messages as we go up.
|
|
216
|
-
while (error.cause) {
|
|
217
|
-
message += error.message + `\n`;
|
|
218
|
-
error = error.cause;
|
|
219
|
-
}
|
|
220
|
-
if (error.name) {
|
|
221
|
-
message += `${error.name}: `;
|
|
222
|
-
}
|
|
223
|
-
message += error.message;
|
|
156
|
+
let message = formatError(error);
|
|
224
157
|
// Prevent HTML in the error message from being interpreted as HTML.
|
|
225
158
|
message = message.replace(/</g, "<").replace(/>/g, ">");
|
|
226
159
|
const html = `<!DOCTYPE html>
|
|
@@ -241,16 +174,14 @@ ${message}
|
|
|
241
174
|
console.error(message);
|
|
242
175
|
}
|
|
243
176
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
function
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
return toString(object);
|
|
177
|
+
// Asynchronous tree router as Express middleware.
|
|
178
|
+
export function treeRouter(tree) {
|
|
179
|
+
// Return a router for the tree source.
|
|
180
|
+
return async function (request, response, next) {
|
|
181
|
+
const handled = await handleRequest(request, response, tree);
|
|
182
|
+
if (!handled) {
|
|
183
|
+
// Module not found, let next middleware function try.
|
|
184
|
+
next();
|
|
185
|
+
}
|
|
186
|
+
};
|
|
256
187
|
}
|