Skip to content

Commit

Permalink
feat: fourceInstantiateWasm (#16)
Browse files Browse the repository at this point in the history
* feat: fourceInstantiateWasm

* update jsdoc

* fix test

* add coverage

* fix lint error
  • Loading branch information
ayame113 committed Aug 20, 2022
1 parent afaf9e3 commit 9b7d62b
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 163 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,23 @@ import { serveFileWithTs } from "https://deno.land/x/ts_serve@$VERSION/mod.ts";
serve((request) => serveFileWithTs(request, "./mod.ts"));
```

### fourceInstantiateWasm function

Optionally, calling the `fourceInstantiateWasm` function before starting the
server will force the wasm file to be read ahead. Otherwise the wasm file will
take about 3 seconds to load the first time it is transpiled.

```ts
import { serve } from "https://deno.land/std@0.144.0/http/mod.ts";
import {
fourceInstantiateWasm,
serveDirWithTs,
} from "https://deno.land/x/ts_serve@$VERSION/mod.ts";

fourceInstantiateWasm();
serve((request) => serveDirWithTs(request));
```

## develop

```shell
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"tasks": {
"test": "deno test --doc --parallel --shuffle --allow-net --allow-read=.",
"test": "deno test --doc --parallel --shuffle --allow-net=deno.land,localhost,0.0.0.0 --allow-read=.",
"check": "deno check ./mod.ts"
},
"importMap": "./import-map.json"
Expand Down
175 changes: 13 additions & 162 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,173 +1,24 @@
import {
serveDir,
type ServeDirOptions,
serveFile,
type ServeFileOptions,
} from "https://deno.land/std@0.151.0/http/file_server.ts";
import { contentType } from "https://deno.land/std@0.151.0/media_types/mod.ts";
import type { Context } from "https://deno.land/x/oak@v10.6.0/mod.ts";
import { convertBodyToBodyInit } from "https://deno.land/x/oak@v10.6.0/response.ts";
import { MediaType, transpile } from "./utils/transpile.ts";

const decoder = new TextDecoder();
const tsType = new Set<string | undefined>(
["ts", ".ts", "mts", ".mts", "video/mp2t"],
);
const tsxType = new Set<string | undefined>(["tsx", ".tsx"]);
const jsxType = new Set<string | undefined>(["jsx", ".jsx", "text/jsx"]);
const jsContentType = contentType(".js")!;
export * from "./src/oak.ts";
export * from "./src/file_server.ts";
export * from "./utils/transpile.ts";
import { transpile } from "./utils/transpile.ts";

/**
* This can be used in the same way as the [serveFile](https://doc.deno.land/https://deno.land/std@0.151.0/http/file_server.ts/~/serveFile) function of the standard library, but if the file is TypeScript, it will be rewritten to JavaScript.
* **Calling this function has no effect whether it is called or not.**
* Calling this function will force the loading of the wasm file used internally.
* For performance sensitive servers, etc., call this function first to tell it to load wasm.
* There is no need to call this function where performance is not important. In that case, the wasm file will be automatically loaded in about 3 seconds when you transpile for the first time.
*
* ```ts
* import { serve } from "https://deno.land/std@0.151.0/http/mod.ts";
* import { serveFileWithTs } from "https://deno.land/x/ts_serve@$VERSION/mod.ts";
*
* serve((request) => serveFileWithTs(request, "./mod.ts"));
* ```
*/
export async function serveFileWithTs(
request: Request,
filePath: string,
options?: ServeFileOptions,
): Promise<Response> {
const response = await serveFile(request, filePath, options);

let url;
try {
url = new URL(request.url, "file:///");
} catch {
url = new URL("file:///src");
}
// if range request, skip
if (response.status === 200) {
if (filePath.endsWith(".ts")) {
return rewriteTsResponse(response, url, MediaType.TypeScript);
} else if (filePath.endsWith(".tsx")) {
return rewriteTsResponse(response, url, MediaType.Tsx);
} else if (filePath.endsWith(".jsx")) {
return rewriteTsResponse(response, url, MediaType.Jsx);
}
}
return response;
}

/**
* This can be used in the same way as the [serveDir](https://doc.deno.land/https://deno.land/std@0.151.0/http/file_server.ts/~/serveDir) function of the standard library, but if the file is TypeScript, it will be rewritten to JavaScript.
*
* ```ts
* import { serve } from "https://deno.land/std@0.151.0/http/mod.ts";
* import { serveDirWithTs } from "https://deno.land/x/ts_serve@$VERSION/mod.ts";
* import { serveDirWithTs, fourceInstantiateWasm } from "https://deno.land/x/ts_serve@$VERSION/mod.ts";
*
* fourceInstantiateWasm();
* serve((request) => serveDirWithTs(request));
* ```
*/
export async function serveDirWithTs(
request: Request,
options?: ServeDirOptions,
): Promise<Response> {
const response = await serveDir(request, options);

let url;
export async function fourceInstantiateWasm() {
try {
url = new URL(request.url, "file:///");
} catch {
return response;
}
// if range request, skip
if (response.status === 200) {
if (url.pathname.endsWith(".ts")) {
return rewriteTsResponse(response, url);
} else if (url.pathname.endsWith(".tsx")) {
return rewriteTsResponse(response, url);
} else if (url.pathname.endsWith(".jsx")) {
return rewriteTsResponse(response, url);
}
}
return response;
}

async function rewriteTsResponse(
response: Response,
url: URL,
mediaType?: MediaType,
) {
const tsCode = await response.text();
const jsCode = await transpile(tsCode, url, mediaType);
const { headers } = response;
headers.set("content-type", jsContentType);
headers.delete("content-length");

return new Response(jsCode, {
status: response.status,
statusText: response.statusText,
headers,
});
await transpile("", new URL("file:///src"));
} catch (_) { /* ignore error*/ }
}

/**
* Oak middleware that rewrites TypeScript response to JavaScript response.
*
* ```ts
* import { Application } from "https://deno.land/x/oak@v10.6.0/mod.ts";
* import { tsMiddleware } from "https://deno.land/x/ts_serve@$VERSION/mod.ts";
*
* const app = new Application();
*
* // use middleware and transpile TS code
* app.use(tsMiddleware);
*
* // serve static file
* app.use(async (ctx, next) => {
* try {
* await ctx.send({ root: "./" });
* } catch {
* await next();
* }
* });
* await app.listen({ port: 8000 });
* ```
*/
export async function tsMiddleware(
ctx: Context,
next: () => Promise<unknown>,
) {
await next();
const mediaType = tsType.has(ctx.response.type)
? MediaType.TypeScript
: tsxType.has(ctx.response.type)
? MediaType.Tsx
: jsxType.has(ctx.response.type)
? MediaType.Jsx
: undefined;

if (mediaType == undefined) {
return;
}

const specifier = ctx.request.url;

if (ctx.response.body == null) {
// skip
} else if (typeof ctx.response.body === "string") {
// major fast path
const tsCode = ctx.response.body;
const jsCode = await transpile(tsCode, specifier, mediaType);
ctx.response.body = jsCode;
} else if (ctx.response.body instanceof Uint8Array) {
// major fast path
const tsCode = decoder.decode(ctx.response.body);
const jsCode = await transpile(tsCode, specifier, mediaType);
ctx.response.body = jsCode;
} else {
// fallback
const [responseInit] = await convertBodyToBodyInit(ctx.response.body);
const tsCode = await new Response(responseInit).text();
const jsCode = await transpile(tsCode, specifier, mediaType);
ctx.response.body = jsCode;
}
ctx.response.type = ".js";
}

export { MediaType, type ServeDirOptions, type ServeFileOptions, transpile };
41 changes: 41 additions & 0 deletions mod_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { assert } from "https://deno.land/std@0.152.0/testing/asserts.ts";
import {
assertSpyCalls,
stub,
} from "https://deno.land/std@0.152.0/testing/mock.ts";

import { fourceInstantiateWasm, transpile } from "./mod.ts";

Deno.test({
name: "fourceInstantiateWasm",
async fn() {
await fourceInstantiateWasm();
const start = Date.now();
await transpile(
"function foo(arg: string): string {return arg}",
new URL("file:///src.ts"),
);
const time = Date.now() - start;
assert(time < 100, `transpile() took ${time} ms`);
},
});

Deno.test({
name: "fourceInstantiateWasm - failed to load wasm",
async fn() {
// Don't throw an error when transpile() throws
const fetchStub = stub(
URL.prototype,
"toString",
() => {
throw new Error("load fail!!");
},
);
try {
await fourceInstantiateWasm();
assertSpyCalls(fetchStub, 1);
} finally {
fetchStub.restore();
}
},
});
103 changes: 103 additions & 0 deletions src/file_server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {
serveDir,
type ServeDirOptions,
serveFile,
type ServeFileOptions,
} from "https://deno.land/std@0.151.0/http/file_server.ts";
import { contentType } from "https://deno.land/std@0.151.0/media_types/mod.ts";
import { MediaType, transpile } from "../utils/transpile.ts";

const jsContentType = contentType(".js");

/**
* This can be used in the same way as the [serveFile](https://doc.deno.land/https://deno.land/std@0.151.0/http/file_server.ts/~/serveFile) function of the standard library, but if the file is TypeScript, it will be rewritten to JavaScript.
*
* ```ts
* import { serve } from "https://deno.land/std@0.151.0/http/mod.ts";
* import { serveFileWithTs, fourceInstantiateWasm } from "https://deno.land/x/ts_serve@$VERSION/mod.ts";
*
* fourceInstantiateWasm();
* serve((request) => serveFileWithTs(request, "./mod.ts"));
* ```
*/
export async function serveFileWithTs(
request: Request,
filePath: string,
options?: ServeFileOptions,
): Promise<Response> {
const response = await serveFile(request, filePath, options);

let url;
try {
url = new URL(request.url, "file:///");
} catch {
url = new URL("file:///src");
}
// if range request, skip
if (response.status === 200) {
if (filePath.endsWith(".ts")) {
return rewriteTsResponse(response, url, MediaType.TypeScript);
} else if (filePath.endsWith(".tsx")) {
return rewriteTsResponse(response, url, MediaType.Tsx);
} else if (filePath.endsWith(".jsx")) {
return rewriteTsResponse(response, url, MediaType.Jsx);
}
}
return response;
}

/**
* This can be used in the same way as the [serveDir](https://doc.deno.land/https://deno.land/std@0.151.0/http/file_server.ts/~/serveDir) function of the standard library, but if the file is TypeScript, it will be rewritten to JavaScript.
*
* ```ts
* import { serve } from "https://deno.land/std@0.151.0/http/mod.ts";
* import { serveDirWithTs, fourceInstantiateWasm } from "https://deno.land/x/ts_serve@$VERSION/mod.ts";
*
* fourceInstantiateWasm();
* serve((request) => serveDirWithTs(request));
* ```
*/
export async function serveDirWithTs(
request: Request,
options?: ServeDirOptions,
): Promise<Response> {
const response = await serveDir(request, options);

let url;
try {
url = new URL(request.url, "file:///");
} catch {
return response;
}
// if range request, skip
if (response.status === 200) {
if (url.pathname.endsWith(".ts")) {
return rewriteTsResponse(response, url);
} else if (url.pathname.endsWith(".tsx")) {
return rewriteTsResponse(response, url);
} else if (url.pathname.endsWith(".jsx")) {
return rewriteTsResponse(response, url);
}
}
return response;
}

async function rewriteTsResponse(
response: Response,
url: URL,
mediaType?: MediaType,
) {
const tsCode = await response.text();
const jsCode = await transpile(tsCode, url, mediaType);
const { headers } = response;
headers.set("content-type", jsContentType);
headers.delete("content-length");

return new Response(jsCode, {
status: response.status,
statusText: response.statusText,
headers,
});
}

export { type ServeDirOptions, type ServeFileOptions };

0 comments on commit 9b7d62b

Please sign in to comment.