├── .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 | [![ci](https://github.com/kt3k/deploy_dir/actions/workflows/ci.yml/badge.svg)](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 | --------------------------------------------------------------------------------