@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.
Files changed (87) hide show
  1. package/exports/exports.js +32 -34
  2. package/package.json +4 -4
  3. package/src/builtins/@arrows.js +1 -1
  4. package/src/builtins/@builtins.js +2 -5
  5. package/src/builtins/@cache.js +1 -1
  6. package/src/builtins/{@tree/concat.js → @concat.js} +3 -3
  7. package/src/builtins/@copy.js +2 -2
  8. package/src/builtins/{@tree/count.js → @count.js} +3 -3
  9. package/src/builtins/@crawl.js +1 -1
  10. package/src/builtins/@debug.js +1 -1
  11. package/src/builtins/{@tree/defineds.js → @defineds.js} +2 -2
  12. package/src/builtins/@document.js +1 -1
  13. package/src/builtins/{@tree/exceptions.js → @exceptions.js} +4 -4
  14. package/src/builtins/@explore.js +2 -5
  15. package/src/builtins/@files.js +1 -1
  16. package/src/builtins/@filter.js +1 -1
  17. package/src/builtins/{@tree/first.js → @first.js} +3 -3
  18. package/src/builtins/{@tree/fn.js → @fnTree.js} +7 -7
  19. package/src/builtins/@globs.js +1 -1
  20. package/src/builtins/{@tree/groupBy.js → @groupBy.js} +6 -6
  21. package/src/builtins/@help.js +1 -1
  22. package/src/builtins/@http.js +1 -1
  23. package/src/builtins/@https.js +1 -1
  24. package/src/builtins/@if.js +1 -1
  25. package/src/builtins/@image/format.js +36 -2
  26. package/src/builtins/@image/resize.js +30 -2
  27. package/src/builtins/@index.js +1 -1
  28. package/src/builtins/@inherited.js +1 -1
  29. package/src/builtins/@inline.js +1 -1
  30. package/src/builtins/{@tree/inners.js → @inners.js} +3 -3
  31. package/src/builtins/@invoke.js +1 -1
  32. package/src/builtins/{@tree/isAsyncTree.js → @isAsyncTree.js} +1 -1
  33. package/src/builtins/@json.js +1 -1
  34. package/src/builtins/{@tree/keys.js → @keys.js} +3 -3
  35. package/src/builtins/{@tree/keysJson.js → @keysJson.js} +3 -3
  36. package/src/builtins/@loaders/ori.js +18 -18
  37. package/src/builtins/@map.js +13 -1
  38. package/src/builtins/@match.js +1 -1
  39. package/src/builtins/{@tree/merge.js → @merge.js} +2 -2
  40. package/src/builtins/{@tree/mergeDeep.js → @mergeDeep.js} +3 -3
  41. package/src/builtins/@once.js +1 -1
  42. package/src/builtins/@ori.js +1 -1
  43. package/src/builtins/@pack.js +1 -1
  44. package/src/builtins/{@tree/parent.js → @parent.js} +3 -3
  45. package/src/builtins/{@tree/paths.js → @paths.js} +3 -3
  46. package/src/builtins/@perf.js +18 -0
  47. package/src/builtins/{@tree/plain.js → @plain.js} +3 -3
  48. package/src/builtins/@project.js +1 -1
  49. package/src/builtins/@redirect.js +8 -0
  50. package/src/builtins/{@tree/reverse.js → @reverse.js} +3 -3
  51. package/src/builtins/@rss.js +1 -1
  52. package/src/builtins/@scope/extend.js +6 -6
  53. package/src/builtins/@scope/get.js +1 -1
  54. package/src/builtins/@scope/set.js +4 -4
  55. package/src/builtins/@serve.js +1 -1
  56. package/src/builtins/{@tree/setDeep.js → @setDeep.js} +1 -1
  57. package/src/builtins/{@tree/shuffle.js → @shuffle.js} +5 -5
  58. package/src/builtins/{@tree/sitemap.js → @sitemap.js} +5 -5
  59. package/src/builtins/{@tree/sort.js → @sort.js} +4 -4
  60. package/src/builtins/{@tree/sortBy.js → @sortBy.js} +6 -6
  61. package/src/builtins/{@tree/static.js → @static.js} +5 -5
  62. package/src/builtins/@svg.js +2 -2
  63. package/src/builtins/{@tree/table.js → @table.js} +3 -3
  64. package/src/builtins/{@tree/take.js → @take.js} +3 -3
  65. package/src/builtins/{@tree/from.js → @tree.js} +3 -3
  66. package/src/builtins/@treeHttp.js +1 -1
  67. package/src/builtins/@treeHttps.js +1 -1
  68. package/src/builtins/@unpack.js +1 -1
  69. package/src/builtins/{@tree/values.js → @values.js} +3 -3
  70. package/src/builtins/{@tree/valuesDeep.js → @valuesDeep.js} +3 -3
  71. package/src/builtins/@watch.js +1 -1
  72. package/src/builtins/@with.js +1 -1
  73. package/src/builtins/@yaml.js +1 -1
  74. package/src/builtins/{@tree/map.d.ts → map.d.ts} +1 -1
  75. package/src/cli/cli.js +2 -15
  76. package/src/common/ExplorableSiteTransform.js +6 -4
  77. package/src/misc/assertScopeIsDefined.js +2 -2
  78. package/src/misc/getTreeArgument.js +16 -6
  79. package/src/{builtins/@tree/dot.js → misc/treeDot.js} +3 -7
  80. package/src/server/constructResponse.js +126 -0
  81. package/src/server/mediaTypes.js +0 -16
  82. package/src/server/server.js +62 -131
  83. package/src/builtins/@tree/flowSvg.js +0 -55
  84. package/src/builtins/@tree/fromJson.js +0 -6
  85. package/src/builtins/@tree/fromYaml.js +0 -24
  86. package/src/builtins/@tree/nextKey.js +0 -29
  87. package/src/builtins/@tree/previousKey.js +0 -29
