Allow to serve raw TS
This commit is contained in:
parent
1b37c64ef8
commit
45fb0df4b6
8 changed files with 206 additions and 78 deletions
22
README.md
22
README.md
|
@ -1,3 +1,25 @@
|
||||||
# typescript/bundler
|
# typescript/bundler
|
||||||
|
|
||||||
[![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/bundler?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=bundler)
|
[![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/bundler?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=bundler)
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
On-the-fly bundler for typescript libraries hosted on Gitea or compatible
|
||||||
|
|
||||||
|
## How to run
|
||||||
|
|
||||||
|
```shell
|
||||||
|
deno run --allow-run=deno --allow-net=0.0.0.0,yourgithosting.net server.ts yourgithosting.net/namespace [port]
|
||||||
|
```
|
||||||
|
|
||||||
|
You can now get raw typescript files (usable by Deno):
|
||||||
|
|
||||||
|
`http://localhost:8000/libname@1.0.0/path/file.ts`
|
||||||
|
|
||||||
|
Or get a bundled JS version:
|
||||||
|
|
||||||
|
`http://localhost:8000/libname@1.0.0/path/file.js`
|
||||||
|
|
||||||
|
If the version is not specified, the master branch is used instead:
|
||||||
|
|
||||||
|
`http://localhost:8000/libname/path/file.js`
|
||||||
|
|
1
config/run.flags
Normal file
1
config/run.flags
Normal file
|
@ -0,0 +1 @@
|
||||||
|
--allow-run=deno --allow-net=0.0.0.0,code.thunderk.net
|
3
doc/about.md
Normal file
3
doc/about.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
## About
|
||||||
|
|
||||||
|
On-the-fly bundler for typescript libraries hosted on Gitea or compatible
|
2
doc/index
Normal file
2
doc/index
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
about
|
||||||
|
run
|
17
doc/run.md
Normal file
17
doc/run.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
## How to run
|
||||||
|
|
||||||
|
```shell
|
||||||
|
deno run --allow-run=deno --allow-net=0.0.0.0,yourgithosting.net server.ts yourgithosting.net/namespace [port]
|
||||||
|
```
|
||||||
|
|
||||||
|
You can now get raw typescript files (usable by Deno):
|
||||||
|
|
||||||
|
`http://localhost:8000/libname@1.0.0/path/file.ts`
|
||||||
|
|
||||||
|
Or get a bundled JS version:
|
||||||
|
|
||||||
|
`http://localhost:8000/libname@1.0.0/path/file.js`
|
||||||
|
|
||||||
|
If the version is not specified, the master branch is used instead:
|
||||||
|
|
||||||
|
`http://localhost:8000/libname/path/file.js`
|
19
run
Executable file
19
run
Executable file
|
@ -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
|
|
@ -1,73 +1,95 @@
|
||||||
import {
|
import {
|
||||||
|
describe,
|
||||||
expect,
|
expect,
|
||||||
it,
|
it,
|
||||||
mockfn,
|
mockfn,
|
||||||
} from "https://code.thunderk.net/typescript/devtools/raw/1.2.2/testing.ts";
|
} from "https://code.thunderk.net/typescript/devtools/raw/1.3.0/testing.ts";
|
||||||
import { processRequest } from "./server.ts";
|
import { processRequest, Response } from "./server.ts";
|
||||||
|
|
||||||
it("serveBundles standard", async () => {
|
describe("serveBundles", () => {
|
||||||
const mock_run = mockfn(() => {
|
it("calls deno bundle if asking for js", async () => {
|
||||||
|
const run = mockfn(() => {
|
||||||
return {
|
return {
|
||||||
output: () => Promise.resolve(new TextEncoder().encode("abc")),
|
output: () => Promise.resolve(new TextEncoder().encode("abc")),
|
||||||
status: () => Promise.resolve({ code: 0, success: true }),
|
status: () => Promise.resolve({ code: 0, success: true }),
|
||||||
} as any;
|
} as any;
|
||||||
});
|
});
|
||||||
|
const [response, _] = await call("/greatlib@1.0.0/reader/file.js", { run });
|
||||||
const response = await processRequest(
|
|
||||||
{ url: "/greatlib/1.0.0/reader/" } as any,
|
|
||||||
mock_run,
|
|
||||||
);
|
|
||||||
expect(response).toEqual({ body: new TextEncoder().encode("abc") });
|
expect(response).toEqual({ body: new TextEncoder().encode("abc") });
|
||||||
expect(mock_run).toHaveBeenCalledTimes(1);
|
expect(run).toHaveBeenCalledTimes(1);
|
||||||
expect(mock_run).toHaveBeenCalledWith({
|
expect(run).toHaveBeenCalledWith({
|
||||||
cmd: [
|
cmd: [
|
||||||
"deno",
|
"deno",
|
||||||
"bundle",
|
"bundle",
|
||||||
"https://code.thunderk.net/typescript/greatlib/raw/1.0.0/reader.ts",
|
"https://git.example.com/libs/greatlib/raw/1.0.0/reader/file.ts",
|
||||||
],
|
],
|
||||||
stdout: "piped",
|
stdout: "piped",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("serveBundles bad path", async () => {
|
it("redirects to raw file if asking for anything other than js", async () => {
|
||||||
const mock_run = mockfn(() => {
|
const [response, run] = await call("/greatlib@1.0.0/reader/file.ts");
|
||||||
return {
|
expect(response).toEqual({
|
||||||
output: () => Promise.resolve(new TextEncoder().encode("abc")),
|
status: 301,
|
||||||
status: () => Promise.resolve({ code: 0, success: true }),
|
headers: new Headers({
|
||||||
} as any;
|
Location:
|
||||||
|
"https://git.example.com/libs/greatlib/raw/1.0.0/reader/file.ts",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(run).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await processRequest(
|
it("handles bad method", async () => {
|
||||||
{ url: "/greatlib/1.0.0/reader{}/" } as any,
|
const [response, run] = await call("/greatlib@1.0.0/reader/file.ts", {
|
||||||
mock_run,
|
method: "POST",
|
||||||
);
|
});
|
||||||
|
expect(response).toEqual({ status: 405 });
|
||||||
|
expect(run).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles bad path", async () => {
|
||||||
|
const [response, run] = await call("/greatlib@1.0.0/reader{}.ts");
|
||||||
expect(response).toEqual(
|
expect(response).toEqual(
|
||||||
{
|
{
|
||||||
status: 400,
|
status: 400,
|
||||||
body:
|
body:
|
||||||
'console.error("bundler error - Invalid path https://code.thunderk.net/typescript/greatlib/raw/1.0.0/reader{}.ts");',
|
'console.error("bundler error - Invalid path https://git.example.com/libs/greatlib/raw/1.0.0/reader{}.ts");',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(mock_run).toHaveBeenCalledTimes(0);
|
expect(run).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("serveBundles bundle fail", async () => {
|
it("handles bundle failure", async () => {
|
||||||
const mock_run = mockfn(() => {
|
const run = mockfn(() => {
|
||||||
return {
|
return {
|
||||||
output: () => Promise.resolve(undefined),
|
output: () => Promise.resolve(undefined),
|
||||||
status: () => Promise.resolve({ code: 1, success: false }),
|
status: () => Promise.resolve({ code: 1, success: false }),
|
||||||
} as any;
|
} as any;
|
||||||
});
|
});
|
||||||
|
const [response, _] = await call("/great_lib@1.0.0-dev1/reader.js", {
|
||||||
const response = await processRequest(
|
run,
|
||||||
{ url: "/great_lib/1.0.0-dev1/reader/" } as any,
|
});
|
||||||
mock_run,
|
|
||||||
);
|
|
||||||
expect(response).toEqual(
|
expect(response).toEqual(
|
||||||
{
|
{
|
||||||
status: 500,
|
status: 500,
|
||||||
body:
|
body:
|
||||||
'console.error("bundler error - Failed to bundle https://code.thunderk.net/typescript/great_lib/raw/1.0.0-dev1/reader.ts");',
|
'console.error("bundler error - Failed to bundle https://git.example.com/libs/great_lib/raw/1.0.0-dev1/reader.ts");',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
type CallOptions = { method: string; run: ReturnType<typeof mockfn> };
|
||||||
|
async function call(
|
||||||
|
url: string,
|
||||||
|
options: Partial<CallOptions> = {},
|
||||||
|
): Promise<[Response, ReturnType<typeof mockfn>]> {
|
||||||
|
const method = options.method ?? "GET";
|
||||||
|
const run = options.run ?? mockfn();
|
||||||
|
const response = await processRequest(
|
||||||
|
{ method, url } as any,
|
||||||
|
"git.example.com/libs",
|
||||||
|
run,
|
||||||
|
);
|
||||||
|
return [response, run];
|
||||||
|
}
|
||||||
|
|
74
server.ts
74
server.ts
|
@ -1,30 +1,51 @@
|
||||||
#!/usr/bin/env -S deno run --allow-run --allow-net
|
#!./run
|
||||||
// Automated bundle server
|
// Automated bundle server
|
||||||
|
|
||||||
import { bool } from "https://code.thunderk.net/typescript/functional/raw/1.0.0/all.ts";
|
|
||||||
import {
|
import {
|
||||||
Response,
|
Response,
|
||||||
serve,
|
serve,
|
||||||
ServerRequest,
|
ServerRequest,
|
||||||
} from "https://deno.land/std@0.79.0/http/server.ts";
|
} from "https://deno.land/std@0.103.0/http/server.ts";
|
||||||
|
|
||||||
export async function processRequest(
|
export async function processRequest(
|
||||||
req: ServerRequest,
|
req: ServerRequest,
|
||||||
|
hostpath: string,
|
||||||
runner = Deno.run,
|
runner = Deno.run,
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const params = req.url.split("/").filter(bool);
|
if (req.method != "GET") {
|
||||||
const lib = params[0] || "all";
|
return { status: 405 };
|
||||||
const version = params[1] || "master";
|
}
|
||||||
const file = params.length > 2 ? params.slice(2).join("/") : "all";
|
|
||||||
const path =
|
const params = req.url.split("/").filter((x) => !!x);
|
||||||
`https://code.thunderk.net/typescript/${lib}/raw/${version}/${file}.ts`;
|
if (params.length < 2) {
|
||||||
if (!path.match(/^[a-z0-9\/\.\-:_]+$/)) {
|
return { status: 404 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [lib, version] = params[0].split("@", 2);
|
||||||
|
const file = params.slice(1).join("/");
|
||||||
|
const branch = version || "master";
|
||||||
|
const path = `https://${hostpath}/${lib}/raw/${branch}/${file}`;
|
||||||
|
if (!isName(lib) || !isName(branch) || !isPath(file)) {
|
||||||
return {
|
return {
|
||||||
status: 400,
|
status: 400,
|
||||||
body: `console.error("bundler error - Invalid path ${path}");`,
|
body: `console.error("bundler error - Invalid path ${path}");`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (path.endsWith(".js")) {
|
||||||
|
return await bundle(path.slice(0, -3) + ".ts", runner);
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
status: 301,
|
||||||
|
headers: new Headers({ Location: path }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bundle(
|
||||||
|
path: string,
|
||||||
|
runner: typeof Deno.run,
|
||||||
|
): Promise<Response> {
|
||||||
const process = runner({
|
const process = runner({
|
||||||
cmd: ["deno", "bundle", path],
|
cmd: ["deno", "bundle", path],
|
||||||
stdout: "piped",
|
stdout: "piped",
|
||||||
|
@ -41,16 +62,37 @@ export async function processRequest(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function serveBundles() {
|
export async function serveBundles(hostpath: string, port: number) {
|
||||||
const listen = { hostname: "0.0.0.0", port: 8000 };
|
const listen = { hostname: "0.0.0.0", port };
|
||||||
const server = serve(listen);
|
const server = serve(listen);
|
||||||
console.log(`Serving bundles on ${listen.hostname}:${listen.port} ...`);
|
console.log(
|
||||||
|
`Serving ${hostpath} bundles on ${listen.hostname}:${listen.port} ...`,
|
||||||
|
);
|
||||||
for await (const req of server) {
|
for await (const req of server) {
|
||||||
const response = await processRequest(req);
|
try {
|
||||||
|
const response = await processRequest(req, hostpath);
|
||||||
await req.respond(response);
|
await req.respond(response);
|
||||||
|
} catch (err) {
|
||||||
|
// console.error(err);
|
||||||
|
await req.respond({ status: 500 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.main) {
|
function isName(input: string): boolean {
|
||||||
await serveBundles();
|
return !!input.match(/^[a-zA-Z0-9_\-\.]+$/);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPath(input: string): boolean {
|
||||||
|
return !input.split("/").some((part) => !isName(part));
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { Response };
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
if (Deno.args.length >= 1 && Deno.args.length <= 2) {
|
||||||
|
await serveBundles(Deno.args[0], parseInt(Deno.args[1] || "8000"));
|
||||||
|
} else {
|
||||||
|
console.error("Usage: server.ts yourgithosting.net/namespace [port]");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue