diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml new file mode 100644 index 0000000..c227553 --- /dev/null +++ b/.github/workflows/build-and-publish.yml @@ -0,0 +1,78 @@ +name: build and publish + +on: + pull_request: + +jobs: + check_and_build: + runs-on: ubuntu-latest + permissions: + id-token: write + outputs: + s3_bucket_key: ${{ steps.publish_package.outputs.s3-bucket-key }} + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 20.x + uses: actions/setup-node@v3 + with: + node-version: 20.x + cache: npm + - name: install + run: npm ci + - name: build + run: make build + - name: test + run: make test bench + - name: Publish + uses: menduz/oddish-action@master + id: publish_package + with: + registry-url: 'https://registry.npmjs.org' + access: public + ## use action runId instead of current date to generate snapshot numbers + deterministic-snapshot: true + + ## publish every package to s3 + s3-bucket: ${{ secrets.SDK_TEAM_S3_BUCKET }} + s3-bucket-key-prefix: '@dcl/${{ github.event.repository.name }}/branch/${{ github.head_ref }}' + s3-bucket-region: ${{ secrets.SDK_TEAM_AWS_REGION }} + + ## inform gitlab after publishing to proceed with CDN propagation + gitlab-token: ${{ secrets.GITLAB_TOKEN }} + gitlab-pipeline-url: ${{ secrets.GITLAB_URL }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + AWS_DEFAULT_REGION: us-east-1 + AWS_ACCESS_KEY_ID: ${{ secrets.SDK_TEAM_AWS_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.SDK_TEAM_AWS_SECRET }} + + notify_deployment: + needs: [check_and_build] + if: ${{ github.event.pull_request.number && !startsWith(github.head_ref, 'dependabot/') }} + runs-on: ubuntu-latest + name: Deployment Notification + steps: + - name: Find Comment + uses: peter-evans/find-comment@v3 + id: fc + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: Test this pull request + + - name: Generate S3 URL + id: url-generator + run: echo "body=${{ secrets.SDK_TEAM_S3_BASE_URL }}/${{ needs.check_and_build.outputs.s3_bucket_key }}" >> $GITHUB_OUTPUT + + - name: Create or update comment + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body: | + # Test this pull request + - The package can be tested by running + ```bash + npm upgrade "${{ steps.url-generator.outputs.body }}" + ``` + edit-mode: replace diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 6e15e84..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,35 +0,0 @@ -on: - push: - branches: - - main - pull_request: - release: - types: - - created - -name: CI -jobs: - build: - runs-on: ubuntu-latest - permissions: - id-token: write - steps: - - uses: actions/checkout@master - - name: Use Node.js 20.x - uses: actions/setup-node@v3 - with: - node-version: 20.x - cache: npm - - name: install - run: npm ci - - name: build - run: make build - - name: test - run: make test bench - - name: Publish - uses: menduz/oddish-action@master - with: - registry-url: "https://registry.npmjs.org" - access: public - env: - NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml new file mode 100644 index 0000000..0f9e7b8 --- /dev/null +++ b/.github/workflows/master.yml @@ -0,0 +1,37 @@ +on: + push: + branches: + - master + - main + tags: + - '*' + release: + types: + - created + +name: Publish NPM package +jobs: + install: + permissions: + id-token: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 18.x + uses: actions/setup-node@v4 + with: + node-version: 18.x + + - name: npm ci + run: npm ci + - name: build + run: make build + - name: test + run: make test bench + + - name: Publish + uses: menduz/oddish-action@master + with: + access: public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..56666d8 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,22 @@ +on: + push: + branches-ignore: + - "main" + pull_request: + +name: PR Validation +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 18.x + uses: actions/setup-node@v4 + with: + node-version: 18.x + - name: npm ci + run: npm ci + - name: build + run: make build + - name: test + run: make test bench diff --git a/Makefile b/Makefile index 8dbee99..ae2ceb9 100644 --- a/Makefile +++ b/Makefile @@ -12,11 +12,9 @@ install: build: ./node_modules/.bin/tsc -p tsconfig.json - rm -rf node_modules/@microsoft/api-extractor/node_modules/typescript || true - ./node_modules/.bin/api-extractor run $(LOCAL_ARG) --typescript-compiler-folder ./node_modules/typescript test: - node --inspect ./node_modules/.bin/jest --forceExit --detectOpenHandles --coverage --verbose $(TEST_FILE) + ./node_modules/.bin/jest --forceExit --detectOpenHandles --coverage --verbose $(TEST_FILE) test-esm: node --experimental-vm-modules ./node_modules/.bin/jest --forceExit --detectOpenHandles --coverage --verbose diff --git a/README.md b/README.md index 0505f9c..b25385c 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# http-server +# wkc-http-server + +forked from https://github.com/well-known-components/http-server diff --git a/api-extractor.json b/api-extractor.json deleted file mode 100644 index e0f7968..0000000 --- a/api-extractor.json +++ /dev/null @@ -1,364 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - - /** - * Optionally specifies another JSON config file that this file extends from. This provides a way for - * standard settings to be shared across multiple projects. - * - * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains - * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be - * resolved using NodeJS require(). - * - * SUPPORTED TOKENS: none - * DEFAULT VALUE: "" - */ - // "extends": "./shared/api-extractor-base.json" - // "extends": "my-package/include/api-extractor-base.json" - - /** - * Determines the "" token that can be used with other config file settings. The project folder - * typically contains the tsconfig.json and package.json config files, but the path is user-defined. - * - * The path is resolved relative to the folder of the config file that contains the setting. - * - * The default value for "projectFolder" is the token "", which means the folder is determined by traversing - * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder - * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error - * will be reported. - * - * SUPPORTED TOKENS: - * DEFAULT VALUE: "" - */ - // "projectFolder": "..", - - /** - * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor - * analyzes the symbols exported by this module. - * - * The file extension must be ".d.ts" and not ".ts". - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * SUPPORTED TOKENS: , , - */ - "mainEntryPointFilePath": "/dist/index.d.ts", - - /** - * A list of NPM package names whose exports should be treated as part of this package. - * - * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", - * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part - * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly - * imports library2. To avoid this, we can specify: - * - * "bundledPackages": [ "library2" ], - * - * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been - * local files for library1. - */ - "bundledPackages": [], - - /** - * Determines how the TypeScript compiler engine will be invoked by API Extractor. - */ - "compiler": { - /** - * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * Note: This setting will be ignored if "overrideTsconfig" is used. - * - * SUPPORTED TOKENS: , , - * DEFAULT VALUE: "/tsconfig.json" - */ - // "tsconfigFilePath": "/tsconfig.json", - /** - * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. - * The object must conform to the TypeScript tsconfig schema: - * - * http://json.schemastore.org/tsconfig - * - * If omitted, then the tsconfig.json file will be read from the "projectFolder". - * - * DEFAULT VALUE: no overrideTsconfig section - */ - // "overrideTsconfig": { - // . . . - // } - /** - * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended - * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when - * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses - * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. - * - * DEFAULT VALUE: false - */ - "skipLibCheck": true, - }, - - /** - * Configures how the API report file (*.api.md) will be generated. - */ - "apiReport": { - /** - * (REQUIRED) Whether to generate an API report. - */ - "enabled": true - - /** - * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce - * a full file path. - * - * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". - * - * SUPPORTED TOKENS: , - * DEFAULT VALUE: ".api.md" - */ - // "reportFileName": ".api.md", - - /** - * Specifies the folder where the API report file is written. The file name portion is determined by - * the "reportFileName" setting. - * - * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, - * e.g. for an API review. - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * SUPPORTED TOKENS: , , - * DEFAULT VALUE: "/etc/" - */ - // "reportFolder": "/etc/", - - /** - * Specifies the folder where the temporary report file is written. The file name portion is determined by - * the "reportFileName" setting. - * - * After the temporary file is written to disk, it is compared with the file in the "reportFolder". - * If they are different, a production build will fail. - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * SUPPORTED TOKENS: , , - * DEFAULT VALUE: "/temp/" - */ - // "reportTempFolder": "/temp/" - }, - - /** - * Configures how the doc model file (*.api.json) will be generated. - */ - "docModel": { - /** - * (REQUIRED) Whether to generate a doc model file. - */ - "enabled": true, - - /** - * The output path for the doc model file. The file extension should be ".api.json". - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * SUPPORTED TOKENS: , , - * DEFAULT VALUE: "/temp/.api.json" - */ - "apiJsonFilePath": "/dist/.api.json" - }, - - /** - * Configures how the .d.ts rollup file will be generated. - */ - "dtsRollup": { - /** - * (REQUIRED) Whether to generate the .d.ts rollup file. - */ - "enabled": false - - /** - * Specifies the output path for a .d.ts rollup file to be generated without any trimming. - * This file will include all declarations that are exported by the main entry point. - * - * If the path is an empty string, then this file will not be written. - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * SUPPORTED TOKENS: , , - * DEFAULT VALUE: "/dist/.d.ts" - */ - // "untrimmedFilePath": "/dist/.d.ts", - - /** - * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. - * This file will include only declarations that are marked as "@public" or "@beta". - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * SUPPORTED TOKENS: , , - * DEFAULT VALUE: "" - */ - // "betaTrimmedFilePath": "/dist/-beta.d.ts", - - /** - * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. - * This file will include only declarations that are marked as "@public". - * - * If the path is an empty string, then this file will not be written. - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * SUPPORTED TOKENS: , , - * DEFAULT VALUE: "" - */ - // "publicTrimmedFilePath": "/dist/-public.d.ts", - - /** - * When a declaration is trimmed, by default it will be replaced by a code comment such as - * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the - * declaration completely. - * - * DEFAULT VALUE: false - */ - // "omitTrimmingComments": true - }, - - /** - * Configures how the tsdoc-metadata.json file will be generated. - */ - "tsdocMetadata": { - /** - * Whether to generate the tsdoc-metadata.json file. - * - * DEFAULT VALUE: true - */ - "enabled": true, - /** - * Specifies where the TSDoc metadata file should be written. - * - * The path is resolved relative to the folder of the config file that contains the setting; to change this, - * prepend a folder token such as "". - * - * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", - * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup - * falls back to "tsdoc-metadata.json" in the package folder. - * - * SUPPORTED TOKENS: , , - * DEFAULT VALUE: "" - */ - "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" - }, - - /** - * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files - * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. - * To use the OS's default newline kind, specify "os". - * - * DEFAULT VALUE: "crlf" - */ - // "newlineKind": "crlf", - - /** - * Configures how API Extractor reports error and warning messages produced during analysis. - * - * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. - */ - "messages": { - /** - * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing - * the input .d.ts files. - * - * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" - * - * DEFAULT VALUE: A single "default" entry with logLevel=warning. - */ - "compilerMessageReporting": { - /** - * Configures the default routing for messages that don't match an explicit rule in this table. - */ - "default": { - /** - * Specifies whether the message should be written to the the tool's output log. Note that - * the "addToApiReportFile" property may supersede this option. - * - * Possible values: "error", "warning", "none" - * - * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail - * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes - * the "--local" option), the warning is displayed but the build will not fail. - * - * DEFAULT VALUE: "warning" - */ - "logLevel": "warning" - - /** - * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), - * then the message will be written inside that file; otherwise, the message is instead logged according to - * the "logLevel" option. - * - * DEFAULT VALUE: false - */ - // "addToApiReportFile": false - } - - // "TS2551": { - // "logLevel": "warning", - // "addToApiReportFile": true - // }, - // - // . . . - }, - - /** - * Configures handling of messages reported by API Extractor during its analysis. - * - * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" - * - * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings - */ - "extractorMessageReporting": { - "default": { - "logLevel": "warning" - // "addToApiReportFile": false - } - - // "ae-extra-release-tag": { - // "logLevel": "warning", - // "addToApiReportFile": true - // }, - // - // . . . - }, - - /** - * Configures handling of messages reported by the TSDoc parser when analyzing code comments. - * - * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" - * - * DEFAULT VALUE: A single "default" entry with logLevel=warning. - */ - "tsdocMessageReporting": { - "default": { - "logLevel": "warning" - // "addToApiReportFile": false - } - - // "tsdoc-link-tag-unescaped-text": { - // "logLevel": "warning", - // "addToApiReportFile": true - // }, - // - // . . . - } - } -} diff --git a/etc/http-server.api.md b/etc/http-server.api.md deleted file mode 100644 index 6c8a7ec..0000000 --- a/etc/http-server.api.md +++ /dev/null @@ -1,256 +0,0 @@ -## API Report File for "@well-known-components/http-server" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import type * as http from 'http'; -import type * as https from 'https'; -import { IBaseComponent } from '@well-known-components/interfaces'; -import { IConfigComponent } from '@well-known-components/interfaces'; -import { IFetchComponent } from '@well-known-components/interfaces'; -import { IHttpServerComponent } from '@well-known-components/interfaces'; -import type { ILoggerComponent } from '@well-known-components/interfaces'; -import { IMetricsComponent } from '@well-known-components/interfaces'; -import { IMiddlewareAdapterHandler } from '@well-known-components/interfaces'; -import { IStatusCheckCapableComponent } from '@well-known-components/interfaces'; -import { Key } from 'path-to-regexp'; -import type { Socket } from 'net'; - -// @public (undocumented) -export type AllowedMethodOptions = Partial<{ - throw: boolean; - notImplemented: NewableFunction; - methodNotAllowed: NewableFunction; -}>; - -// @public (undocumented) -export type ApiDefinition = { - prefix: string; - instance: Resource; - middlewares: any[]; - resources: ApiDefinitionEntry[]; - metadata: Array<[string, any]>; -}; - -// @public (undocumented) -export type ApiDefinitionEntry = { - httpMethod: string; - httpRoute: string; - handlerName: string | symbol | number; - middlewares: any[]; - delegate(context: any): HandlerReturnType; - metadata: Array<[string, any]>; -}; - -// @public (undocumented) -export type AsyncRequestHandlersOf = { - [K in keyof T]: T[K] extends (...args: any[]) => HandlerReturnType ? K : never; -}[keyof T]; - -// @public (undocumented) -export type AsyncResourceDecorator = , V>(classPrototype: T, propertyKey: K, descriptor?: TypedPropertyDescriptor) => void; - -// @public (undocumented) -export type AsyncResourceParameterDecorator = >(target: Base, propertyKey: K, parameterIndex: number) => void; - -// @public -export function createServerComponent(components: ServerComponents, options: Partial): Promise>; - -// @public -export function createStatusCheckComponent(components: { - server: IHttpServerComponent; - config: IConfigComponent; -}): Promise; - -// @public -export function createTestServerComponent(): ITestHttpServerComponent; - -// @public (undocumented) -export function defineParamExtractor(prototype: any, method: any, paramIndex: number, extractor: Extractor): void; - -// @public (undocumented) -export type Extractor = (context: Context) => Promise; - -// @public (undocumented) -export type FullHttpServerComponent = IHttpServerComponent & IBaseComponent & IStatusCheckCapableComponent & { - resetMiddlewares(): void; -}; - -// @public (undocumented) -export function getDefaultHttpMetrics(): IMetricsComponent.MetricsRecordDefinition; - -// Warning: (ae-forgotten-export) The symbol "InternalMetadata" needs to be exported by the entry point index.d.ts -// -// @public -export function getOrCreateInternalMetadata(prototype: object, method: string | number | symbol): InternalMetadata; - -// @public (undocumented) -export function getUnderlyingServer(server: IHttpServerComponent): Promise; - -// Warning: (ae-forgotten-export) The symbol "metrics" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -export type HttpMetrics = keyof typeof metrics; - -// @public (undocumented) -export type IHttpServerOptions = { - cors?: CorsOptions; -} & ({ - https: https.ServerOptions; -} | { - http: http.ServerOptions; -}); - -// @public (undocumented) -export function instrumentHttpServerWithPromClientRegistry(options: { - server: IHttpServerComponent>; - config: IConfigComponent; - metrics: IMetricsComponent; - registry: IMetricsComponent.Registry; -}): Promise; - -// @public (undocumented) -export type ITestHttpServerComponent = IHttpServerComponent & IFetchComponent & { - resetMiddlewares(): void; -}; - -// @alpha (undocumented) -export type IWebSocketComponent = { - createWebSocket(url: string, protocols?: string | string[]): W; -}; - -// @public (undocumented) -export abstract class Resource { - createRouter(): { - router: Router<{}>; - api: ApiDefinition; - }; - // (undocumented) - static getApiDefinition(res: Resource): ApiDefinition; - // (undocumented) - static Handler: (method: IHttpServerComponent.HTTPMethod, route: string) => AsyncResourceDecorator; - static Prefix: (prefix: string) => ClassDecorator_2; - registerResource(globalRouter: Router): { - router: Router<{}>; - api: ApiDefinition; - }; - // (undocumented) - static RequestContext: AsyncResourceParameterDecorator; - // (undocumented) - static UrlParam: (param: string) => AsyncResourceParameterDecorator; - // Warning: (ae-forgotten-export) The symbol "Middleware" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "ClassDecorator_2" needs to be exported by the entry point index.d.ts - static WithMiddleware: (middlware: Middleware) => ClassDecorator_2 & AsyncResourceDecorator; -} - -// @public (undocumented) -export type RoutedContext = IHttpServerComponent.PathAwareContext & { - captures: string[]; - matched?: Layer[]; - routerPath?: string; -}; - -// @public (undocumented) -export type RoutePathSignature = (path: T, ...middlewares: Array>>) => Router; - -// @public -export class Router implements IHttpServerComponent.MethodHandlers { - constructor(opts?: RouterOptions); - all(path: T, middleware: IHttpServerComponent.IRequestHandler>): this; - allowedMethods(options?: AllowedMethodOptions): IHttpServerComponent.IRequestHandler; - // (undocumented) - connect: RoutePathSignature; - // (undocumented) - delete: RoutePathSignature; - // (undocumented) - get: RoutePathSignature; - // (undocumented) - head: RoutePathSignature; - match(path: string, method: string): { - path: Layer[]; - pathAndMethod: Layer[]; - route: boolean; - }; - // (undocumented) - methods: (IHttpServerComponent.HTTPMethod | string)[]; - middleware(): IHttpServerComponent.IRequestHandler; - // (undocumented) - options: RoutePathSignature; - // (undocumented) - opts: RouterOptions; - // (undocumented) - patch: RoutePathSignature; - // (undocumented) - post: RoutePathSignature; - prefix(prefix: string): this; - // (undocumented) - put: RoutePathSignature; - redirect(source: string, destination: string, code?: number): this; - // Warning: (ae-forgotten-export) The symbol "LayerOptions" needs to be exported by the entry point index.d.ts - register(path: Path, methods: ReadonlyArray, middleware: IHttpServerComponent.IRequestHandler, opts?: LayerOptions): Layer; - // (undocumented) - stack: Layer[]; - // (undocumented) - trace: RoutePathSignature; - use(...middlewares: IHttpServerComponent.IRequestHandler>[]): this; - // (undocumented) - use

