Skip to content

Commit

Permalink
v3
Browse files Browse the repository at this point in the history
  • Loading branch information
ayame113 committed Apr 12, 2023
1 parent b929b42 commit 3b31413
Show file tree
Hide file tree
Showing 21 changed files with 521 additions and 31 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
# ts-serve
# jit-server

[![Test](https://github.com/ayame113/ts-serve/actions/workflows/test.yml/badge.svg)](https://github.com/ayame113/ts-serve/actions/workflows/test.yml)
[![codecov](https://codecov.io/gh/ayame113/ts-serve/branch/main/graph/badge.svg?token=mz0SfmUYRL)](https://codecov.io/gh/ayame113/ts-serve)

**jit-server** is a file server with just-in-time compilation.

> **Note** Up until version 2, this library was a specialized server for
> transpiling TypeScript to JavaScript, called ts-serve.
# ts-serve

TypeScript + ES Modules

Transpile TypeScript on the fly and serve it from your server as ES Modules.
Expand Down
49 changes: 47 additions & 2 deletions example/serve.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,48 @@
import { serve } from "https://deno.land/std@0.178.0/http/mod.ts";
import { serveDirWithTs } from "../mod.ts";
serve((req) => serveDirWithTs(req, { fsRoot: "example" }));
import { App } from "../mod.ts";
import { tsServe } from "../middlewear/ts-serve.ts";
import { markdown } from "../middlewear/gfm.ts";
// import { webpConverter } from "../middlewear/webp.ts";
// import basicAuth from "https://deno.land/x/lume@v1.15.3/middlewares/basic_auth.ts";

const app = new App();

// middleware
app
// .use(basicAuth({
// users: {
// "user": "pass",
// },
// }))
.use(tsServe())
// .use(webpConverter())
.use(markdown({
renderOptions: { disableHtmlSanitization: true },
frontMatter: true,
format(body, { CSS }, frontMatter) {
console.log(frontMatter);
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
main {
max-width: 800px;
margin: 0 auto;
}
${CSS}
</style>
</head>
<body>
<main data-color-mode="light" data-light-theme="light" data-dark-theme="dark" class="markdown-body">
${body}
</main>
</body>
</html>
`;
},
}));

serve(app.handler);
3 changes: 3 additions & 0 deletions example/serve_old.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { serve } from "https://deno.land/std@0.178.0/http/mod.ts";
import { serveDirWithTs } from "../mod.ts";
serve((req) => serveDirWithTs(req, { fsRoot: "example" }));
6 changes: 6 additions & 0 deletions middlewear/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## middlewares

This directory contains middleware that can be used in conjunction with the
`App` class exported from `/mod.ts`.

A detailed usage example is in [../example/serve.ts](../example/serve.ts).
57 changes: 57 additions & 0 deletions middlewear/_webp_encode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// https://github.com/jamsinclair/jSquash/blob/1edfc086e22b6aa01910cff5fd20826cf9e3dfa2/packages/webp/encode.ts
// avoid top-level-await for deno deploy

/**
* Copyright 2020 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Notice: I (Jamie Sinclair) have modified this file.
* Updated to support a partial subset of WebP encoding options to be provided.
* The WebP options are defaulted to defaults from the meta.ts file.
* Also manually allow instantiation of the Wasm Module.
*/
import type { WebPModule } from "https://esm.sh/@jsquash/webp@1.1.3/codec/enc/webp_enc";
import type { EncodeOptions } from "https://esm.sh/@jsquash/webp@1.1.3/meta";

import { defaultOptions } from "https://esm.sh/@jsquash/webp@1.1.3/meta";
import { initEmscriptenModule } from "https://esm.sh/@jsquash/webp@1.1.3/utils";
import { simd } from "https://esm.sh/wasm-feature-detect@1.5.0";

import webpEncoder from "https://esm.sh/@jsquash/webp@1.1.3/codec/enc/webp_enc";
import webpEncoderSimd from "https://esm.sh/@jsquash/webp@1.1.3/codec/enc/webp_enc_simd";

let emscriptenModule: Promise<WebPModule>;

export async function init(module?: WebAssembly.Module): Promise<WebPModule> {
if (await simd()) {
emscriptenModule = initEmscriptenModule(webpEncoderSimd, module);
return emscriptenModule;
}
emscriptenModule = initEmscriptenModule(webpEncoder, module);
return emscriptenModule;
}

export default async function encode(
data: ImageData,
options: Partial<EncodeOptions> = {},
): Promise<ArrayBuffer> {
if (!emscriptenModule) emscriptenModule = init();

const _options: EncodeOptions = { ...defaultOptions, ...options };
const module = await emscriptenModule;
const result = module.encode(data.data, data.width, data.height, _options);

if (!result) throw new Error("Encoding error.");

return result.buffer;
}
59 changes: 59 additions & 0 deletions middlewear/_webp_lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import decodeJpeg, {
init as initJpegWasm,
} from "https://esm.sh/@jsquash/jpeg@1.1.5/decode";
import decodePng, {
init as initPngWasm,
} from "https://esm.sh/@jsquash/png@2.0.0/decode";
// import encodeWebp, {
// init as initWebpWasm,
// } from "https://esm.sh/@jsquash/webp@1.1.3/encode";
import encodeWebp, { init as initWebpWasm } from "./_webp_encode.ts";

const jpegWasm =
"https://esm.sh/@jsquash/jpeg@1.1.5/codec/dec/mozjpeg_dec.wasm";
const pngWasm = "https://esm.sh/@jsquash/png@2.0.0/codec/squoosh_png_bg.wasm";
const webpWasm =
"https://esm.sh/@jsquash/webp@1.1.3/codec/enc/webp_enc_simd.wasm";

async function loadWasmModule(url: string) {
return await WebAssembly.compileStreaming(fetch(url));
}

export const jpegWasmInit = loadWasmModule(jpegWasm).then(initJpegWasm);
export const pngWasmInit = loadWasmModule(pngWasm).then(initPngWasm);
export const webpWasmInit = loadWasmModule(webpWasm).then(initWebpWasm);

globalThis.ImageData ??= class ImageData {
colorSpace = "srgb" as const;
data: Uint8ClampedArray;
width: number;
height: number;
constructor(data: Uint8ClampedArray, width: number, height?: number);
constructor(data: number, width: number);
constructor(
data: Uint8ClampedArray | number,
width: number,
height?: number,
) {
if (!(data instanceof Uint8ClampedArray) || typeof height !== "number") {
throw new Error("unimplemented");
}
this.data = data;
this.width = width;
this.height = height;
}
};

export async function jpegToWebp(buf: ArrayBuffer) {
await jpegWasmInit;
const imageData = await decodeJpeg(buf);
await webpWasmInit;
return await encodeWebp(imageData);
}

export async function pngToWebp(buf: ArrayBuffer) {
await pngWasmInit;
const imageData = await decodePng(buf);
await webpWasmInit;
return await encodeWebp(imageData);
}
55 changes: 55 additions & 0 deletions middlewear/gfm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createTranspiler } from "../mod.ts";
import {
CSS,
KATEX_CSS,
render,
type RenderOptions,
} from "https://deno.land/x/gfm@0.2.0/mod.ts";
import {
extract,
test,
} from "https://deno.land/std@0.176.0/encoding/front_matter/any.ts";
import type { JSONValue } from "https://deno.land/std@0.176.0/encoding/jsonc.ts";

export interface MarkdownOptions {
/** Transpile only if the file name matches this value (format follows URLPattern). */
targetDir?: string;
/** If the file name matches this value, it will not be transpiled (format follows URLPattern). */
excludeDir?: string;
/** whether to parse frontMatter. If set to true, the parsed frontMatter is given to the argument of the format function. */
frontMatter?: boolean;
/** A function that creates full HTML from parsed markdown body. */
format(
body: string,
css: { CSS: string; KATEX_CSS: string },
frontMatter: JSONValue,
): string | Promise<string>;
/** Options passed to [deno.land/x/gfm](https://deno.land/x/gfm)'s render function */
renderOptions?: RenderOptions;
}

/** Middleware for converting markdown to HTML using [deno.land/x/gfm](https://deno.land/x/gfm) . */
export function markdown(options: MarkdownOptions) {
const {
targetDir,
excludeDir,
frontMatter,
format,
renderOptions,
} = options;

return createTranspiler({
from: ".md",
to: ".html",
targetDir,
excludeDir,
fn(content) {
let attrs: JSONValue = {};
if (frontMatter && test(content)) {
({ body: content, attrs } = extract(content));
}
const body = render(content, renderOptions);
return format(body, { CSS, KATEX_CSS }, attrs);
},
});
}
35 changes: 35 additions & 0 deletions middlewear/ts-serve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createTranspiler } from "../src/app/mod.ts";
import { fourceInstantiateWasm } from "../src/utils/fource_instantiate_wasm.ts";
import { MediaType, transpile } from "../src/utils/transpile.ts";

export interface TsServeOptions {
/** Transpile only if the file name matches this value (format follows URLPattern). */
targetDir?: string;
/** If the file name matches this value, it will not be transpiled (format follows URLPattern). */
excludeDir?: string;
}

/**
* A Promise that resolves when the wasm files used internally by this library are initialized.
* Normally you wouldn't use this variable, but if a test gives an error that it's leaking an asynchronous resource, awaiting this promise before running this test might solve the problem.
*/
export const denoEmitWasmInitPromise = fourceInstantiateWasm();

/** Middleware that transpiles TypeScript to JavaScript. */
export function tsServe({ targetDir, excludeDir }: TsServeOptions = {}) {
return createTranspiler({
from: [".jsx", ".tsx", ".ts"],
to: ".js",
targetDir,
excludeDir,
fn(content, { ctx, request }) {
const url = new URL(request.url);
const mediaType = {
".ts": MediaType.TypeScript,
".jsx": MediaType.Jsx,
".tsx": MediaType.Tsx,
}[ctx.type];
return transpile(content, url, mediaType);
},
});
}
32 changes: 32 additions & 0 deletions middlewear/webp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createTranspiler } from "../mod.ts";
import { jpegToWebp, pngToWebp } from "./_webp_lib.ts";

export interface WebpConverterOptions {
/** Transpile only if the file name matches this value (format follows URLPattern). */
targetDir?: string;
/** If the file name matches this value, it will not be transpiled (format follows URLPattern). */
excludeDir?: string;
}

/** Middleware to transpile images to webp. */
export function webpConverter(
{ targetDir, excludeDir }: WebpConverterOptions = {},
) {
return createTranspiler({
from: [".jpg", ".jpeg", ".png"],
to: ".webp",
type: "arrayBuffer",
targetDir,
excludeDir,
async fn(content, { ctx, request }) {
if (!request.headers.get("accept")?.includes("image/webp")) {
throw new Error("Accept header does not contain image/webp.");
}
if (ctx.type === ".png") {
return new Uint8Array(await pngToWebp(content));
} else {
return new Uint8Array(await jpegToWebp(content));
}
},
});
}
26 changes: 3 additions & 23 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,5 @@
export * from "./src/oak.ts";
export * from "./src/file_server.ts";
export * from "./utils/transpile.ts";
import { MediaType, transpile } from "./utils/transpile.ts";

/**
* Calling this function will load the wasm file used in the deno_emit of the dependency.
* Even if you don't call this function, if you call the transpile function, the wasm file will be read automatically at that timing.
* However, performance can be an issue on the server as loading the wasm file takes time.
* In that case, calling this function in advance can speed up later calls to the transpile function.
*
* ```ts
* import { serve } from "https://deno.land/std@0.178.0/http/mod.ts";
* import { serveDirWithTs, fourceInstantiateWasm } from "https://deno.land/x/ts_serve@$MODULE_VERSION/mod.ts";
*
* // load the wasm file in the background when the server starts.
* fourceInstantiateWasm();
* serve((request) => serveDirWithTs(request));
* ```
*/
export async function fourceInstantiateWasm() {
try {
await transpile("", new URL("file:///src"), MediaType.TypeScript);
} catch (_) { /* ignore error*/ }
}
export * from "./src/utils/transpile.ts";
export * from "./src/utils/fource_instantiate_wasm.ts";
export * from "./src/app/mod.ts";

0 comments on commit 3b31413

Please sign in to comment.