Skip to content

Commit

Permalink
feat: implove soure map url (#14)
Browse files Browse the repository at this point in the history
* feat: soure map url

* update test

* add JSDoc

* add comment
  • Loading branch information
ayame113 committed Aug 20, 2022
1 parent 988398d commit afaf9e3
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 69 deletions.
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 --jobs --shuffle --allow-net --allow-read=.",
"test": "deno test --doc --parallel --shuffle --allow-net --allow-read=.",
"check": "deno check ./mod.ts"
},
"importMap": "./import-map.json"
Expand Down
46 changes: 38 additions & 8 deletions file_server_test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { assertEquals } from "https://deno.land/std@0.151.0/testing/asserts.ts";
import { serve } from "https://deno.land/std@0.151.0/http/mod.ts";
import { serveDirWithTs, serveFileWithTs, transpile } from "./mod.ts";
import {
MediaType,
serveDirWithTs,
serveFileWithTs,
transpile,
} from "./mod.ts";

Deno.test({
name: "file server - serveFileWithTs",
Expand Down Expand Up @@ -31,7 +36,7 @@ Deno.test({
await res.text(),
await transpile(
await Deno.readTextFile(new URL("./mod.ts", import.meta.url)),
new URL("file:///src.ts"),
new URL("http://localhost:8886/mod.ts"),
),
);
assertEquals(
Expand All @@ -45,7 +50,7 @@ Deno.test({
await res.text(),
await transpile(
await Deno.readTextFile(new URL("./test/a.tsx", import.meta.url)),
new URL("file:///src.tsx"),
new URL("http://localhost:8886/test/a.tsx"),
),
);
assertEquals(
Expand All @@ -59,7 +64,7 @@ Deno.test({
await res.text(),
await transpile(
await Deno.readTextFile(new URL("./test/a.jsx", import.meta.url)),
new URL("file:///src.jsx"),
new URL("http://localhost:8886/test/a.jsx"),
),
);
assertEquals(
Expand Down Expand Up @@ -91,6 +96,31 @@ Deno.test({
},
});

Deno.test({
name: "file server - serveFileWithTs (invalid url)",
async fn() {
const request = new Request("http://localhost/");
Object.defineProperty(request, "url", {
value: "http://",
});
// Determine file type from file path and don't give error
const res = await serveFileWithTs(request, "./mod.ts");
assertEquals(
await res.text(),
await transpile(
await Deno.readTextFile(new URL("./mod.ts", import.meta.url)),
new URL("file:///src"),
MediaType.TypeScript,
),
);
assertEquals(res.status, 200);
assertEquals(
res.headers.get("Content-Type"),
"application/javascript; charset=UTF-8",
);
},
});

Deno.test({
name: "file server - serveDirWithTs",
async fn() {
Expand All @@ -110,7 +140,7 @@ Deno.test({
await res.text(),
await transpile(
await Deno.readTextFile(new URL("./mod.ts", import.meta.url)),
new URL("file:///src.ts"),
new URL("http://localhost:8887/mod.ts"),
),
);
assertEquals(
Expand All @@ -124,7 +154,7 @@ Deno.test({
await res.text(),
await transpile(
await Deno.readTextFile(new URL("./test/a.tsx", import.meta.url)),
new URL("file:///src.tsx"),
new URL("http://localhost:8887/test/a.tsx"),
),
);
assertEquals(
Expand All @@ -138,7 +168,7 @@ Deno.test({
await res.text(),
await transpile(
await Deno.readTextFile(new URL("./test/a.jsx", import.meta.url)),
new URL("file:///src.jsx"),
new URL("http://localhost:8887/test/a.jsx"),
),
);
assertEquals(
Expand Down Expand Up @@ -198,7 +228,7 @@ Deno.test({
await res.text(),
await transpile(
await Deno.readTextFile(new URL("./mod.ts", import.meta.url)),
new URL("file:///src.ts"),
new URL("file:///mod.ts"),
),
);
assertEquals(
Expand Down
103 changes: 58 additions & 45 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,14 @@ import {
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 { transpile } from "./utils/transpile.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 tsUrl = new URL("file:///src.ts");
const tsxUrl = new URL("file:///src.tsx");
const jsxUrl = new URL("file:///src.jsx");
const jsContentType = contentType(".js")!;

/**
Expand All @@ -36,14 +33,21 @@ export async function serveFileWithTs(
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, tsUrl);
return rewriteTsResponse(response, url, MediaType.TypeScript);
} else if (filePath.endsWith(".tsx")) {
return rewriteTsResponse(response, tsxUrl);
return rewriteTsResponse(response, url, MediaType.Tsx);
} else if (filePath.endsWith(".jsx")) {
return rewriteTsResponse(response, jsxUrl);
return rewriteTsResponse(response, url, MediaType.Jsx);
}
}
return response;
Expand All @@ -63,29 +67,34 @@ export async function serveDirWithTs(
request: Request,
options?: ServeDirOptions,
): Promise<Response> {
let pathname;
const response = await serveDir(request, options);

let url;
try {
pathname = new URL(request.url, "file:///").pathname;
url = new URL(request.url, "file:///");
} catch {
return await serveDir(request, options);
return response;
}
const response = await serveDir(request, options);
// if range request, skip
if (response.status === 200) {
if (pathname.endsWith(".ts")) {
return rewriteTsResponse(response, tsUrl);
} else if (pathname.endsWith(".tsx")) {
return rewriteTsResponse(response, tsxUrl);
} else if (pathname.endsWith(".jsx")) {
return rewriteTsResponse(response, jsxUrl);
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) {
async function rewriteTsResponse(
response: Response,
url: URL,
mediaType?: MediaType,
) {
const tsCode = await response.text();
const jsCode = await transpile(tsCode, url);
const jsCode = await transpile(tsCode, url, mediaType);
const { headers } = response;
headers.set("content-type", jsContentType);
headers.delete("content-length");
Expand Down Expand Up @@ -125,36 +134,40 @@ export async function tsMiddleware(
next: () => Promise<unknown>,
) {
await next();
const specifier = tsType.has(ctx.response.type)
? tsUrl
const mediaType = tsType.has(ctx.response.type)
? MediaType.TypeScript
: tsxType.has(ctx.response.type)
? tsxUrl
? MediaType.Tsx
: jsxType.has(ctx.response.type)
? jsxUrl
? MediaType.Jsx
: undefined;

if (specifier) {
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);
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);
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);
ctx.response.body = jsCode;
}
ctx.response.type = ".js";
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 { type ServeDirOptions, type ServeFileOptions, transpile };
export { MediaType, type ServeDirOptions, type ServeFileOptions, transpile };
14 changes: 5 additions & 9 deletions oak_test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { assertEquals } from "https://deno.land/std@0.151.0/testing/asserts.ts";
import { deferred } from "https://deno.land/std@0.151.0/async/mod.ts";
import { Application } from "https://deno.land/x/oak@v10.6.0/mod.ts";
import { transpile, tsMiddleware } from "./mod.ts";
import { MediaType, transpile, tsMiddleware } from "./mod.ts";

const port = 8888;
const jsContentType = "application/javascript; charset=utf-8";
Expand Down Expand Up @@ -41,11 +41,7 @@ async function readTextFile(path: string) {
return await Deno.readTextFile(new URL(path, import.meta.url));
}
async function transpileFile(path: string) {
const url = path.endsWith(".ts")
? new URL("file:///src.ts")
: path.endsWith(".tsx")
? new URL("file:///src.tsx")
: new URL("file:///src.jsx");
const url = new URL(path, `http://localhost:${port}`);
return await transpile(await readTextFile(path), url);
}

Expand Down Expand Up @@ -125,7 +121,7 @@ Deno.test({
const res = await app.handle(new Request("http://localhost/"));
assertEquals(
await res!.text(),
await transpile(code, new URL("file:///src.ts")),
await transpile(code, new URL("http://localhost/"), MediaType.TypeScript),
);
},
});
Expand All @@ -143,7 +139,7 @@ Deno.test({
const res = await app.handle(new Request("http://localhost/"));
assertEquals(
await res!.text(),
await transpile(code, new URL("file:///src.ts")),
await transpile(code, new URL("http://localhost/"), MediaType.TypeScript),
);
},
});
Expand All @@ -161,7 +157,7 @@ Deno.test({
const res = await app.handle(new Request("http://localhost/"));
assertEquals(
await res!.text(),
await transpile(code, new URL("file:///src.ts")),
await transpile(code, new URL("http://localhost/"), MediaType.TypeScript),
);
},
});
39 changes: 34 additions & 5 deletions utils/transpile.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,41 @@
import { emit } from "https://deno.land/x/emit@0.4.0/mod.ts";

/** File type. You can pass it as an option to the transpile function to tell it what media type the source is. */
export enum MediaType {
TypeScript,
Jsx,
Tsx,
}

// https://github.com/denoland/deno_ast/blob/ea1ccec37e1aa8e5e1e70f983a7ed1472d0e132a/src/media_type.rs#L117
const contentType = {
[MediaType.TypeScript]: "text/typescript; charset=utf-8",
[MediaType.Jsx]: "text/jsx; charset=utf-8",
[MediaType.Tsx]: "text/tsx; charset=utf-8",
};

/**
* Transpile the given TypeScript code into JavaScript code.
*
* @param content TypeScript code
* @param specifier URL like `new URL("file:///src.ts")` or `new URL("file:///src.tsx")`
* @param specifier The URL that will be used for the source map.
* @param mediaType Indicates whether the source code is TypeScript, JSX or TSX. If this argument is not passed, the file type is guessed using the extension of the URL passed as the second argument.
* @return JavaScript code
*
* ```ts
* import { transpile } from "https://deno.land/x/ts_serve@$VERSION/mod.ts";
* import { transpile, MediaType } from "https://deno.land/x/ts_serve@$VERSION/mod.ts";
* console.log(await transpile(
* "function name(params:type) {}",
* new URL("file:///src.ts")
* new URL("file:///src.ts"),
* MediaType.TypeScript,
* ));
* ```
*/
export async function transpile(content: string, specifier: URL) {
export async function transpile(
content: string,
specifier: URL,
mediaType?: MediaType,
) {
const urlStr = specifier.toString();
const result = await emit(specifier, {
load(specifier) {
Expand All @@ -27,7 +47,16 @@ export async function transpile(content: string, specifier: URL) {
headers: { "content-type": "application/javascript; charset=utf-8" },
});
}
return Promise.resolve({ kind: "module", specifier, content });
return Promise.resolve({
kind: "module",
specifier,
content,
headers: {
"content-type": mediaType != undefined
? contentType[mediaType]
: undefined!,
},
});
},
});
return result[urlStr];
Expand Down

0 comments on commit afaf9e3

Please sign in to comment.