Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2 #38

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft

v2 #38

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/[email protected]/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/[email protected]/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/[email protected]/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/[email protected]/codec/enc/webp_enc";
import type { EncodeOptions } from "https://esm.sh/@jsquash/[email protected]/meta";

import { defaultOptions } from "https://esm.sh/@jsquash/[email protected]/meta";
import { initEmscriptenModule } from "https://esm.sh/@jsquash/[email protected]/utils";
import { simd } from "https://esm.sh/[email protected]";

import webpEncoder from "https://esm.sh/@jsquash/[email protected]/codec/enc/webp_enc";
import webpEncoderSimd from "https://esm.sh/@jsquash/[email protected]/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/[email protected]/decode";
import decodePng, {
init as initPngWasm,
} from "https://esm.sh/@jsquash/[email protected]/decode";
// import encodeWebp, {
// init as initWebpWasm,
// } from "https://esm.sh/@jsquash/[email protected]/encode";
import encodeWebp, { init as initWebpWasm } from "./_webp_encode.ts";

const jpegWasm =
"https://esm.sh/@jsquash/[email protected]/codec/dec/mozjpeg_dec.wasm";
const pngWasm = "https://esm.sh/@jsquash/[email protected]/codec/squoosh_png_bg.wasm";
const webpWasm =
"https://esm.sh/@jsquash/[email protected]/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/[email protected]/mod.ts";
import {
extract,
test,
} from "https://deno.land/[email protected]/encoding/front_matter/any.ts";
import type { JSONValue } from "https://deno.land/[email protected]/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/[email protected]/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";
Loading