├── .bmp.yml
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── cli.ts
├── cli_test.ts
├── mod.ts
├── mod_test.ts
├── testdata
├── bar.ts
├── foo.txt
└── index.html
├── util.ts
└── util_test.ts
/.bmp.yml:
--------------------------------------------------------------------------------
1 | version: 0.3.2
2 | commit: 'chore: bump to v%.%.%'
3 | files:
4 | README.md:
5 | - deploy_dir v%.%.%
6 | - 'https://deno.land/x/deploy_dir@v%.%.%/cli.ts'
7 | cli.ts: const VERSION = "%.%.%";
8 | mod.ts: 'https://deno.land/x/deploy_dir@v%.%.%'
9 | mod_test.ts: 'https://deno.land/x/deploy_dir@v%.%.%'
10 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 | on:
3 | push:
4 | branches: [main]
5 | pull_request:
6 | branches: [main]
7 | jobs:
8 | build:
9 | strategy:
10 | matrix:
11 | deno:
12 | - v1.x
13 | - canary
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v2
17 | - uses: denoland/setup-deno@main
18 | with:
19 | deno-version: ${{ matrix.deno }}
20 | - run: deno fmt --check
21 | - run: deno lint
22 | - run: deno test -A
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | deploy.ts
2 | deploy.js
3 | .vscode
4 | .vim
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 | Copyright © 2021 Yoshiya Hinosawa ( @kt3k )
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 | THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | deploy.ts:
2 | deno run -A cli.ts testdata -y -o deploy.ts --ts
3 |
4 | deploy.js:
5 | deno run -A cli.ts testdata -y -o deploy.js
6 |
7 | test:
8 | deno test -A
9 |
10 | fmt:
11 | deno fmt
12 |
13 | serve:
14 | make deploy.ts
15 | deployctl run deploy.ts
16 |
17 | .PHONY: deploy.ts test
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ⚠️ This project is now deprecated.
2 |
3 | Deno Deploy now supports [static files](https://deno.com/blog/deploy-static-files). You can retreive static files in your repository by using Deno FS APIs such as `Deno.readFile`, `Deno.readDir`, etc. There is no need of using this tool to host static files in Deno Deploy.
4 |
5 | # deploy_dir v0.3.2
6 |
7 | [](https://github.com/kt3k/deploy_dir/actions/workflows/ci.yml)
8 |
9 | `deploy_dir` is a CLI tool for hosting static web sites in
10 | [Deno Deploy](https://deno.com/deploy).
11 |
12 | `deploy_dir` reads the contents of a directory and package them as source code
13 | for Deno Deploy.
14 |
15 | Note: This tool is not suitable for hosting large static contents like videos,
16 | audios, high-res images, etc.
17 |
18 | # Install
19 |
20 | Deno >= 1.10 is recommended.
21 |
22 | ```
23 | deno install -f --allow-read=. --allow-write=. https://deno.land/x/deploy_dir@v0.3.2/cli.ts
24 | ```
25 |
26 | # Usage
27 |
28 | The basic usage of the CLI is:
29 |
30 | ```
31 | deploy_dir dist -o deploy.js
32 | ```
33 |
34 | This command reads the files under `./dist/` directory and writes the source
35 | code for [Deno Deploy](https://deno.com/deploy) to `./deploy.js`
36 |
37 | You can check the behavior of this deployment by using
38 | [deployctl](https://deno.land/x/deploy) command:
39 |
40 | ```
41 | deployctl run deploy.js
42 | ```
43 |
44 | This serves the contents of the source directory such as
45 | http://localhost:8080/foo.txt , http://localhost:8080/bar.ts , etc (Note: The
46 | directory index path maps to `dir/index.html` automatically)
47 |
48 | # CLI usage
49 |
50 | `deploy_dir` supports the following options:
51 |
52 | ```
53 | Usage: deploy_dir
[-h][-v][-o ][--js][-r ]
54 |
55 | Read the files under the given directory and outputs the source code for Deno Deploy
56 | which serves the contents of the given directory.
57 |
58 | Options:
59 | -r, --root Specifies the root path of the deployed static files. Default is '/'.
60 | -o, --output Specifies the output filename. If not specified, the tool shows the source code to stdout.
61 | --ts Output source code as TypeScript. Default is false.
62 | --basic-auth Performs basic authentication in the deployed site. The credentials are in the form of :
63 | --cache Specifies the cache control header for specific file paths.
64 | e.g. --cache "/css:max-age=3600,/img:max-age=86400"
65 | -y, --yes Answers yes when the tool ask for overwriting the output.
66 | -v, --version Shows the version number.
67 | -h, --help Shows the help message.
68 |
69 | Example:
70 | deploy_dir dist/ -o deploy.js
71 | Reads the files under dist/ directory and outputs 'deploy.js' file which
72 | serves the contents under dist/ as deno deploy worker.
73 | ```
74 |
75 | # Internals
76 |
77 | The output source typically looks like the below:
78 |
79 | ```ts
80 | // This script is generated by https://deno.land/x/deploy_dir@v0.1.6
81 | import { decode } from "https://deno.land/std@0.97.0/encoding/base64.ts";
82 | import { gunzip } from "https://raw.githubusercontent.com/kt3k/compress/bbe0a818d2acd399350b30036ff8772354b1c2df/gzip/gzip.ts";
83 | console.log("init");
84 | const dirData: Record = {};
85 | dirData["/bar.ts"] = [
86 | decode("H4sIAAAAAAAAA0vOzyvOz0nVy8lP11BKSixS0rTmAgCz8kN9FAAAAA=="),
87 | "text/typescript",
88 | ];
89 | dirData["/foo.txt"] = [
90 | decode("H4sIAAAAAAAAA0vLz+cCAKhlMn4EAAAA"),
91 | "text/plain",
92 | ];
93 | dirData["/index.html"] = [
94 | decode("H4sIAAAAAAAAA/NIzcnJV+QCAJ7YQrAHAAAA"),
95 | "text/html",
96 | ];
97 | addEventListener("fetch", (e) => {
98 | let { pathname } = new URL(e.request.url);
99 | if (pathname.endsWith("/")) {
100 | pathname += "index.html";
101 | }
102 | let data = dirData[pathname];
103 | if (!data) {
104 | data = dirData[pathname + ".html"];
105 | }
106 | if (data) {
107 | const [bytes, mediaType] = data;
108 | const acceptsGzip = e.request.headers.get("accept-encoding")?.split(
109 | /[,;]s*/,
110 | ).includes("gzip");
111 | if (acceptsGzip) {
112 | e.respondWith(
113 | new Response(bytes, {
114 | headers: {
115 | "content-type": mediaType,
116 | "content-encoding": "gzip",
117 | },
118 | }),
119 | );
120 | } else {
121 | e.respondWith(
122 | new Response(gunzip(bytes), { headers: { "content-type": mediaType } }),
123 | );
124 | }
125 | return;
126 | }
127 | e.respondWith(new Response("404 Not Found", { status: 404 }));
128 | });
129 | ```
130 |
131 | You can extend this deploy source code by removing the last line
132 | `e.respondWith(new Response("404 Not Found", { status: 404 }));` and replace it
133 | with your own handler.
134 |
135 | # Limitation
136 |
137 | If your generated script exceeds 5MB, your deployment will become very unstable.
138 | This is because Deno Deploy has
139 | [256MB memory limit](https://deno.com/deploy/docs/pricing-and-limits). In that
140 | case, we recommend using
141 | [proxying technique](https://deno.com/deploy/docs/serve-static-assets) for
142 | serving static web site.
143 |
144 | # History
145 |
146 | - 2021-06-17 v0.3.2 Add --cache option.
147 | - 2021-06-17 v0.3.1 The output defaults to JavaScript. Add `--ts` option.
148 | - 2021-06-17 v0.3.0 Add Etag Support.
149 |
150 | # License
151 |
152 | MIT
153 |
--------------------------------------------------------------------------------
/cli.ts:
--------------------------------------------------------------------------------
1 | import { parse } from "https://deno.land/std@0.97.0/flags/mod.ts";
2 | import { red } from "https://deno.land/std@0.97.0/fmt/colors.ts";
3 | import { readDirCreateSource } from "./mod.ts";
4 | import { parseCacheOption } from "./util.ts";
5 |
6 | const NAME = "deploy_dir";
7 | const VERSION = "0.3.2";
8 |
9 | function usage() {
10 | console.log(`
11 | Usage: ${NAME} [-h][-v][-o ][--ts][-r ]
12 |
13 | Read the files under the given directory and outputs the source code for Deno Deploy
14 | which serves the contents of the given directory.
15 |
16 | Options:
17 | -r, --root Specifies the root path of the deployed static files. Default is '/'.
18 | -o, --output Specifies the output filename. If not specified, the tool shows the source code to stdout.
19 | --ts Output source code as TypeScript. Default is false.
20 | --basic-auth Performs basic authentication in the deployed site. The credentials are in the form of :
21 | --cache Specifies the cache control header for specific file paths.
22 | e.g. --cache "/css:max-age=3600,/img:max-age=86400"
23 | -y, --yes Answers yes when the tool ask for overwriting the output.
24 | -v, --version Shows the version number.
25 | -h, --help Shows the help message.
26 |
27 | Example:
28 | deploy_dir dist/ -o deploy.ts
29 | Reads the files under dist/ directory and outputs 'deploy.ts' file which
30 | serves the contents under dist/ as deno deploy worker.
31 | `.trim());
32 | }
33 |
34 | type CliArgs = {
35 | _: string[];
36 | version: boolean;
37 | help: boolean;
38 | cache: string;
39 | root: string;
40 | output: string;
41 | ts: boolean;
42 | "basic-auth": string;
43 | yes: boolean;
44 | };
45 |
46 | export async function main(cliArgs: string[]) {
47 | const {
48 | version,
49 | help,
50 | cache,
51 | root = "/",
52 | output,
53 | ts,
54 | "basic-auth": basicAuth,
55 | yes,
56 | _: args,
57 | } = parse(cliArgs, {
58 | boolean: ["help", "version", "ts", "yes"],
59 | string: ["cache", "root", "output", "basic-auth"],
60 | alias: {
61 | h: "help",
62 | v: "version",
63 | o: "output",
64 | r: "root",
65 | y: "yes",
66 | },
67 | }) as CliArgs;
68 |
69 | if (help) {
70 | usage();
71 | return 0;
72 | }
73 |
74 | if (version) {
75 | console.log(`${NAME}@${VERSION}`);
76 | return 0;
77 | }
78 |
79 | const [dir] = args;
80 |
81 | if (!dir) {
82 | console.log(red("Error: target directory is not given"));
83 | usage();
84 | return 1;
85 | }
86 |
87 | const cacheRecord = cache ? parseCacheOption(cache) : undefined;
88 |
89 | const source = await readDirCreateSource(dir, root, {
90 | cache: cacheRecord,
91 | toJavaScript: !ts,
92 | basicAuth,
93 | });
94 | if (!output) {
95 | console.log(source);
96 | return 0;
97 | }
98 | try {
99 | const stat = await Deno.lstat(output);
100 | if (stat.isDirectory) {
101 | console.log(red(`Error: the output path ${output} is directory`));
102 | return 1;
103 | }
104 | if (
105 | yes || confirm(
106 | `The output path ${output} already exists. Are you sure to write this file?`,
107 | )
108 | ) {
109 | await performSourceCodeWrite(output, source);
110 | return 0;
111 | } else {
112 | console.log("Aborting");
113 | return 1;
114 | }
115 | } catch (e) {
116 | if (e.name === "NotFound") {
117 | await performSourceCodeWrite(output, source);
118 | return 0;
119 | }
120 | throw e;
121 | }
122 | }
123 |
124 | async function performSourceCodeWrite(output: string, source: string) {
125 | console.log(`Writing the source code to '${output}'`);
126 | await Deno.writeTextFile(output, source);
127 | console.log(`Done`);
128 | }
129 |
130 | if (import.meta.main) {
131 | Deno.exit(await main(Deno.args));
132 | }
133 |
--------------------------------------------------------------------------------
/cli_test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertEquals,
3 | assertStringIncludes,
4 | } from "https://deno.land/std@0.97.0/testing/asserts.ts";
5 | import { join, resolve } from "https://deno.land/std@0.97.0/path/mod.ts";
6 | import { main } from "./cli.ts";
7 |
8 | Deno.test("deploy_dir -h", async () => {
9 | const code = await main(["-h"]);
10 | assertEquals(code, 0);
11 | });
12 |
13 | Deno.test("deploy_dir -v", async () => {
14 | const code = await main(["-v"]);
15 | assertEquals(code, 0);
16 | });
17 |
18 | Deno.test("deploy_dir - target dir is not given", async () => {
19 | const code = await main([]);
20 | assertEquals(code, 1);
21 | });
22 |
23 | Deno.test("deploy_dir testdata", async () => {
24 | const tempdir = await Deno.makeTempDir();
25 | const code = await denoRun([
26 | resolve("cli.ts"),
27 | resolve("testdata"),
28 | "-o",
29 | "deploy.ts",
30 | ], { cwd: tempdir });
31 | assertEquals(code, 0);
32 | const source = await Deno.readTextFile(join(tempdir, "deploy.ts"));
33 | assertStringIncludes(source, "addEventListener");
34 | assertStringIncludes(source, "foo.txt");
35 | assertStringIncludes(source, "bar.ts");
36 | });
37 |
38 | async function denoRun(
39 | args: string[],
40 | { cwd }: { cwd?: string } = {},
41 | ): Promise {
42 | const p = Deno.run({
43 | cmd: [Deno.execPath(), "run", "-A", ...args],
44 | cwd,
45 | });
46 | const status = await p.status();
47 | p.close();
48 | return status.code;
49 | }
50 |
--------------------------------------------------------------------------------
/mod.ts:
--------------------------------------------------------------------------------
1 | import { walk } from "https://deno.land/std@0.97.0/fs/walk.ts";
2 | import { encode } from "https://deno.land/std@0.97.0/encoding/base64.ts";
3 | import { join, relative } from "https://deno.land/std@0.97.0/path/mod.ts";
4 | import { gzip } from "https://deno.land/x/compress@v0.3.8/gzip/gzip.ts";
5 | import { createHash } from "https://deno.land/std@0.99.0/hash/mod.ts";
6 |
7 | /**
8 | * Reads the contents of the given directory and creates the source code for Deno Deploy,
9 | * which serves the files in that directory
10 | */
11 | export async function readDirCreateSource(
12 | dir: string,
13 | root = "/",
14 | opts: {
15 | cache?: Record;
16 | toJavaScript?: boolean;
17 | basicAuth?: string;
18 | gzipTimestamp?: number;
19 | } = {},
20 | ): Promise {
21 | const buf: string[] = [];
22 | if (!root.startsWith("/")) {
23 | root = "/" + root;
24 | }
25 | buf.push(
26 | "// This script is generated by https://deno.land/x/deploy_dir@v0.3.2",
27 | );
28 | if (opts.basicAuth) {
29 | buf.push(
30 | 'import { basicAuth } from "https://deno.land/x/basic_auth@v1.0.0/mod.ts";',
31 | );
32 | }
33 | buf.push(
34 | 'import { decode } from "https://deno.land/std@0.97.0/encoding/base64.ts";',
35 | 'import { gunzip } from "https://raw.githubusercontent.com/kt3k/compress/bbe0a818d2acd399350b30036ff8772354b1c2df/gzip/gzip.ts";',
36 | );
37 | buf.push('console.log("init");');
38 | if (opts?.toJavaScript) {
39 | buf.push("const dirData = {};");
40 | } else {
41 | buf.push(
42 | "const dirData: Record = {};",
43 | );
44 | }
45 | const items: [string, string, string][] = [];
46 | for await (const { path } of walk(dir)) {
47 | const stat = await Deno.lstat(path);
48 | if (stat.isDirectory) {
49 | continue;
50 | }
51 | const name = join(root, relative(dir, path));
52 | const type = getMediaType(name);
53 | const contents = await Deno.readFile(path);
54 | const base64 = encode(
55 | gzip(contents, { timestamp: opts.gzipTimestamp || 0 }),
56 | );
57 | items.push([name, base64, type]);
58 | }
59 | items.sort(([name0], [name1]) => {
60 | if (name0 < name1) {
61 | return -1;
62 | } else if (name0 > name1) {
63 | return 1;
64 | }
65 | return 0;
66 | });
67 | for (const [name, base64, type] of items) {
68 | const hash = createHash("md5");
69 | hash.update(base64);
70 | const etag = hash.toString();
71 | let cacheControl = "private";
72 | if (opts.cache) {
73 | for (const [path, c] of Object.entries(opts.cache)) {
74 | if (name.startsWith(path)) {
75 | cacheControl = c;
76 | }
77 | }
78 | }
79 | buf.push(
80 | `dirData[${
81 | JSON.stringify(name)
82 | }] = [decode("${base64}"), "${type}", '"${etag}"', "${cacheControl}"];`,
83 | );
84 | }
85 | buf.push('addEventListener("fetch", (e) => {');
86 | if (opts.basicAuth) {
87 | const [user, password] = opts.basicAuth.split(":");
88 | if (!user || !password) {
89 | throw new Error(
90 | `Invalid form of basic auth creadentials: ${opts.basicAuth}`,
91 | );
92 | }
93 | buf.push(
94 | ` const unauthorized = basicAuth(e.request, "Access to the site", ${
95 | JSON.stringify({ [user]: password })
96 | });
97 | if (unauthorized) {
98 | e.respondWith(unauthorized);
99 | return;
100 | }
101 | `,
102 | );
103 | }
104 | buf.push(` let { pathname } = new URL(e.request.url);
105 | if (pathname.endsWith("/")) {
106 | pathname += "index.html";
107 | }
108 | let data = dirData[pathname];
109 | if (!data) {
110 | data = dirData[pathname + '.html'];
111 | }
112 | if (data) {
113 | const [bytes, mediaType, etag, cacheControl] = data;
114 | const acceptsGzip = e.request.headers.get("accept-encoding")?.split(/[,;]\s*/).includes("gzip");
115 | if (e.request.headers.get("if-none-match") === etag) {
116 | e.respondWith(new Response(null, { status: 304, statusText: "Not Modified" }));
117 | return;
118 | }
119 | if (acceptsGzip) {
120 | e.respondWith(new Response(bytes, { headers: {
121 | etag,
122 | "cache-control": cacheControl,
123 | "content-type": mediaType,
124 | "content-encoding": "gzip",
125 | } }));
126 | } else {
127 | e.respondWith(new Response(gunzip(bytes), { headers: {
128 | etag,
129 | "cache-control": cacheControl,
130 | "content-type": mediaType
131 | } }));
132 | }
133 | return;
134 | }
135 | e.respondWith(new Response("404 Not Found", { status: 404 }));
136 | });`);
137 |
138 | return buf.join("\n");
139 | }
140 |
141 | const MEDIA_TYPES: Record = {
142 | ".md": "text/markdown",
143 | ".html": "text/html",
144 | ".htm": "text/html",
145 | ".json": "application/json",
146 | ".jpg": "image/jpeg",
147 | ".jpeg": "image/jpeg",
148 | ".gif": "image/gif",
149 | ".png": "image/png",
150 | ".avif": "image/avif",
151 | ".webp": "image/webp",
152 | ".map": "application/json",
153 | ".txt": "text/plain",
154 | ".ts": "text/typescript",
155 | ".tsx": "text/tsx",
156 | ".js": "application/javascript",
157 | ".jsx": "text/jsx",
158 | ".gz": "application/gzip",
159 | ".css": "text/css",
160 | ".wasm": "application/wasm",
161 | ".mjs": "application/javascript",
162 | ".svg": "image/svg+xml",
163 | };
164 |
165 | export function getMediaType(path: string): string {
166 | const m = path.toLowerCase().match(/\.[a-z]+$/);
167 | if (m) {
168 | const [ext] = m;
169 | const mediaType = MEDIA_TYPES[ext];
170 | if (mediaType) {
171 | return mediaType;
172 | }
173 | }
174 | return "text/plain";
175 | }
176 |
--------------------------------------------------------------------------------
/mod_test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertEquals,
3 | assertStringIncludes,
4 | assertThrowsAsync,
5 | } from "https://deno.land/std@0.97.0/testing/asserts.ts";
6 | import { getMediaType, readDirCreateSource } from "./mod.ts";
7 |
8 | Deno.test("readDirCreateSource", async () => {
9 | const source = await readDirCreateSource("testdata", undefined, {
10 | gzipTimestamp: 0,
11 | });
12 | assertEquals(
13 | source,
14 | `
15 | // This script is generated by https://deno.land/x/deploy_dir@v0.3.2
16 | import { decode } from "https://deno.land/std@0.97.0/encoding/base64.ts";
17 | import { gunzip } from "https://raw.githubusercontent.com/kt3k/compress/bbe0a818d2acd399350b30036ff8772354b1c2df/gzip/gzip.ts";
18 | console.log("init");
19 | const dirData: Record = {};
20 | dirData["/bar.ts"] = [decode("H4sIAAAAAAAAA0vOzyvOz0nVy8lP11BKSixS0rTmAgCz8kN9FAAAAA=="), "text/typescript", '"aa9bb63fe50cf09a95776fc7dbbd5eb7"', "private"];
21 | dirData["/foo.txt"] = [decode("H4sIAAAAAAAAA0vLz+cCAKhlMn4EAAAA"), "text/plain", '"4ebd3923247dff92a9b80f2c1ff1caee"', "private"];
22 | dirData["/index.html"] = [decode("H4sIAAAAAAAAA/NIzcnJV+QCAJ7YQrAHAAAA"), "text/html", '"275c264be2a95c29ac07e6f63e3d016c"', "private"];
23 | addEventListener("fetch", (e) => {
24 | let { pathname } = new URL(e.request.url);
25 | if (pathname.endsWith("/")) {
26 | pathname += "index.html";
27 | }
28 | let data = dirData[pathname];
29 | if (!data) {
30 | data = dirData[pathname + '.html'];
31 | }
32 | if (data) {
33 | const [bytes, mediaType, etag, cacheControl] = data;
34 | const acceptsGzip = e.request.headers.get("accept-encoding")?.split(/[,;]s*/).includes("gzip");
35 | if (e.request.headers.get("if-none-match") === etag) {
36 | e.respondWith(new Response(null, { status: 304, statusText: "Not Modified" }));
37 | return;
38 | }
39 | if (acceptsGzip) {
40 | e.respondWith(new Response(bytes, { headers: {
41 | etag,
42 | "cache-control": cacheControl,
43 | "content-type": mediaType,
44 | "content-encoding": "gzip",
45 | } }));
46 | } else {
47 | e.respondWith(new Response(gunzip(bytes), { headers: {
48 | etag,
49 | "cache-control": cacheControl,
50 | "content-type": mediaType
51 | } }));
52 | }
53 | return;
54 | }
55 | e.respondWith(new Response("404 Not Found", { status: 404 }));
56 | });
57 | `.trim(),
58 | );
59 | });
60 |
61 | Deno.test("readDirCreateSource - toJavaScript", async () => {
62 | const source = await readDirCreateSource("testdata", undefined, {
63 | toJavaScript: true,
64 | });
65 | assertStringIncludes(source, "const dirData = {};");
66 | });
67 |
68 | Deno.test("readDirCreateSource - with basic auth", async () => {
69 | await assertThrowsAsync(
70 | async () => {
71 | await readDirCreateSource("testdata", undefined, {
72 | basicAuth: "user-pw",
73 | });
74 | },
75 | Error,
76 | "Invalid form of basic auth creadentials: user-pw",
77 | );
78 | });
79 |
80 | Deno.test("readDirCreateSource - with basic auth", async () => {
81 | const source = await readDirCreateSource("testdata", undefined, {
82 | basicAuth: "user:pw",
83 | });
84 | assertStringIncludes(
85 | source,
86 | `import { basicAuth } from "https://deno.land/x/basic_auth@v1.0.0/mod.ts";`,
87 | );
88 | assertStringIncludes(
89 | source,
90 | `
91 | const unauthorized = basicAuth(e.request, "Access to the site", {"user":"pw"});
92 | if (unauthorized) {
93 | e.respondWith(unauthorized);
94 | return;
95 | }`.trim(),
96 | );
97 | });
98 |
99 | Deno.test("readDirCreateSource with root", async () => {
100 | assertStringIncludes(
101 | await readDirCreateSource("testdata", "/root", { gzipTimestamp: 0 }),
102 | `
103 | dirData["/root/bar.ts"] = [decode("H4sIAAAAAAAAA0vOzyvOz0nVy8lP11BKSixS0rTmAgCz8kN9FAAAAA=="), "text/typescript", '"aa9bb63fe50cf09a95776fc7dbbd5eb7"', "private"];
104 | dirData["/root/foo.txt"] = [decode("H4sIAAAAAAAAA0vLz+cCAKhlMn4EAAAA"), "text/plain", '"4ebd3923247dff92a9b80f2c1ff1caee"', "private"];
105 | dirData["/root/index.html"] = [decode("H4sIAAAAAAAAA/NIzcnJV+QCAJ7YQrAHAAAA"), "text/html", '"275c264be2a95c29ac07e6f63e3d016c"', "private"];
106 | `.trim(),
107 | );
108 | });
109 |
110 | Deno.test("readDirCreateSource with root 2", async () => {
111 | assertStringIncludes(
112 | await readDirCreateSource("testdata", "root", { gzipTimestamp: 0 }),
113 | `
114 | dirData["/root/bar.ts"] = [decode("H4sIAAAAAAAAA0vOzyvOz0nVy8lP11BKSixS0rTmAgCz8kN9FAAAAA=="), "text/typescript", '"aa9bb63fe50cf09a95776fc7dbbd5eb7"', "private"];
115 | dirData["/root/foo.txt"] = [decode("H4sIAAAAAAAAA0vLz+cCAKhlMn4EAAAA"), "text/plain", '"4ebd3923247dff92a9b80f2c1ff1caee"', "private"];
116 | dirData["/root/index.html"] = [decode("H4sIAAAAAAAAA/NIzcnJV+QCAJ7YQrAHAAAA"), "text/html", '"275c264be2a95c29ac07e6f63e3d016c"', "private"];
117 | `.trim(),
118 | );
119 | });
120 |
121 | Deno.test("getMediaType", () => {
122 | assertEquals(getMediaType("README.md"), "text/markdown");
123 | assertEquals(getMediaType("index.html"), "text/html");
124 | assertEquals(getMediaType("inde.htm"), "text/html");
125 | assertEquals(getMediaType("package.json"), "application/json");
126 | assertEquals(getMediaType("image.jpg"), "image/jpeg");
127 | assertEquals(getMediaType("image.jpeg"), "image/jpeg");
128 | assertEquals(getMediaType("image.avif"), "image/avif");
129 | assertEquals(getMediaType("image.webp"), "image/webp");
130 | assertEquals(getMediaType("image.png"), "image/png");
131 | assertEquals(getMediaType("image.gif"), "image/gif");
132 | assertEquals(getMediaType("foo.txt"), "text/plain");
133 | assertEquals(getMediaType("foo.ts"), "text/typescript");
134 | assertEquals(getMediaType("Component.tsx"), "text/tsx");
135 | assertEquals(getMediaType("script.js"), "application/javascript");
136 | assertEquals(getMediaType("Component.jsx"), "text/jsx");
137 | assertEquals(getMediaType("archive.tar.gz"), "application/gzip");
138 | assertEquals(getMediaType("style.css"), "text/css");
139 | assertEquals(getMediaType("lib.wasm"), "application/wasm");
140 | assertEquals(getMediaType("mod.mjs"), "application/javascript");
141 | assertEquals(getMediaType("logo.svg"), "image/svg+xml");
142 | });
143 |
--------------------------------------------------------------------------------
/testdata/bar.ts:
--------------------------------------------------------------------------------
1 | console.log("bar");
2 |
--------------------------------------------------------------------------------
/testdata/foo.txt:
--------------------------------------------------------------------------------
1 | foo
2 |
--------------------------------------------------------------------------------
/testdata/index.html:
--------------------------------------------------------------------------------
1 | Hello!
2 |
--------------------------------------------------------------------------------
/util.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Parses the cache option string to Record
3 | */
4 | export function parseCacheOption(opts: string): Record {
5 | return Object.fromEntries(
6 | opts.split(/\s*,\s*/).map((opt) => {
7 | const [k, v] = opt.split(":");
8 | if (!k || !v) {
9 | throw new Error(`Invalid --cache option: ${opts}`);
10 | }
11 | return [k, v];
12 | }),
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/util_test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals } from "https://deno.land/std@0.99.0/testing/asserts.ts";
2 | import { parseCacheOption } from "./util.ts";
3 |
4 | Deno.test("parseCacheOption", () => {
5 | assertEquals(parseCacheOption("/css:max-age=3600,/img:max-age=86400"), {
6 | "/css": "max-age=3600",
7 | "/img": "max-age=86400",
8 | });
9 | });
10 |
--------------------------------------------------------------------------------