@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.
Files changed (42) hide show
  1. package/README.md +56 -5
  2. package/dist/bin/reef-door-bundle.d.ts +3 -0
  3. package/dist/bin/reef-door-bundle.d.ts.map +1 -0
  4. package/dist/bin/reef-door-bundle.js +56 -0
  5. package/dist/bin/reef-door-bundle.js.map +1 -0
  6. package/dist/door/config.d.ts +44 -0
  7. package/dist/door/config.d.ts.map +1 -0
  8. package/dist/door/config.js +89 -0
  9. package/dist/door/config.js.map +1 -0
  10. package/dist/door/next.d.ts +12 -0
  11. package/dist/door/next.d.ts.map +1 -0
  12. package/dist/door/next.js +36 -0
  13. package/dist/door/next.js.map +1 -0
  14. package/dist/door/package.d.ts +66 -0
  15. package/dist/door/package.d.ts.map +1 -0
  16. package/dist/door/package.js +150 -0
  17. package/dist/door/package.js.map +1 -0
  18. package/dist/door/server.d.ts +48 -0
  19. package/dist/door/server.d.ts.map +1 -0
  20. package/dist/door/server.js +272 -0
  21. package/dist/door/server.js.map +1 -0
  22. package/dist/door/verify.d.ts +52 -0
  23. package/dist/door/verify.d.ts.map +1 -0
  24. package/dist/door/verify.js +133 -0
  25. package/dist/door/verify.js.map +1 -0
  26. package/dist/index.d.ts +6 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +6 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/serve.d.ts +26 -1
  31. package/dist/serve.d.ts.map +1 -1
  32. package/dist/serve.js +84 -39
  33. package/dist/serve.js.map +1 -1
  34. package/package.json +5 -3
  35. package/src/bin/reef-door-bundle.ts +60 -0
  36. package/src/door/config.ts +112 -0
  37. package/src/door/next.ts +35 -0
  38. package/src/door/package.ts +184 -0
  39. package/src/door/server.ts +308 -0
  40. package/src/door/verify.ts +152 -0
  41. package/src/index.ts +45 -1
  42. 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 { createServer, type Server } from "node:http";
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
- const base = path.resolve(root);
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
- const file = path.join(base, decodeURIComponent(url.pathname));
42
- if (file !== base && !file.startsWith(`${base}${path.sep}`)) {
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 send = (target: string, status = 200) => {
47
- const type = types[path.extname(target)] ?? "application/octet-stream";
48
- const encoding = encodingFor(type, String(request.headers["accept-encoding"] ?? ""));
49
- response.writeHead(status, {
50
- "Content-Type": type,
51
- Vary: "Accept-Encoding",
52
- ...(encoding && { "Content-Encoding": encoding }),
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
- send(path.join(base, "404.html"), 404);
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. */