(route: P, ...middlewares: IHttpServerComponent.IRequestHandler>[]): this; -} - -// @public (undocumented) -export type RouterOptions = Partial<{ - methods: IHttpServerComponent.HTTPMethod[]; - prefix: string; - routerPath: string; - sensitive: boolean; - strict: boolean; -}>; - -// @public (undocumented) -export type ServerComponents = { - config: IConfigComponent; - logs: ILoggerComponent; - ws?: WebSocketServer; -}; - -// @beta (undocumented) -export type StandardStatusResponse = { - status: 'pass' | 'fail' | 'warn'; - version?: string; - releaseId?: string; - notes?: string[]; - output?: string; - serviceId?: string; - description?: string; - details: Record; -}; - -// @beta (undocumented) -export type StandardStatusResponseDetail = { - status: 'pass' | 'fail' | 'warn'; - componentType?: string; - componentId?: string; -}; - -// @alpha (undocumented) -export type TestServerWithWs = { - ws(path: string, protocols: string | string[]): WebSocket; -}; - -// @alpha @deprecated (undocumented) -export interface WebSocketServer { - // (undocumented) - handleUpgrade(request: http.IncomingMessage, socket: Socket, upgradeHead: Buffer, callback: (client: any, request: http.IncomingMessage) => void): void; -} - -// Warnings were encountered during analysis: -// -// dist/Resource.d.ts:53:5 - (ae-forgotten-export) The symbol "HandlerReturnType" needs to be exported by the entry point index.d.ts -// dist/router.d.ts:20:5 - (ae-forgotten-export) The symbol "Layer" needs to be exported by the entry point index.d.ts -// dist/types.d.ts:19:5 - (ae-incompatible-release-tags) The symbol "ws" is marked as @public, but its signature references "WebSocketServer" which is marked as @alpha -// dist/types.d.ts:25:5 - (ae-forgotten-export) The symbol "CorsOptions" needs to be exported by the entry point index.d.ts - -// (No @packageDocumentation comment for this package) - -``` diff --git a/jest.config.js b/jest.config.js index a83546d..0d3f8fd 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,7 +8,6 @@ module.exports = { // preset: "ts-jest/presets/default-esm", preset: "ts-jest", coverageDirectory: "coverage", - coverageProvider: "v8", collectCoverageFrom: ["src/**/*.ts", "src/**/*.js", "!src/benchmark.ts",], testMatch: ["**/*.spec.(ts)"], testEnvironment: "node", diff --git a/package.json b/package.json index b1deb05..4227f95 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,19 @@ { - "name": "@well-known-components/http-server", - "version": "2.0.0", + "name": "@dcl/wkc-http-server", + "version": "0.0.1", "description": "http server component", "main": "dist/index.js", "typings": "dist/index.d.ts", "scripts": {}, "repository": { "type": "git", - "url": "git+https://github.com/well-known-components/http-server.git" + "url": "git+https://github.com/decentraland/wkc-http-server.git" }, "keywords": [], "author": "", "license": "Apache-2.0", "bugs": { - "url": "https://github.com/well-known-components/http-server/issues" + "url": "https://github.com/decentraland/wkc-http-server/issues" }, "prettier": { "printWidth": 120, @@ -22,9 +22,8 @@ "trailingComma": "none", "tabWidth": 2 }, - "homepage": "https://github.com/well-known-components/http-server#readme", + "homepage": "https://github.com/decentraland/wkc-http-server#readme", "devDependencies": { - "@microsoft/api-extractor": "^7.51.1", "@types/busboy": "^1.5.4", "@types/destroy": "^1.0.0", "@types/node": "^20.3.2", diff --git a/src/Resource.ts b/src/Resource.ts deleted file mode 100644 index bec0b97..0000000 --- a/src/Resource.ts +++ /dev/null @@ -1,254 +0,0 @@ -import 'reflect-metadata' - -import { IHttpServerComponent } from '@well-known-components/interfaces' -import HttpErrors from 'http-errors' -import { Router } from './router' -import { Middleware } from './middleware' - -const RESOURCE_PREFIX = 'resource:prefix' -const RESOURCE_METHOD = 'resource:method' -const RESOURCE_ROUTE = 'resource:route' -const RESOURCE_HANDLERS = 'resource:handlers' -const RESOURCE_MIDDLEWARES = 'resource:middlewares' -const RESOURCE_ARGUMENT_EXTRACTORS = 'resource:argumentextractors' -const INTERNAL_METADATA = 'internal-metadata' -const ROOT = ':root:' - -type HandlerReturnType = Promise - -/** - * @public - */ -export type AsyncRequestHandlersOf = { - [K in keyof T]: T[K] extends (...args: any[]) => HandlerReturnType ? K : never; -}[keyof T] - -/** - * @public - */ -export type AsyncResourceDecorator = - , V>(classPrototype: T, propertyKey: K, descriptor?: TypedPropertyDescriptor) => void - -/** - * @public - */ -export type AsyncResourceParameterDecorator = - >(target: Base, propertyKey: K, parameterIndex: number) => void - -type ClassDecorator = Resource>(target: TFunction) => TFunction | void - -/** - * @public - */ -export type Extractor = (context: Context) => Promise - -type InternalMetadata = Record - -/** - * @public - * Helper to access the internal metadata of a method in a resource. - */ -export function getOrCreateInternalMetadata(prototype: object, method: string | number | symbol): InternalMetadata { - const ret: InternalMetadata = Reflect.getMetadata(INTERNAL_METADATA, prototype) ?? {} - Reflect.defineMetadata(INTERNAL_METADATA, ret, prototype) - ret[method] = ret[method] ?? {} - return ret[method] -} - -function getOrCreateMetadata(key: string, target: T, method: string | symbol | number, gen: () => V): V { - const map = getOrCreateInternalMetadata(target, method) - map[key] = map[key] ?? gen() - return map[key] -} - -function getHandlerSet(prototype: object): Set { - return getOrCreateMetadata(RESOURCE_HANDLERS, prototype, ROOT, () => new Set()) -} - -function getParamExtractors(prototype: object, method: string | number | symbol): Array { - const fn = (prototype as any)[method] as CallableFunction - return getOrCreateMetadata(RESOURCE_ARGUMENT_EXTRACTORS, prototype, method, () => new Array(fn.length).map((_) => null)) -} - -/** - * @public - */ -export function defineParamExtractor(prototype: any, method: any, paramIndex: number, extractor: Extractor) { - const args = getParamExtractors(prototype, method) - if (args[paramIndex]) { - throw new Error(`Parameter #${paramIndex} of ${prototype.constructor.name}.${method} already defined`) - } - args[paramIndex] = extractor -} - -/** - * @public - */ -export type ApiDefinition = { - prefix: string - instance: Resource - middlewares: any[] - resources: ApiDefinitionEntry[] - metadata: Array<[string, any]> -} - -/** - * @public - */ -export type ApiDefinitionEntry = { - httpMethod: string - httpRoute: string - handlerName: string | symbol | number - middlewares: any[] - delegate(context: any): HandlerReturnType - metadata: Array<[string, any]> -} - -/** - * @public - */ -export abstract class Resource { - static Handler: (method: IHttpServerComponent.HTTPMethod, route: string) => AsyncResourceDecorator = - (httpMethod, httpRoute) => - (prototype, method, _descriptor) => { - if (typeof httpRoute != 'string' || !httpRoute.startsWith('/')) - throw new Error('http route must start with /') - const meta = getOrCreateInternalMetadata(prototype, method) - meta[RESOURCE_METHOD] = httpMethod - meta[RESOURCE_ROUTE] = httpRoute - getHandlerSet(prototype).add(method) - } - - static UrlParam = (param: string): AsyncResourceParameterDecorator => - (prototype, method, paramIndex) => { - defineParamExtractor(prototype, method, paramIndex, async (ctx) => { - const ret = (ctx as any).params[param] - if (ret === undefined) { - throw new HttpErrors.InternalServerError(`Could not resolve param ${param}`) - } - return ret - }) - } - - static RequestContext: AsyncResourceParameterDecorator = - (prototype, method, paramIndex) => { - defineParamExtractor(prototype, method, paramIndex, async (ctx) => { - return ctx - }) - } - - /** - * Uses all provided middlewares for the annotated target. - * - In resources, all handlers will use the middleware. - * - In methods, only the annotated target will use the middleware. - */ - static WithMiddleware: (middlware: Middleware) => ClassDecorator & AsyncResourceDecorator = - (middleware) => function(target: any, key?: string | number | symbol) { - if (typeof key === 'undefined') { - getOrCreateMetadata(RESOURCE_MIDDLEWARES, target.prototype, ROOT, () => [] as Middleware[]).unshift(middleware) - } else { - getOrCreateMetadata(RESOURCE_MIDDLEWARES, target, key, () => [] as Middleware[]).unshift(middleware) - } - } - - /** - * Annotates the resource setting a prefix for the URL. - */ - static Prefix: (prefix: string) => ClassDecorator = (prefix) => - (target: any) => { - if (typeof prefix != 'string' || !prefix.startsWith('/')) - throw new Error('prefix must start with /') - - const meta = getOrCreateInternalMetadata(target.prototype, ROOT) - meta[RESOURCE_PREFIX] = prefix - } - - static getApiDefinition(res: Resource): ApiDefinition { - const root = getOrCreateInternalMetadata(res, ROOT) - - const ret: ApiDefinition = { - middlewares: root[RESOURCE_MIDDLEWARES] ?? [], - prefix: root[RESOURCE_PREFIX], - instance: res, - resources: [], - metadata: Object.keys(root).sort().map(key => [key, root[key]]) - } - - for (const handlerName of getHandlerSet(res)) { - const paramExtractors = getParamExtractors(res, handlerName) - paramExtractors.forEach(($, ix) => { - if (!$) { - throw new Error( - `The method ${String(handlerName)} is lacking an annotation for the parameter #${ix}.\n` + - `The router does not know how to fulfill that parameter.` - ) - } - }) - - const impl: any = (res as any)[handlerName] - const delegate = async (context: any) => { - const args: any[] = [] - for (const extractor of paramExtractors) { - args.push(await extractor!(context)) - } - return await impl.apply(res, args) - } - - const meta = getOrCreateInternalMetadata(res, handlerName) - const entry: ApiDefinitionEntry = { - middlewares: meta[RESOURCE_MIDDLEWARES] ?? [], - delegate, - httpRoute: meta[RESOURCE_ROUTE], - httpMethod: meta[RESOURCE_METHOD], - handlerName, - metadata: Object.keys(meta).sort().map(key => [key, meta[key]]) - } - ret.resources.push(entry) - } - return ret - } - - /** - * Creates a new router for the API definition of this Resource. - */ - createRouter() { - const router = new Router() - - const api = Resource.getApiDefinition(this) - - if (api.prefix) { - router.prefix(api.prefix) - } - - if (api.middlewares.length) { - router.use(...api.middlewares) - } - - api.resources.forEach(($) => { - router[$.httpMethod.toLowerCase() as keyof IHttpServerComponent.MethodHandlers]( - $.httpRoute, - ...$.middlewares, - $.delegate - ) - }) - - return { - router, - api - } - } - - /** - * Registers the current resource in the global router. - */ - registerResource(globalRouter: Router) { - const { api, router } = this.createRouter() - globalRouter.use(router.middleware()) - globalRouter.use(router.allowedMethods()) - - return { - router, - api - } - } -} diff --git a/src/index.ts b/src/index.ts index a4f6fc2..9979168 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,4 +6,3 @@ export * from './status-checks' export * from './helpers' export * from './router' export * from './metrics' -export * from './Resource' diff --git a/src/logic.ts b/src/logic.ts index e27d883..c435d37 100644 --- a/src/logic.ts +++ b/src/logic.ts @@ -17,9 +17,14 @@ export function getServer( options: Partial, listener: http.RequestListener ): http.Server | https.Server { - if ('https' in options && options.https) return https.createServer(options.https, listener) - if ('http' in options && options.http) return http.createServer(options.http, listener) - return http.createServer(listener) + let server: http.Server | https.Server + if ('https' in options && options.https) server = https.createServer(options.https, listener) + if ('http' in options && options.http) server = http.createServer(options.http, listener) + else server = http.createServer(listener) + + server.keepAliveTimeout = options.keepAliveTimeout ?? 70_000 + server.headersTimeout = options.headersTimeout ?? 75_000 + return server } const NAME = Symbol.toStringTag diff --git a/src/types.ts b/src/types.ts index b550a90..d7ef57b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -31,4 +31,6 @@ export type ServerComponents = { */ export type IHttpServerOptions = { cors?: CorsOptions + keepAliveTimeout?: number + headersTimeout?: number } & ({ https: https.ServerOptions } | { http: http.ServerOptions }) diff --git a/test/resource.spec.ts b/test/resource.spec.ts deleted file mode 100644 index 643a743..0000000 --- a/test/resource.spec.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { IHttpServerComponent } from "@well-known-components/interfaces" -import { Resource } from "../src" -import { describeE2E } from './test-e2e-harness' - -test('basic functions', () => { - @Resource.Prefix('/a') - @Resource.WithMiddleware((next: any) => next()) - @Resource.WithMiddleware((next: any) => next()) - class MyResource extends Resource { - @Resource.Handler('GET', '/hello') - async hello() { - return { status: 200 } - } - - @Resource.Handler('POST', '/hello/:id') - @Resource.WithMiddleware((next) => next()) - async createHello(@Resource.UrlParam('id') id: string) { - return { body: id } - } - - async emptyFn() { - - } - } - - // first we create an instance to assess the decorators are not - // being added to the functions multiple times in a global - // fashion - new MyResource() - - const res = new MyResource() - - const ret = MyResource.getApiDefinition(res) - - expect(ret).toEqual({ - middlewares: [expect.any(Function), expect.any(Function)], - prefix: '/a', - instance: res, - metadata: [ - ["resource:handlers", new Set(['hello', 'createHello'])], - ["resource:middlewares", [expect.any(Function), expect.any(Function)]], - ["resource:prefix", "/a"], - ], - resources: [ - { - httpMethod: 'GET', - httpRoute: '/hello', - middlewares: [], - delegate: expect.any(Function), - handlerName: 'hello', - metadata: [ - ["resource:argumentextractors", []], - ["resource:method", "GET"], - ["resource:route", "/hello"], - ] - }, - { - httpMethod: 'POST', - httpRoute: '/hello/:id', - middlewares: [expect.any(Function)], - delegate: expect.any(Function), - handlerName: 'createHello', - metadata: [ - ["resource:argumentextractors", [expect.any(Function)]], - ["resource:method", "POST"], - ["resource:middlewares", [expect.any(Function)]], - ["resource:route", "/hello/:id"], - ] - } - ] - }) -}) - -test('fails registering an invalid prefix', () => { - expect(() => { - @Resource.Prefix('prefix-wo-slash') - class MyResource extends Resource { } - }).toThrow('must start with /') - - expect(() => { - class MyResource extends Resource { - @Resource.Handler('POST', 'path-wo-slash') - async createHello() { - return {} - } - } - - new MyResource() - }).toThrow('must start with /') -}) - - -describeE2E('resource integration suite', function({ components }) { - test('resource works', async () => { - const { fetch, server } = components - server.resetMiddlewares() - - @Resource.Prefix('/a') - class MyResource1 extends Resource { - @Resource.Handler('GET', '/hello') - async hello() { - return { status: 200 } - } - - @Resource.Handler('GET', '/hello/ctx') - async helloWithContext( - @Resource.RequestContext ctx: IHttpServerComponent.DefaultContext - ) { - return { status: 200, body: { url: ctx.url.pathname.toString() } } - } - - @Resource.Handler('GET', '/hello/:id/:name') - async helloById( - @Resource.UrlParam('id') id: string, - @Resource.UrlParam('id') idRepeated: string, - @Resource.UrlParam('name') name: string - ) { - return { status: 200, body: { id, idRepeated, name } } - } - - @Resource.Handler('POST', '/hello') - async createHello() { - return { status: 201 } - } - } - - const resource = new MyResource1() - server.use(resource.createRouter().router.middleware()) - - { - const res = await fetch.fetch(`/a/hello`) - expect(res.status).toEqual(200) - } - { - const res = await fetch.fetch(`/a/hello`, { method: 'POST' }) - expect(res.status).toEqual(201) - } - { - const res = await fetch.fetch(`/a/hello/asd/test`) - expect(await res.json()).toEqual({ id: "asd", idRepeated: "asd", name: "test" }) - } - { - const res = await fetch.fetch(`/a/hello/ctx`) - expect(await res.json()).toEqual({ "url": "/a/hello/ctx" }) - } - }) - - test('middlewares also work', async () => { - const { fetch, server } = components - server.resetMiddlewares() - - let calls: string[] = [] - - @Resource.Prefix('/a') - @Resource.WithMiddleware((_ctx, next) => { calls.push('outer1'); return next() }) - @Resource.WithMiddleware((_ctx, next) => { calls.push('outer2'); return next() }) - class MyResource2 extends Resource { - - test = 111 - - @Resource.Handler('GET', '/hello') - @Resource.WithMiddleware((_ctx, next) => { calls.push('inner1'); return next() }) - async hello() { - calls.push('hello') - return { status: 200 } - } - - @Resource.Handler('GET', '/hello/ctx') - @Resource.WithMiddleware((_ctx, next) => { calls.push('inner1'); return next() }) - @WithSpan(calls) - @Resource.WithMiddleware((_ctx, next) => { calls.push('inner2'); return next() }) - async helloWithContext( - @Resource.RequestContext ctx: IHttpServerComponent.DefaultContext - ) { - calls.push('helloWithContext') - return { status: 200, body: { url: ctx.url.pathname.toString() } } - } - - @Resource.Handler('GET', '/hello/mut') - @Resource.WithMiddleware((_ctx, next) => { calls.push('mut-inner1'); return next() }) - @Resource.WithMiddleware(async () => { calls.push('mut-inner2'); return { status: 301 } }) - async helloWithMiddleware(): Promise { - throw new Error('unreachable') - } - } - - const resource = new MyResource2() - server.use(resource.createRouter().router.middleware()) - - { - const res = await fetch.fetch(`/a/hello`) - expect(res.status).toEqual(200) - expect(calls).toEqual([ - "outer1", - "outer2", - "inner1", - "hello", - ]) - calls.length = 0 - } - { - const res = await fetch.fetch(`/a/hello/ctx`) - expect(res.status).toEqual(200) - expect(calls).toEqual([ - "outer1", - "outer2", - "inner1", - "inner2", - "enter WithSpan", - "helloWithContext", - "leave WithSpan", - ]) - calls.length = 0 - } - { - const res = await fetch.fetch(`/a/hello/mut`) - expect(res.status).toEqual(301) - expect(calls).toEqual([ - "outer1", - "outer2", - "mut-inner1", - "mut-inner2", - ]) - calls.length = 0 - } - }) -}) - - -// Example of a decorator that replaces the function entirely. This is not unusual -// practice -export function WithSpan(calls: string[]): MethodDecorator { - return function(_classPrototype, propertyKey, descriptor) { - if (typeof descriptor.value !== 'function') return - - const originalMethod = descriptor.value - - const fnName = `${propertyKey as string}:@WithSpan` - - descriptor.value = { - async [fnName](...args: any[]) { - calls.push('enter WithSpan') - const ret = await originalMethod.apply(this, args) - calls.push('leave WithSpan') - return ret - } - }[fnName] as any - } -} diff --git a/test/tsconfig.json b/test/tsconfig.json index 52d0e77..0e022d9 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -16,11 +16,9 @@ "node", "jest" ], - "esModuleInterop": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "skipLibCheck": true, /* Skip type checking of declaration files. */ "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ "noEmit": true } -} +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 18d3465..f592797 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,11 +17,9 @@ "outDir": "./dist", /* Redirect output structure to the directory. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "skipLibCheck": true, /* Skip type checking of declaration files. */ - "emitDecoratorMetadata": true, - "experimentalDecorators": true, "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ }, "include": [ "src" ] -} +} \ No newline at end of file diff --git a/tsdoc-metadata.json b/tsdoc-metadata.json deleted file mode 100644 index 5f8706d..0000000 --- a/tsdoc-metadata.json +++ /dev/null @@ -1,11 +0,0 @@ -// This file is read by tools that parse documentation comments conforming to the TSDoc standard. -// It should be published with your NPM package. It should not be tracked by Git. -{ - "tsdocVersion": "0.12", - "toolPackages": [ - { - "packageName": "@microsoft/api-extractor", - "packageVersion": "7.12.1" - } - ] -}