Add CI & Deno Supported & Fix a lot of bugs & Webpack for building package

This commit is contained in:
186526 2022-07-02 17:01:00 +00:00 committed by GitHub
parent b3552cb6a2
commit 057c9b9cb5
25 changed files with 2035 additions and 86 deletions

14
.devcontainer/Dockerfile Normal file
View File

@ -0,0 +1,14 @@
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
ARG VARIANT=16-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment if you want to install an additional version of node using nvm
# ARG EXTRA_NODE_VERSION=10
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
# [Optional] Uncomment if you want to install more global node packages
# RUN su node -c "npm install -g <your-package-list -here>"

View File

@ -0,0 +1,17 @@
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
ARG VARIANT=16-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
# Install tslint, typescript. eslint is installed by javascript image
ARG NODE_MODULES="tslint-to-eslint-config typescript"
COPY library-scripts/meta.env /usr/local/etc/vscode-dev-containers
RUN su node -c "umask 0002 && npm install -g ${NODE_MODULES}" \
&& npm cache clean --force > /dev/null 2>&1
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment if you want to install an additional version of node using nvm
# ARG EXTRA_NODE_VERSION=10
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"

View File

@ -0,0 +1,39 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/typescript-node
{
"name": "Node.js & TypeScript",
"build": {
"dockerfile": "Dockerfile",
// Update 'VARIANT' to pick a Node version: 18, 16, 14.
// Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local on arm64/Apple Silicon.
"args": {
"VARIANT": "16-bullseye"
}
},
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"dbaeumer.vscode-eslint",
"ms-vscode.vscode-typescript-next",
"oderwat.indent-rainbow",
]
}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "yarn install",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node",
"features": {
"git": "latest"
}
}

22
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: CI
on: push
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: yarn install
- name: Test handlers.js
run: yarn test

View File

@ -1,3 +1,5 @@
{
"compile-hero.disable-compile-files-on-did-save-code": false
"compile-hero.disable-compile-files-on-did-save-code": true,
"deno.enable": false,
"editor.formatOnSave": true,
}

View File

@ -4,7 +4,6 @@ import {
handler,
route,
response,
ChainInterrupted,
} from "../index";
import errorHandler from "./errorHandler";
@ -25,10 +24,18 @@ App.binding(
App.create(
"ANY",
(): Promise<string> =>
new Promise((resolve) => {
new Promise(() => {
console.log("Hello World!");
resolve("Hello World!");
throw ChainInterrupted;
throw new response("Hello World!");
})
)
).binding(
"/(.*)",
App.create(
"ANY",
(): Promise<string> =>
new Promise((resolve) => {
resolve("Hello World?")
})
)
);

View File

@ -6,7 +6,6 @@ export { router } from "./src/router";
export { methodENUM as method } from "./src/interface/method";
export { response } from "./src/interface/response";
export { request } from "./src/interface/request";
export { ChainInterrupted } from "./src/interface/index";
export * as platformAdapater from "./src/platform/export";
export { rootRouter };

6
jest.config.js Normal file
View File

@ -0,0 +1,6 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
export default {
preset: 'ts-jest',
testEnvironment: 'node',
forceExit: true
};

View File

@ -20,22 +20,35 @@
},
"devDependencies": {
"@cloudflare/workers-types": "^3.13.0",
"@types/jest": "^28.1.4",
"@types/node": "^18.0.0",
"@webpack-cli/generators": "^2.5.0",
"axios": "^0.27.2",
"jest": "^28.1.2",
"prettier": "^2.7.1",
"ts-jest": "^28.0.5",
"ts-loader": "^9.3.1",
"ts-node": "^10.8.1",
"typescript": "^4.7.4",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0"
},
"sideEffects": false,
"scripts": {
"build": "yarn build:node && yarn build:webworker",
"build:node": "TARGET=node webpack",
"build:webworker": "TARGET=webworker webpack",
"watch": "webpack --watch"
"build": "yarn clean && yarn build:node && yarn build:serviceworker && yarn build:cfworker && yarn build:deno",
"build:node": "BUILD_TARGET=node webpack",
"build:serviceworker": "BUILD_TARGET=serviceworker webpack",
"build:cfworker": "BUILD_TARGET=cfworker webpack",
"build:deno": "BUILD_TARGET=deno webpack",
"watch": "webpack --watch",
"clean": "rm -rf ./dist",
"demo": "env NODE_ENV=development yarn build:node && node ./dist/main.node.js",
"tsc": "tsc",
"test": "jest",
"coverage": "jest --collectCoverage --"
},
"engines": {
"node": ">=14.0.0"
}
},
"type": "module"
}

