@weborigami/origami 0.6.16 → 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/hashBytes.js +26 -0
- 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 -0
- package/src/dev/explore.js +1 -0
- package/src/dev/help.yaml +29 -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/hash.js +17 -0
- package/src/origami/htmlDom.js +21 -0
- package/src/origami/htmlParse.js +6 -13
- package/src/origami/mdHtml.js +1 -0
- package/src/origami/once.js +4 -1
- package/src/origami/ori.js +10 -11
- package/src/origami/origami.js +7 -1
- package/src/origami/randomFrom.js +15 -0
- package/src/origami/randomsFrom.js +65 -0
- 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/origami/project.js +0 -6
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { extension, isPacked, toString, Tree } from "@weborigami/async-tree";
|
|
2
|
+
import { symbols } from "@weborigami/language";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
2
4
|
import { computedMIMEType } from "whatwg-mimetype";
|
|
3
5
|
import { mediaTypeForExtension } from "./mediaTypes.js";
|
|
4
6
|
|
|
@@ -19,6 +21,16 @@ export default async function constructResponse(request, resource) {
|
|
|
19
21
|
// Determine media type, what data we'll send, and encoding.
|
|
20
22
|
const url = new URL(request?.url ?? "", `https://${request?.headers.host}`);
|
|
21
23
|
|
|
24
|
+
if (!isPacked(resource) && typeof resource.pack === "function") {
|
|
25
|
+
resource = await resource.pack();
|
|
26
|
+
if (typeof resource === "function") {
|
|
27
|
+
resource = await resource();
|
|
28
|
+
}
|
|
29
|
+
if (resource instanceof Response) {
|
|
30
|
+
return resource;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
if (!url.pathname.endsWith("/") && Tree.isMaplike(resource)) {
|
|
23
35
|
// Maplike resource: redirect to its index page.
|
|
24
36
|
const Location = `${url.pathname}/`;
|
|
@@ -30,16 +42,6 @@ export default async function constructResponse(request, resource) {
|
|
|
30
42
|
});
|
|
31
43
|
}
|
|
32
44
|
|
|
33
|
-
if (!isPacked(resource) && typeof resource.pack === "function") {
|
|
34
|
-
resource = await resource.pack();
|
|
35
|
-
if (typeof resource === "function") {
|
|
36
|
-
resource = await resource();
|
|
37
|
-
}
|
|
38
|
-
if (resource instanceof Response) {
|
|
39
|
-
return resource;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
45
|
let body = resource;
|
|
44
46
|
if (!isPacked(resource)) {
|
|
45
47
|
// Can we treat it as text?
|
|
@@ -50,6 +52,7 @@ export default async function constructResponse(request, resource) {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
// Determine MIME type
|
|
55
|
+
/** @type {string | undefined} */
|
|
53
56
|
let mediaType;
|
|
54
57
|
if (resource.mediaType) {
|
|
55
58
|
// Resource indicates its own media type.
|
|
@@ -77,6 +80,10 @@ export default async function constructResponse(request, resource) {
|
|
|
77
80
|
} else {
|
|
78
81
|
mediaType = sniffedType.toString();
|
|
79
82
|
}
|
|
83
|
+
if (mediaType === "text/plain") {
|
|
84
|
+
// Prefer UTF-8 encoding for text/plain
|
|
85
|
+
mediaType += "; charset=utf-8";
|
|
86
|
+
}
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
}
|
|
@@ -91,7 +98,35 @@ export default async function constructResponse(request, resource) {
|
|
|
91
98
|
);
|
|
92
99
|
}
|
|
93
100
|
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
// Compute ETag from the body content.
|
|
102
|
+
let etag;
|
|
103
|
+
if (!resource[symbols.volatileSymbol]) {
|
|
104
|
+
const hash = createHash("sha1");
|
|
105
|
+
if (typeof body === "string" || body instanceof String) {
|
|
106
|
+
hash.update(String(body), "utf8");
|
|
107
|
+
} else {
|
|
108
|
+
hash.update(body);
|
|
109
|
+
}
|
|
110
|
+
const digest = hash.digest("hex");
|
|
111
|
+
// Store ETag with quotes in cache to match If-None-Match header
|
|
112
|
+
etag = `"${digest}"`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @type {Record<string, string>} */
|
|
116
|
+
const headers = {};
|
|
117
|
+
if (mediaType) {
|
|
118
|
+
headers["Content-Type"] = mediaType;
|
|
119
|
+
}
|
|
120
|
+
if (etag) {
|
|
121
|
+
headers["Cache-Control"] = "no-cache";
|
|
122
|
+
headers.ETag = etag;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const response = new Response(body, { headers });
|
|
126
|
+
|
|
127
|
+
if (resource[symbols.volatileSymbol]) {
|
|
128
|
+
response[symbols.volatileSymbol] = true;
|
|
129
|
+
}
|
|
130
|
+
|
|
96
131
|
return response;
|
|
97
132
|
}
|
package/src/server/server.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
TraverseError,
|
|
3
|
-
Tree,
|
|
4
2
|
keysFromPath,
|
|
5
3
|
trailingSlash,
|
|
4
|
+
TraverseError,
|
|
5
|
+
Tree,
|
|
6
6
|
} from "@weborigami/async-tree";
|
|
7
|
-
import { formatError } from "@weborigami/language";
|
|
7
|
+
import { formatError, systemCache, SystemCacheMap } from "@weborigami/language";
|
|
8
8
|
import { ServerResponse } from "node:http";
|
|
9
9
|
import constructResponse from "./constructResponse.js";
|
|
10
10
|
import parsePostData from "./parsePostData.js";
|
|
@@ -13,35 +13,30 @@ import parsePostData from "./parsePostData.js";
|
|
|
13
13
|
* Copy a constructed response to a ServerResponse. Return true if the response
|
|
14
14
|
* was successfully copied, and false if there was a problem.
|
|
15
15
|
*
|
|
16
|
-
* @param {Response}
|
|
16
|
+
* @param {Response} original
|
|
17
17
|
* @param {ServerResponse} response
|
|
18
18
|
*/
|
|
19
|
-
async function copyResponse(
|
|
20
|
-
|
|
21
|
-
response.
|
|
19
|
+
async function copyResponse(original, response) {
|
|
20
|
+
const clone = original.clone();
|
|
21
|
+
response.statusCode = clone.status;
|
|
22
|
+
response.statusMessage = clone.statusText;
|
|
22
23
|
|
|
23
24
|
// @ts-ignore Headers has an iterator in ES2022 but tsc doesn't know that.
|
|
24
|
-
for (const [key, value] of
|
|
25
|
+
for (const [key, value] of clone.headers) {
|
|
25
26
|
response.setHeader(key, value);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
({ done, value } = await reader.read());
|
|
36
|
-
}
|
|
37
|
-
response.end();
|
|
38
|
-
} catch (/** @type {any} */ error) {
|
|
39
|
-
console.error(error.message);
|
|
40
|
-
return false;
|
|
29
|
+
if (clone.body) {
|
|
30
|
+
// Write the response body
|
|
31
|
+
const reader = clone.body.getReader();
|
|
32
|
+
let { done, value } = await reader.read();
|
|
33
|
+
while (!done) {
|
|
34
|
+
response.write(value);
|
|
35
|
+
({ done, value } = await reader.read());
|
|
41
36
|
}
|
|
42
37
|
}
|
|
43
38
|
|
|
44
|
-
|
|
39
|
+
response.end();
|
|
45
40
|
}
|
|
46
41
|
|
|
47
42
|
/**
|
|
@@ -54,36 +49,68 @@ async function copyResponse(constructed, response) {
|
|
|
54
49
|
export async function handleRequest(request, response, map) {
|
|
55
50
|
// For parsing purposes, we assume HTTPS -- it doesn't affect parsing.
|
|
56
51
|
const url = new URL(request.url ?? "", `https://${request.headers.host}`);
|
|
57
|
-
const keys = keysFromUrl(url);
|
|
58
52
|
|
|
53
|
+
// Do we already have an ETag for this resource?
|
|
54
|
+
let cachePath = SystemCacheMap.joinPath("_site", url.pathname.slice(1));
|
|
55
|
+
if (url.pathname.endsWith("/")) {
|
|
56
|
+
cachePath += "index.html";
|
|
57
|
+
}
|
|
58
|
+
const cacheEntry = systemCache.get(cachePath)?.value;
|
|
59
|
+
const etag = cacheEntry?.headers?.get("Etag");
|
|
60
|
+
if (etag) {
|
|
61
|
+
// Does the client already have this version?
|
|
62
|
+
const ifNoneMatch = request?.headers?.["if-none-match"];
|
|
63
|
+
if (ifNoneMatch === etag) {
|
|
64
|
+
// Client already has this version
|
|
65
|
+
response.writeHead(304, {
|
|
66
|
+
"Cache-Control": "no-cache",
|
|
67
|
+
ETag: etag,
|
|
68
|
+
});
|
|
69
|
+
response.end();
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const keys = keysFromUrl(url);
|
|
59
75
|
const data = request.method === "POST" ? await parsePostData(request) : null;
|
|
60
76
|
|
|
61
77
|
// Ask the tree for the resource with those keys.
|
|
62
|
-
let resource;
|
|
63
78
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
79
|
+
// We wrap the tree traversal in a call that will both set the etag for this
|
|
80
|
+
// resource and copy the constructed response to the ServerResponse. The
|
|
81
|
+
// etag is already included in the response headers so we don't need to
|
|
82
|
+
// receive it here.
|
|
83
|
+
const constructed = await systemCache.getOrInsertComputedAsync(
|
|
84
|
+
cachePath,
|
|
85
|
+
async () => {
|
|
86
|
+
let resource = await Tree.traverseOrThrow(map, ...keys);
|
|
87
|
+
|
|
88
|
+
// If resource is a function, invoke to get the object we want to return.
|
|
89
|
+
// For a POST request, pass the data to the function.
|
|
90
|
+
if (typeof resource === "function") {
|
|
91
|
+
resource = data ? await resource(data) : await resource();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Construct the response
|
|
95
|
+
return resource != null
|
|
96
|
+
? await constructResponse(request, resource)
|
|
97
|
+
: null;
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Copy the constructed (and cached) response to the server response
|
|
102
|
+
if (constructed) {
|
|
103
|
+
await copyResponse(constructed, response);
|
|
104
|
+
return true;
|
|
70
105
|
}
|
|
71
|
-
|
|
72
|
-
if (resource == null) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Construct the response.
|
|
77
|
-
const constructed = await constructResponse(request, resource);
|
|
78
|
-
|
|
79
|
-
// Copy the construct response to the ServerResponse and return true if
|
|
80
|
-
// the response was valid.
|
|
81
|
-
return copyResponse(constructed, response);
|
|
82
106
|
} catch (/** @type {any} */ error) {
|
|
83
107
|
// Display an error
|
|
84
|
-
respondWithError(response, error);
|
|
108
|
+
await respondWithError(response, error);
|
|
85
109
|
return true;
|
|
86
110
|
}
|
|
111
|
+
|
|
112
|
+
// No resource found at this path.
|
|
113
|
+
return false;
|
|
87
114
|
}
|
|
88
115
|
|
|
89
116
|
export function keysFromUrl(url) {
|