From 0a9301037efb60a003861974087b2cb7e5efbba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Tue, 7 Sep 2021 00:16:35 +0200 Subject: [PATCH] Initial release --- .editorconfig | 9 +++ .gitignore | 5 ++ README.md | 10 ++++ TODO.md | 4 ++ cli.ts | 6 ++ config/run.flags | 1 + config/test.flags | 1 + deps.testing.ts | 5 ++ deps.ts | 9 +++ doc/about.md | 6 ++ doc/index | 1 + run | 19 +++++++ src/server.test.ts | 8 +++ src/server.ts | 133 +++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 10 ++++ web-check.ts | 3 + web/index.html | 16 ++++++ 17 files changed, 246 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 README.md create mode 100644 TODO.md create mode 100755 cli.ts create mode 100644 config/run.flags create mode 100644 config/test.flags create mode 100644 deps.testing.ts create mode 100644 deps.ts create mode 100644 doc/about.md create mode 100644 doc/index create mode 100755 run create mode 100644 src/server.test.ts create mode 100644 src/server.ts create mode 100644 tsconfig.json create mode 100644 web-check.ts create mode 100644 web/index.html diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..83c1115 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.{ts,json}] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b7abdc --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +deno.d.ts +.vscode +.local +.output +web/*.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..24015b6 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# typescript/devserver + +[![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/devserver?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=devserver) + +## About + +Web development server for other typescript projects. + +This utility serves the local `web` directory, bundling requested js files on +the fly from sources. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..76c13c5 --- /dev/null +++ b/TODO.md @@ -0,0 +1,4 @@ +# TODO + +- Cache +- Autoreload diff --git a/cli.ts b/cli.ts new file mode 100755 index 0000000..b88852d --- /dev/null +++ b/cli.ts @@ -0,0 +1,6 @@ +#!./run +import { runDevServer } from "./src/server.ts"; + +if (import.meta.main) { + await runDevServer(); +} diff --git a/config/run.flags b/config/run.flags new file mode 100644 index 0000000..66387f6 --- /dev/null +++ b/config/run.flags @@ -0,0 +1 @@ +--allow-net=0.0.0.0 --allow-read=. --allow-run=deno \ No newline at end of file diff --git a/config/test.flags b/config/test.flags new file mode 100644 index 0000000..794cfc4 --- /dev/null +++ b/config/test.flags @@ -0,0 +1 @@ +--allow-read=. \ No newline at end of file diff --git a/deps.testing.ts b/deps.testing.ts new file mode 100644 index 0000000..769812b --- /dev/null +++ b/deps.testing.ts @@ -0,0 +1,5 @@ +export { + describe, + expect, + it, +} from "https://js.thunderk.net/testing@1.0.0/mod.ts"; diff --git a/deps.ts b/deps.ts new file mode 100644 index 0000000..04897b8 --- /dev/null +++ b/deps.ts @@ -0,0 +1,9 @@ +export { Sys } from "https://js.thunderk.net/system@1.0.0/mod.ts"; + +export { + listenAndServe, + ServerRequest, +} from "https://deno.land/std@0.106.0/http/server.ts"; +export type { Response as ServerResponse } from "https://deno.land/std@0.106.0/http/server.ts"; +export { serveFile } from "https://deno.land/std@0.106.0/http/file_server.ts"; +export { posix } from "https://deno.land/std@0.106.0/path/mod.ts"; diff --git a/doc/about.md b/doc/about.md new file mode 100644 index 0000000..b3021df --- /dev/null +++ b/doc/about.md @@ -0,0 +1,6 @@ +## About + +Web development server for other typescript projects. + +This utility serves the local `web` directory, bundling requested js files on +the fly from sources. diff --git a/doc/index b/doc/index new file mode 100644 index 0000000..93caaca --- /dev/null +++ b/doc/index @@ -0,0 +1 @@ +about \ No newline at end of file diff --git a/run b/run new file mode 100755 index 0000000..74d1c6d --- /dev/null +++ b/run @@ -0,0 +1,19 @@ +#!/bin/sh +# Simplified run tool for deno commands + +if test $# -eq 0 +then + echo "Usage: $0 [file or command]" + exit 1 +elif echo $1 | grep -q '.*.ts' +then + denocmd=run + denoargs=$1 + shift +else + denocmd=$1 + shift +fi + +denoargs="$(cat config/$denocmd.flags 2> /dev/null) $denoargs $@" +exec deno $denocmd $denoargs diff --git a/src/server.test.ts b/src/server.test.ts new file mode 100644 index 0000000..2087b80 --- /dev/null +++ b/src/server.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from "../deps.testing.ts"; +import { normalizeURL } from "./server.ts"; + +describe("server", () => { + it("normalizes URLs", () => { + expect(normalizeURL("/dir/file.txt?t=1")).toEqual("/dir/file.txt"); + }); +}); diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..4a14593 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,133 @@ +// Adapted from https://deno.land/std@0.106.0/http/file_server.ts +import { + listenAndServe, + posix, + serveFile, + ServerRequest, + ServerResponse, + Sys, +} from "../deps.ts"; + +export function runDevServer(): void { + const target = posix.resolve("./web/"); + + const handler = async (req: ServerRequest) => { + let response: ServerResponse | undefined; + try { + const normalizedUrl = normalizeURL(req.url); + let fsPath = posix.join(target, normalizedUrl); + if (fsPath.indexOf(target) !== 0) { + fsPath = target; + } + + const srcPath = fsPath.replace(/\/web\/(.*)\.js$/, "/$1.ts"); + if (srcPath != fsPath) { + // TODO check only local + await bundle(srcPath, fsPath); + } + + const fileInfo = await Deno.stat(fsPath); + if (fileInfo.isDirectory) { + response = await serveFile(req, posix.join(fsPath, "index.html")); + } else { + response = await serveFile(req, fsPath); + } + } catch (e) { + console.error(e.message); + response = await serveFallback(req, e); + } finally { + serverLog(req, response!); + try { + await req.respond(response!); + } catch (e) { + console.error(e.message); + } + } + }; + + const port = 4507; + const host = "0.0.0.0"; + const addr = `${host}:${port}`; + + listenAndServe(addr, handler); + console.log(`Dev server listening on http://${addr}/`); +} + +export function normalizeURL(url: string): string { + let normalizedUrl = url; + try { + normalizedUrl = decodeURI(normalizedUrl); + } catch (e) { + if (!(e instanceof URIError)) { + throw e; + } + } + + try { + //allowed per https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html + const absoluteURI = new URL(normalizedUrl); + normalizedUrl = absoluteURI.pathname; + } catch (e) { //wasn't an absoluteURI + if (!(e instanceof TypeError)) { + throw e; + } + } + + if (normalizedUrl[0] !== "/") { + throw new URIError("The request URI is malformed."); + } + + normalizedUrl = posix.normalize(normalizedUrl); + const startOfParams = normalizedUrl.indexOf("?"); + return startOfParams > -1 + ? normalizedUrl.slice(0, startOfParams) + : normalizedUrl; +} + +const encoder = new TextEncoder(); +function serveFallback(_req: ServerRequest, e: Error): Promise { + if (e instanceof URIError) { + return Promise.resolve({ + status: 400, + body: encoder.encode("Bad Request"), + }); + } else if (e instanceof Deno.errors.NotFound) { + return Promise.resolve({ + status: 404, + body: encoder.encode("Not Found"), + }); + } else { + return Promise.resolve({ + status: 500, + body: encoder.encode("Internal server error"), + }); + } +} + +function serverLog(req: ServerRequest, res: ServerResponse): void { + const d = new Date().toISOString(); + const dateFmt = `[${d.slice(0, 10)} ${d.slice(11, 19)}]`; + const s = `${dateFmt} "${req.method} ${req.url} ${req.proto}" ${res.status}`; + console.log(s); +} + +async function run(...cmd: string[]): Promise { + const process = Sys.run({ cmd }); + const result = await process.status(); + return result.success; +} + +async function bundle( + source: string, + destination: string, +): Promise { + // TODO add config flags + if ( + await run("deno", "bundle", source, destination) && + await run("deno", "fmt", destination) + ) { + return; + } else { + console.log(`Bundle failed for: ${source}`); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e28737f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "ESNext", + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "preserveConstEnums": true + } +} diff --git a/web-check.ts b/web-check.ts new file mode 100644 index 0000000..092351e --- /dev/null +++ b/web-check.ts @@ -0,0 +1,3 @@ +export function check() { + alert("All good!"); +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..27b661c --- /dev/null +++ b/web/index.html @@ -0,0 +1,16 @@ + + + + + + + + +

Test

+

Should get an alert...

+ + + \ No newline at end of file