View File

@ -1 +0,0 @@
console.log("Hello World!");

View File

@ -3,6 +3,5 @@ export { response } from "./response";
export { method } from "./method";
export { headers } from "./headers";
export { responder } from "./responder";
export const ChainInterrupted = new Error("ChainInterrupted");
export const AllMismatchInterrupted = new Error("AllMismatchInterrupted");
export type path = string | RegExp;

View File

@ -56,6 +56,6 @@ export enum methodENUM {
ANY = "ANY",
}
export type method = methodENUM | string;
export type method = "CONNECT" | "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT" | "TRACE" | "ANY" | methodENUM;
export default method;

View File

@ -4,17 +4,23 @@ export const platform = (() => {
if (typeof process != "undefined") {
return "Node.js";
}
if (typeof self != "undefined") {
return "Web Worker";
if (typeof Deno != "undefined") {
return "Deno";
}
return "Unknown";
if (typeof self != "undefined") {
return "Service Worker";
}
return undefined;
})();
export const version = (() => {
switch (platform) {
case "Node.js":
return process.version;
case "Deno":
return Deno.version.deno;
case "Service Worker":
return undefined;
default:
return "Unknown";
return undefined;
}
})();

67
src/platform/deno.ts Normal file
View File

@ -0,0 +1,67 @@
import { SWPlatformAdapter } from "./serviceworker";
import { platformAdapater } from "./index";
import { request } from "../interface/request";
import { headers } from "../interface/headers";
import { methodENUM } from "src/interface/method";
const DefaultConn: Deno.Conn = {
localAddr: {
transport: "tcp",
hostname: "0.0.0.0",
port: 80,
},
remoteAddr: {
transport: "tcp",
hostname: "0.0.0.0",
port: 80,
},
rid: 0,
closeWrite: async () => undefined,
readable: "",
writable: "",
read: async (p: Uint8Array) => null,
write: async (p: Uint8Array) => 0,
close: () => undefined,
};
export class DenoPlatformAdapter<T = any, K = any>
extends SWPlatformAdapter<T, K>
implements platformAdapater<T, K>
{
async listen(port: number): Promise<void> {
const Server: Deno.Listener = Deno.listen({ port });
for await (const connection of Server) {
const httpConnection = Deno.serveHttp(connection);
for await (const requestEvent of httpConnection) {
requestEvent.respondWith(this.handler(requestEvent, connection));
}
}
}
async handleRequest(nativeRequest: Request, connection: Deno.Conn = DefaultConn): Promise<request<T>> {
const requestHeaders = new headers(
Object.fromEntries(nativeRequest.headers.entries())
);
const requestMessage: request<T> = new request(
<methodENUM>nativeRequest.method,
new URL(nativeRequest.url),
requestHeaders,
await nativeRequest.text(),
{},
`${connection.remoteAddr.hostname}:${connection.remoteAddr.port}` || ""
);
return requestMessage;
}
async handler(event: FetchEvent, connection: Deno.Conn = DefaultConn): Promise<Response> {
return await this.handleResponse(
await this.handleRequest(event.request, connection).then((request) =>
this.router.respond(request)
)
)
}
}

View File

@ -1,9 +1,11 @@
import { NodePlatformAdapter } from "./node";
import { SWPlatformAdapter } from "./serviceworker";
import { DenoPlatformAdapter } from "./deno";
export const platformAdapaterMapping = {
"Node.js": NodePlatformAdapter,
"Web Worker": SWPlatformAdapter,
"Service Worker": SWPlatformAdapter,
"Deno": DenoPlatformAdapter,
};
export { NodePlatformAdapter, SWPlatformAdapter };
export { NodePlatformAdapter, SWPlatformAdapter, DenoPlatformAdapter };

View File

@ -1,5 +1,5 @@
import { request, response } from "../interface";
import { router } from "../../index";
import { request, response } from "../interface/index";
import { router } from "../router";
export interface platformAdapater<T = any, K = any> {
router: router<T, K>;

View File

@ -1,9 +1,10 @@
import { platformAdapater } from "./index";
import { request, response } from "../interface";
import { request, response } from "../interface/index";
import { router } from "../router";
import { headers } from "../interface/headers";
import http from "http";
import { methodENUM } from "src/interface/method";
export class NodePlatformAdapter<T = any, K = any> implements platformAdapater {
public router: router<T, K>;
@ -38,7 +39,7 @@ export class NodePlatformAdapter<T = any, K = any> implements platformAdapater {
}
let body: string = "";
const ip: string = nativeRequest.socket.remoteAddress?.replace("::ffff:","") ?? "0.0.0.0";
const ip: string = nativeRequest.socket.remoteAddress?.replace("::ffff:", "") ?? "0.0.0.0";
const requestHeaders = new headers(<any>nativeRequest.headers);
if (!["GET", "HEAD", "DELETE", "OPTIONS"].includes(nativeRequest.method)) {
@ -54,7 +55,7 @@ export class NodePlatformAdapter<T = any, K = any> implements platformAdapater {
}
return new request<T>(
nativeRequest.method,
<methodENUM>nativeRequest.method,
new URL(
nativeRequest.url,
`http://${requestHeaders.get("host") ?? "localhost"}`

View File

@ -4,6 +4,8 @@ import { response } from "../interface/response";
import { router } from "../router";
import { headers } from "../interface/headers";
import { methodENUM } from "src/interface/method";
export class SWPlatformAdapter<T = any, K = any> implements platformAdapater {
public router: router<T, K>;
@ -22,7 +24,7 @@ export class SWPlatformAdapter<T = any, K = any> implements platformAdapater {
Object.fromEntries(nativeRequest.headers.entries())
);
const requestMessage: request<T> = new request(
nativeRequest.method,
<methodENUM>nativeRequest.method,
new URL(nativeRequest.url),
requestHeaders,
await nativeRequest.text(),

View File

@ -3,7 +3,6 @@ import {
path,
response,
request,
ChainInterrupted,
AllMismatchInterrupted,
responder,
method,
@ -132,8 +131,8 @@ export class router<K = any, V = any> {
throw AllMismatchInterrupted;
}
} catch (e) {
if (e === ChainInterrupted) {
return e.response;
if (e instanceof response) {
throw e;
}
if (e === AllMismatchInterrupted) mismatchCount++;
else {
@ -183,16 +182,16 @@ export class rootRouter<K = any, V = any> extends router<K, V> {
public adapater: platformAdapater<K, V>;
errorResponder =
(errorCode: number, errorMessage?: string) =>
async (_request: request<K>): Promise<response<V>> =>
new response(errorMessage ?? "", errorCode);
async (_request: request<K>): Promise<response<V>> =>
new response(errorMessage ?? "", errorCode);
respond = async (request: request<K>): Promise<response<V>> => {
let responseMessage: response<V> = new response("");
try {
responseMessage = await this._respond(request, responseMessage);
} catch (e) {
if (e === ChainInterrupted) {
return responseMessage;
if (e instanceof response) {
return e;
} else if (e === AllMismatchInterrupted) {
responseMessage =
(await this.errorResponder(404, "404 Not Found\n")(request)) ??
@ -214,7 +213,8 @@ export class rootRouter<K = any, V = any> extends router<K, V> {
useMappingAdapter(
mapping: { [platform: string]: platformAdapaterConstructor } = platformAdapaterMapping
): this {
if(mapping[platform] == undefined) throw new Error("Platform not found in mapping");
if (typeof platform == "undefined") throw new Error("Cannot detect platform");
if (mapping[platform] == undefined) throw new Error("Platform not found in mapping");
else this.useAdapater(mapping[platform]);
return this;
}

60
test/node.test.ts Normal file
View File

@ -0,0 +1,60 @@
import _ from "./test-server";
import Axios from "axios";
_.listen(3000);
const Instance = Axios.create({
baseURL: "http://localhost:3000"
})
const randomString = () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
describe("Test server", () => {
test("normal 200 response", async () => {
expect.assertions(2);
const { data, status } = await Instance.get("/");
expect(status).toEqual(200);
expect(data).toEqual("200 OK");
})
test("post response", async () => {
expect.assertions(2);
const string = randomString();
const { data, status } = await Instance.post("/post", string);
expect(status).toEqual(200);
expect(data).toEqual(string);
})
test("change header and status code", async () => {
expect.assertions(3);
const { data, status, headers } = await Instance.get("/header");
expect(status).toEqual(204);
expect(headers["itis"]).toEqual("work");
expect(data).toEqual("");
})
test("get param", async () => {
expect.assertions(2);
const string = randomString();
const { data, status } = await Instance.get(`/info/${string}`);
expect(status).toEqual(200);
expect(data).toEqual(string);
})
test("chain interrupted", async () => {
expect.assertions(2);
const { data, status } = await Instance.get(`/info/foo`);
expect(status).toEqual(200);
expect(data).toEqual("hit");
})
})

25
test/test-server.ts Normal file
View File

@ -0,0 +1,25 @@
import * as handlersJS from '../index';
const App = new handlersJS.rootRouter();
App.binding("/", App.create("GET", async () => "200 OK"));
App.binding("/post", App.create("POST", async (request: handlersJS.request<any>) => request.body));
App.binding("/header", App.create("GET", async () => {
const response = new handlersJS.response<any>("");
response.status = 204;
response.headers.set("itis", "work");
return response;
}));
App
.route("/info/(.*)")
.binding("/foo", App.create("GET", (): Promise<handlersJS.response<any>> => new Promise(resolve => {
throw new handlersJS.response("hit")
})))
.binding("/(.*)", App.create("GET", async (request: handlersJS.request<any>) => request.params[0] ?? "not found"));
App.useMappingAdapter();
export default App;

View File

@ -1,14 +1,9 @@
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"lib": [
"ESNext",
],
"types": [
"@cloudflare/workers-types",
"@types/node",
],
"module": "esnext",
"lib": ["ESNext"],
"types": ["@cloudflare/workers-types", "@types/node", "@types/jest"],
"skipLibCheck": true,
"sourceMap": true,
"outDir": "./dist",
@ -28,18 +23,12 @@
"experimentalDecorators": true,
"resolveJsonModule": true,
"baseUrl": ".",
"plugins": [
{
"transform": "@zerollup/ts-transform-paths",
}
],
"plugins": [{
"transform": "@zerollup/ts-transform-paths"
}],
"declaration": true
},
"exclude": [
"node_modules"
],
"include": [
"index.ts",
"src/**/*.ts",
"demo/**/*.ts",
],
"exclude": ["node_modules"],
"include": ["index.ts", "src/**/*.ts", "demo/**/*.ts", "types/deno.d.ts"],
// "esm": true
}

109
types/deno.d.ts vendored Normal file
View File

@ -0,0 +1,109 @@
declare namespace Deno {
export const version: {
/** Deno's version. For example: `"1.0.0"` */
deno: string;
/** The V8 version used by Deno. For example: `"8.0.0.0"` */
v8: string;
/** The TypeScript version used by Deno. For example: `"4.0.0"` */
typescript: string;
};
export interface NetAddr {
transport: "tcp" | "udp";
hostname: string;
port: number;
}
export type Addr = NetAddr;
export interface Closer {
close(): void;
}
export interface Reader {
/** Reads up to `p.byteLength` bytes into `p`. It resolves to the number of
* bytes read (`0` < `n` <= `p.byteLength`) and rejects if any error
* encountered. Even if `read()` resolves to `n` < `p.byteLength`, it may
* use all of `p` as scratch space during the call. If some data is
* available but not `p.byteLength` bytes, `read()` conventionally resolves
* to what is available instead of waiting for more.
*
* When `read()` encounters end-of-file condition, it resolves to EOF
* (`null`).
*
* When `read()` encounters an error, it rejects with an error.
*
* Callers should always process the `n` > `0` bytes returned before
* considering the EOF (`null`). Doing so correctly handles I/O errors that
* happen after reading some bytes and also both of the allowed EOF
* behaviors.
*
* Implementations should not retain a reference to `p`.
*
* Use `itereateReader` from from https://deno.land/std/streams/conversion.ts to
* turn a Reader into an AsyncIterator.
*/
read(p: Uint8Array): Promise<number | null>;
}
export interface Writer {
/** Writes `p.byteLength` bytes from `p` to the underlying data stream. It
* resolves to the number of bytes written from `p` (`0` <= `n` <=
* `p.byteLength`) or reject with the error encountered that caused the
* write to stop early. `write()` must reject with a non-null error if
* would resolve to `n` < `p.byteLength`. `write()` must not modify the
* slice data, even temporarily.
*
* Implementations should not retain a reference to `p`.
*/
write(p: Uint8Array): Promise<number>;
}
export interface Conn extends Reader, Writer, Closer {
/** The local address of the connection. */
readonly localAddr: Addr;
/** The remote address of the connection. */
readonly remoteAddr: Addr;
/** The resource ID of the connection. */
readonly rid: number;
/** Shuts down (`shutdown(2)`) the write side of the connection. Most
* callers should just use `close()`. */
closeWrite(): Promise<void>;
readonly readable: ReadableStream<Uint8Array>;
readonly writable: WritableStream<Uint8Array>;
}
/** A generic network listener for stream-oriented protocols. */
export interface Listener extends AsyncIterable<Conn> {
/** Waits for and resolves to the next connection to the `Listener`. */
accept(): Promise<Conn>;
/** Close closes the listener. Any pending accept promises will be rejected
* with errors. */
close(): void;
/** Return the address of the `Listener`. */
readonly addr: Addr;
/** Return the rid of the `Listener`. */
readonly rid: number;
[Symbol.asyncIterator](): AsyncIterableIterator<Conn>;
}
export interface ListenOptions {
port: number;
}
export function listen(
options: ListenOptions & { transport?: "tcp" }
): Listener;
export function serveHttp(conn: Conn): HttpConn;
export interface HttpConn extends AsyncIterable<FetchEvent> {
readonly rid: number;
nextRequest(): Promise<FetchEvent | null>;
close(): void;
}
}

View File

@ -1,12 +1,20 @@
// Generated using webpack-cli https://github.com/webpack/webpack-cli
const path = require("path");
import path from "path";
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const isProduction = process.env.NODE_ENV == "production";
const config = {
output: {
path: path.resolve(__dirname, "dist"),
chunkFormat: "module",
library: {
type: 'module',
},
},
plugins: [
// Add your plugins here
@ -30,9 +38,12 @@ const config = {
"http": false
}
},
experiments: {
outputModule: true,
},
};
module.exports = () => {
export default () => {
if (isProduction) {
config.mode = "production";
config.entry = "./index.ts";
@ -40,14 +51,23 @@ module.exports = () => {
config.mode = "development";
config.entry = "./demo/index.ts";
}
switch(process.env.TARGET) {
switch (process.env.BUILD_TARGET) {
case "node":
config.target = "node14";
config.target = "node12";
config.output.filename = "main.node.js";
break;
case "webworker":
case "serviceworker":
config.target = "webworker";
config.output.filename = "main.webworker.js";
config.output.filename = "main.serviceworker.js";
break;
case "deno":
config.target = "webworker";
config.output.filename = "main.deno.js";
break;
case "cfworker":
config.mode = "production";
config.target = "webworker";
config.output.filename = "main.cfworker.js";
break;
default:
config.target = "es6";

1599
yarn.lock

File diff suppressed because it is too large Load Diff