@@ -1,5 +1,5 @@
1
1
  import { Scope } from "@weborigami/language";
2
- import getTreeArgument from "../../misc/getTreeArgument.js";
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 = `take tree, n\tReturn the first n items from tree`;
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 "../../misc/getTreeArgument.js";
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 = `from <treelike>\tConvert JSON, YAML, function, or plain object to a tree`;
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
 
@@ -8,6 +8,6 @@ import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
8
8
  * @returns
9
9
  */
10
10
  export default function unpack(obj) {
11
- assertScopeIsDefined(this);
11
+ assertScopeIsDefined(this, "unpack");
12
12
  return obj?.unpack?.();
13
13
  }
@@ -1,5 +1,5 @@
1
1
  import { Tree } from "@weborigami/async-tree";
2
- import getTreeArgument from "../../misc/getTreeArgument.js";
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 = `values <tree>\tThe top-level values in the tree`;
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 "../../misc/getTreeArgument.js";
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 = `valuesDeep <tree>\tThe in-order tree values as a flat array`;
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";
@@ -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?.();
@@ -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);
@@ -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 "../../../index.ts";
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
- // Work up to the root cause, displaying intermediate messages as we go up.
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 (key === "index.html") {
41
- value = await index.call(scope, this);
42
- } else if (key === ".keys.json") {
43
- value = await keysJson.stringify(this);
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
- "Tree methods must be called a scope. If you don't want to pass a scope, invoke with: <methodName>.call(null)"
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(scope, args, treelike) {
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
- return Tree.from(treelike);
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
- "An Origami tree function was called with no tree argument and no scope."
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 "../../common/serialize.js";
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 = await getTreeArgument(this, arguments, treelike);
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
+ }
@@ -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
- };
@@ -1,16 +1,41 @@
1
- import {
2
- ObjectTree,
3
- Tree,
4
- isPlainObject,
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
- const TypedArray = Object.getPrototypeOf(Uint8Array);
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
- // Asynchronous tree router as Express middleware.
42
- export function treeRouter(tree) {
43
- // Return a router for the tree source.
44
- return async function (request, response, next) {
45
- const handled = await handleRequest(request, response, tree);
46
- if (!handled) {
47
- // Module not found, let next middleware function try.
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
- let mediaType;
77
-
78
- if (!resource) {
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, "&lt;").replace(/>/g, "&gt;");
226
159
  const html = `<!DOCTYPE html>
@@ -241,16 +174,14 @@ ${message}
241
174
  console.error(message);
242
175
  }
243
176
 
244
- /**
245
- * Convert to a string if we can, but leave objects that convert to something
246
- * like "[object Object]" alone.
247
- *
248
- * @param {any} object
249
- */
250
- function textOrObject(object) {
251
- // Return buffers and typed arrays as is.
252
- if (object instanceof ArrayBuffer || object instanceof TypedArray) {
253
- return object;
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
  }