@coralreefventures/site-tools 0.1.1 → 0.2.0
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/README.md +56 -5
- package/dist/bin/reef-door-bundle.d.ts +3 -0
- package/dist/bin/reef-door-bundle.d.ts.map +1 -0
- package/dist/bin/reef-door-bundle.js +56 -0
- package/dist/bin/reef-door-bundle.js.map +1 -0
- package/dist/door/config.d.ts +44 -0
- package/dist/door/config.d.ts.map +1 -0
- package/dist/door/config.js +89 -0
- package/dist/door/config.js.map +1 -0
- package/dist/door/next.d.ts +12 -0
- package/dist/door/next.d.ts.map +1 -0
- package/dist/door/next.js +36 -0
- package/dist/door/next.js.map +1 -0
- package/dist/door/package.d.ts +66 -0
- package/dist/door/package.d.ts.map +1 -0
- package/dist/door/package.js +150 -0
- package/dist/door/package.js.map +1 -0
- package/dist/door/server.d.ts +48 -0
- package/dist/door/server.d.ts.map +1 -0
- package/dist/door/server.js +272 -0
- package/dist/door/server.js.map +1 -0
- package/dist/door/verify.d.ts +52 -0
- package/dist/door/verify.d.ts.map +1 -0
- package/dist/door/verify.js +133 -0
- package/dist/door/verify.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/serve.d.ts +26 -1
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +84 -39
- package/dist/serve.js.map +1 -1
- package/package.json +5 -3
- package/src/bin/reef-door-bundle.ts +60 -0
- package/src/door/config.ts +112 -0
- package/src/door/next.ts +35 -0
- package/src/door/package.ts +184 -0
- package/src/door/server.ts +308 -0
- package/src/door/verify.ts +152 -0
- package/src/index.ts +45 -1
- package/src/serve.ts +101 -29
package/src/serve.ts
CHANGED
|
@@ -1,73 +1,145 @@
|
|
|
1
1
|
import { createReadStream } from "node:fs";
|
|
2
2
|
import { stat } from "node:fs/promises";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createServer,
|
|
5
|
+
type IncomingMessage,
|
|
6
|
+
type OutgoingHttpHeaders,
|
|
7
|
+
type Server,
|
|
8
|
+
type ServerResponse,
|
|
9
|
+
} from "node:http";
|
|
4
10
|
import path from "node:path";
|
|
5
11
|
import { createBrotliCompress, createGzip } from "node:zlib";
|
|
6
12
|
|
|
13
|
+
// This module is also part of the door gate's compute bundle (door/package.ts copies it), so it imports nothing but
|
|
14
|
+
// Node's own modules.
|
|
15
|
+
|
|
7
16
|
const types: Record<string, string> = {
|
|
8
17
|
".html": "text/html; charset=utf-8",
|
|
9
18
|
".js": "text/javascript; charset=utf-8",
|
|
19
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
10
20
|
".css": "text/css; charset=utf-8",
|
|
11
21
|
".json": "application/json",
|
|
22
|
+
".webmanifest": "application/manifest+json",
|
|
12
23
|
".txt": "text/plain; charset=utf-8",
|
|
13
24
|
".xml": "application/xml",
|
|
14
25
|
".png": "image/png",
|
|
26
|
+
".jpg": "image/jpeg",
|
|
27
|
+
".jpeg": "image/jpeg",
|
|
28
|
+
".gif": "image/gif",
|
|
29
|
+
".webp": "image/webp",
|
|
30
|
+
".avif": "image/avif",
|
|
15
31
|
".svg": "image/svg+xml",
|
|
32
|
+
".woff": "font/woff",
|
|
16
33
|
".woff2": "font/woff2",
|
|
17
34
|
".ico": "image/x-icon",
|
|
18
35
|
};
|
|
19
36
|
|
|
37
|
+
/** The Content-Type a file is served with, by its extension. */
|
|
38
|
+
export const contentTypeOf = (file: string): string => types[path.extname(file)] ?? "application/octet-stream";
|
|
39
|
+
|
|
20
40
|
const isFile = async (file: string) => (await stat(file).catch(() => null))?.isFile() ?? false;
|
|
21
41
|
const isDirectory = async (file: string) => (await stat(file).catch(() => null))?.isDirectory() ?? false;
|
|
22
42
|
|
|
23
43
|
/** Brotli or gzip for text, as the browser accepts; images and fonts are already compressed. */
|
|
24
44
|
export const encodingFor = (type: string, accepts: string): "br" | "gzip" | undefined => {
|
|
25
|
-
if (!/^(text\/|application\/(json|xml)|image\/svg)/.test(type)) {
|
|
45
|
+
if (!/^(text\/|application\/(json|xml|manifest\+json)|image\/svg)/.test(type)) {
|
|
26
46
|
return undefined;
|
|
27
47
|
}
|
|
28
48
|
return accepts.includes("br") ? "br" : accepts.includes("gzip") ? "gzip" : undefined;
|
|
29
49
|
};
|
|
30
50
|
|
|
51
|
+
/**
|
|
52
|
+
* What a decoded path names inside an export, the way Amplify Hosting maps one: a file, a directory's index.html
|
|
53
|
+
* when the path ends in a slash, a directory reached without its slash (the caller redirects), nothing, or a path
|
|
54
|
+
* outside the export altogether (the caller refuses it).
|
|
55
|
+
*/
|
|
56
|
+
export type StaticTarget =
|
|
57
|
+
| { kind: "file"; file: string }
|
|
58
|
+
| { kind: "directory" }
|
|
59
|
+
| { kind: "missing"; notFound: string }
|
|
60
|
+
| { kind: "outside" };
|
|
61
|
+
|
|
62
|
+
export const staticTarget = async (root: string, decodedPath: string): Promise<StaticTarget> => {
|
|
63
|
+
const base = path.resolve(root);
|
|
64
|
+
const file = path.join(base, decodedPath);
|
|
65
|
+
if (file !== base && !file.startsWith(`${base}${path.sep}`)) {
|
|
66
|
+
return { kind: "outside" };
|
|
67
|
+
}
|
|
68
|
+
const notFound = path.join(base, "404.html");
|
|
69
|
+
if (await isFile(file)) {
|
|
70
|
+
return { kind: "file", file };
|
|
71
|
+
}
|
|
72
|
+
if (await isDirectory(file)) {
|
|
73
|
+
if (decodedPath.endsWith("/")) {
|
|
74
|
+
const index = path.join(file, "index.html");
|
|
75
|
+
return (await isFile(index)) ? { kind: "file", file: index } : { kind: "missing", notFound };
|
|
76
|
+
}
|
|
77
|
+
return { kind: "directory" };
|
|
78
|
+
}
|
|
79
|
+
return { kind: "missing", notFound };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Streams one file, typed by its extension and compressed as the request accepts, with any extra headers the caller
|
|
84
|
+
* adds. A HEAD request gets the headers alone. A file that cannot be read after the headers are out ends the
|
|
85
|
+
* response rather than sending anything else.
|
|
86
|
+
*/
|
|
87
|
+
export const sendFile = async (
|
|
88
|
+
request: IncomingMessage,
|
|
89
|
+
response: ServerResponse,
|
|
90
|
+
file: string,
|
|
91
|
+
status = 200,
|
|
92
|
+
headers: OutgoingHttpHeaders = {},
|
|
93
|
+
): Promise<void> => {
|
|
94
|
+
if (!(await isFile(file))) {
|
|
95
|
+
response.writeHead(status, { ...headers, "Content-Length": "0" }).end();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const type = contentTypeOf(file);
|
|
99
|
+
const encoding = encodingFor(type, String(request.headers["accept-encoding"] ?? ""));
|
|
100
|
+
response.writeHead(status, {
|
|
101
|
+
...headers,
|
|
102
|
+
"Content-Type": type,
|
|
103
|
+
Vary: "Accept-Encoding",
|
|
104
|
+
...(encoding && { "Content-Encoding": encoding }),
|
|
105
|
+
});
|
|
106
|
+
if (request.method === "HEAD") {
|
|
107
|
+
response.end();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const body = createReadStream(file);
|
|
111
|
+
const compressor = encoding === "br" ? createBrotliCompress() : encoding === "gzip" ? createGzip() : undefined;
|
|
112
|
+
body.on("error", () => response.destroy());
|
|
113
|
+
(compressor ? body.pipe(compressor) : body).pipe(response);
|
|
114
|
+
};
|
|
115
|
+
|
|
31
116
|
/**
|
|
32
117
|
* A server for a static export the way Amplify Hosting serves one, for screen tests and Lighthouse runs: a directory
|
|
33
118
|
* answers with its index.html, a directory path without its slash redirects to it, anything else is the 404 page
|
|
34
119
|
* with status 404, and text is compressed (Brotli or gzip) as the CDN compresses it. Not started: `listen` it, or
|
|
35
120
|
* call `serve`.
|
|
36
121
|
*/
|
|
37
|
-
export const createStaticServer = (root: string): Server =>
|
|
38
|
-
|
|
39
|
-
return createServer(async (request, response) => {
|
|
122
|
+
export const createStaticServer = (root: string): Server =>
|
|
123
|
+
createServer(async (request, response) => {
|
|
40
124
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
41
|
-
|
|
42
|
-
|
|
125
|
+
let decoded: string;
|
|
126
|
+
try {
|
|
127
|
+
decoded = decodeURIComponent(url.pathname);
|
|
128
|
+
} catch {
|
|
43
129
|
response.writeHead(400).end();
|
|
44
130
|
return;
|
|
45
131
|
}
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
});
|
|
54
|
-
const body = createReadStream(target);
|
|
55
|
-
const compressor = encoding === "br" ? createBrotliCompress() : encoding === "gzip" ? createGzip() : undefined;
|
|
56
|
-
(compressor ? body.pipe(compressor) : body).pipe(response);
|
|
57
|
-
};
|
|
58
|
-
if (await isFile(file)) {
|
|
59
|
-
send(file);
|
|
60
|
-
} else if (await isDirectory(file)) {
|
|
61
|
-
if (url.pathname.endsWith("/")) {
|
|
62
|
-
send(path.join(file, "index.html"));
|
|
63
|
-
} else {
|
|
64
|
-
response.writeHead(301, { Location: `${url.pathname}/${url.search}` }).end();
|
|
65
|
-
}
|
|
132
|
+
const target = await staticTarget(root, decoded);
|
|
133
|
+
if (target.kind === "outside") {
|
|
134
|
+
response.writeHead(400).end();
|
|
135
|
+
} else if (target.kind === "file") {
|
|
136
|
+
await sendFile(request, response, target.file);
|
|
137
|
+
} else if (target.kind === "directory") {
|
|
138
|
+
response.writeHead(301, { Location: `${url.pathname}/${url.search}` }).end();
|
|
66
139
|
} else {
|
|
67
|
-
|
|
140
|
+
await sendFile(request, response, target.notFound, 404);
|
|
68
141
|
}
|
|
69
142
|
});
|
|
70
|
-
};
|
|
71
143
|
|
|
72
144
|
export type ServeOptions = {
|
|
73
145
|
/** The static export; `out` by default, Next's. */
|