From 61cbc693e09a394bafaab82d9939c997a94ec4e1 Mon Sep 17 00:00:00 2001 From: Ardalan Amini Date: Sun, 25 Jun 2023 17:55:18 +0330 Subject: [PATCH 1/6] feat(config): update config package This commit updates the `config` package with the following changes: - Renamed `#src/config` to `#src/config-content` - Updated imports in `config.cjs.ts` and `config.esm.ts` - Added a new file, `config.ts`, which exports a class called `Config` - Added a new file, `index.ts`, which exports an instance of the `Config` class - Added new files for different configurations: json, jsonp, proxy, query, server and subdomain. - Each configuration has its own schema and constructor function. --- jest.config.ts | 29 ++- packages/config/package.json | 4 +- packages/config/src/config.cjs.ts | 4 +- packages/config/src/config.esm.ts | 7 +- packages/config/src/config/config.ts | 116 ++++++++++++ packages/config/src/config/index.ts | 28 +++ packages/config/src/config/json.config.ts | 68 +++++++ packages/config/src/config/jsonp.config.ts | 52 ++++++ packages/config/src/config/proxy.config.ts | 52 ++++++ packages/config/src/config/query.config.ts | 53 ++++++ packages/config/src/config/server.config.ts | 70 +++++++ .../config/src/config/subdomain.config.ts | 52 ++++++ packages/config/src/constants.ts | 173 ------------------ .../config/src/constants/CONFIG_FILEPATH.ts | 28 +++ packages/config/src/constants/ENV.ts | 30 +++ .../config/src/constants/SERVER_PROTOCOL.ts | 29 +++ packages/config/src/constants/index.ts | 28 +++ packages/config/src/index.ts | 119 +----------- packages/config/src/utils/Node.ts | 61 ++++++ packages/config/src/utils/index.ts | 26 +++ packages/config/tests/index.test.ts | 61 +++++- 21 files changed, 788 insertions(+), 302 deletions(-) create mode 100644 packages/config/src/config/config.ts create mode 100644 packages/config/src/config/index.ts create mode 100644 packages/config/src/config/json.config.ts create mode 100644 packages/config/src/config/jsonp.config.ts create mode 100644 packages/config/src/config/proxy.config.ts create mode 100644 packages/config/src/config/query.config.ts create mode 100644 packages/config/src/config/server.config.ts create mode 100644 packages/config/src/config/subdomain.config.ts delete mode 100644 packages/config/src/constants.ts create mode 100644 packages/config/src/constants/CONFIG_FILEPATH.ts create mode 100644 packages/config/src/constants/ENV.ts create mode 100644 packages/config/src/constants/SERVER_PROTOCOL.ts create mode 100644 packages/config/src/constants/index.ts create mode 100644 packages/config/src/utils/Node.ts create mode 100644 packages/config/src/utils/index.ts diff --git a/jest.config.ts b/jest.config.ts index 8c78e79..32a40c2 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,3 +1,28 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + /* * For a detailed explanation regarding each configuration property and type check, visit: * https://jestjs.io/docs/configuration @@ -34,7 +59,7 @@ export default { ], // Indicates which provider should be used to instrument code for coverage - // coverageProvider: "babel", + coverageProvider: "v8", // A list of reporter names that Jest uses when writing coverage reports // coverageReporters: [ @@ -188,7 +213,7 @@ export default { "^.+\\.tsx?$": [ "ts-jest", { - tsconfig: "/tsconfig.json", + tsconfig : "/tsconfig.json", isolatedModules: true, }, ], diff --git a/packages/config/package.json b/packages/config/package.json index ec59e93..b836f23 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -46,7 +46,7 @@ "node": ">=16" }, "imports": { - "#src/config": { + "#src/config-content": { "import": "./src/config.esm.ts", "default": "./src/config.cjs.ts" }, @@ -82,7 +82,7 @@ "./package.json": "./package.json" }, "imports": { - "#src/config": { + "#src/config-content": { "import": { "types": "./.build/esm/config.esm.d.ts", "default": "./.build/esm/config.esm.js" diff --git a/packages/config/src/config.cjs.ts b/packages/config/src/config.cjs.ts index 3d5974b..b8f44f8 100644 --- a/packages/config/src/config.cjs.ts +++ b/packages/config/src/config.cjs.ts @@ -1,4 +1,4 @@ -import { CONFIG_FILEPATH, ConfigI } from "#src/constants"; +import { CONFIG_FILEPATH } from "#src/constants/index"; /* ------------------------- Read the config file ------------------------- */ @@ -14,6 +14,6 @@ try { resolved = {}; } -const content: ConfigI = resolved.default ?? {}; +const content: any = resolved.default ?? null; export default content; diff --git a/packages/config/src/config.esm.ts b/packages/config/src/config.esm.ts index 5c6ba6d..9d5dc22 100644 --- a/packages/config/src/config.esm.ts +++ b/packages/config/src/config.esm.ts @@ -1,13 +1,12 @@ -import { CONFIG_FILEPATH, ConfigI } from "#src/constants"; +import { CONFIG_FILEPATH } from "#src/constants/index"; /* ------------------------- Read the config file ------------------------- */ // TODO: Just to avoid the CommonJS build issue // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error,@typescript-eslint/ban-ts-comment // @ts-ignore -const content: ConfigI = await import(CONFIG_FILEPATH) +const content: any = await import(CONFIG_FILEPATH) .then(resolved => resolved.default) - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - .catch(() => ({}) as never); + .catch(() => null); export default content; diff --git a/packages/config/src/config/config.ts b/packages/config/src/config/config.ts new file mode 100644 index 0000000..dacae0a --- /dev/null +++ b/packages/config/src/config/config.ts @@ -0,0 +1,116 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +import os from "node:os"; +import Joi from "joi"; +import content from "#src/config-content"; +import { ENV } from "#src/constants/index"; +import { Node, Schema } from "#src/utils/index"; +import { JsonConfig } from "./json.config.js"; +import { JsonpConfig } from "./jsonp.config.js"; +import { ProxyConfig } from "./proxy.config.js"; +import { QueryConfig } from "./query.config.js"; +import { ServerConfig } from "./server.config.js"; +import { SubdomainConfig } from "./subdomain.config.js"; + +export class Config extends Node { + + public static SCHEMA: Schema = { + env: Joi.string().valid(...Object.values(ENV)) + .default(process.env.NODE_ENV as ENV | undefined ?? ENV.DEVELOPMENT), + etag : Joi.function(), + workers: Joi.number().integer() + .min(1) + .max(os.cpus().length) + .default(1), + xPoweredBy: Joi.boolean().default(true), + }; + + /** + * Node.js environment. + * @default process.env.NODE_ENV ?? "development" + */ + public env: ENV; + + /** + * ETag response header value generator. + */ + public etag?: (body: Buffer | string, encoding?: BufferEncoding) => string; + + /** + * JSON config. + */ + public json = new JsonConfig; + + /** + * JSONP config. + */ + public jsonp = new JsonpConfig; + + /** + * Proxy config. + */ + public proxy = new ProxyConfig; + + /** + * Request query string config. + */ + public query = new QueryConfig; + + /** + * Server config. + */ + public server = new ServerConfig; + + /** + * Subdomain config. + */ + public subdomain = new SubdomainConfig; + + /** + * Number of Node.js cluster workers to be created. + * In case of `1` Node.js cluster workers won't be used. + * @default 1 + */ + public workers: number; + + /** + * Indicates whether the "X-Powered-By" header should be present or not. + * @default true + */ + public xPoweredBy: boolean; + + public constructor(config: Partial = content ?? {}) { + super(); + + const { env, etag, workers, xPoweredBy } = config as Required; + + this.env = env; + this.etag = etag; + this.workers = workers; + this.xPoweredBy = xPoweredBy; + } + +} diff --git a/packages/config/src/config/index.ts b/packages/config/src/config/index.ts new file mode 100644 index 0000000..a8088ab --- /dev/null +++ b/packages/config/src/config/index.ts @@ -0,0 +1,28 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +import { Config } from "./config.js"; + +export const config = new Config; diff --git a/packages/config/src/config/json.config.ts b/packages/config/src/config/json.config.ts new file mode 100644 index 0000000..d72a622 --- /dev/null +++ b/packages/config/src/config/json.config.ts @@ -0,0 +1,68 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +import Joi from "joi"; +import content from "#src/config-content"; +import { Node, Schema } from "#src/utils/index"; + +export class JsonConfig extends Node { + + public static SCHEMA: Schema = { + escape : Joi.boolean().default(false), + replacer: Joi.function(), + spaces : Joi.number().integer() + .min(0) + .default(0), + }; + + /** + * Enable escaping JSON responses from the `res.json`, `res.jsonp`, and `res.send` APIs. + * @default false + */ + public escape: boolean; + + /** + * The `replacer` argument used by `JSON.stringify`. + */ + public replacer?: (key: string, value: unknown) => unknown; + + /** + * The `space` argument used by `JSON.stringify`. + * This is typically set to the number of spaces to use to indent prettified JSON. + * @default 0 + */ + public spaces: number; + + public constructor(config: Partial = content?.json ?? {}) { + super(); + + const { escape, replacer, spaces } = config as Required; + + this.escape = escape; + this.replacer = replacer; + this.spaces = spaces; + } + +} diff --git a/packages/config/src/config/jsonp.config.ts b/packages/config/src/config/jsonp.config.ts new file mode 100644 index 0000000..4ddf873 --- /dev/null +++ b/packages/config/src/config/jsonp.config.ts @@ -0,0 +1,52 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +import Joi from "joi"; +import content from "#src/config-content"; +import { Node, Schema } from "#src/utils/index"; + + +export class JsonpConfig extends Node { + + public static SCHEMA: Schema = { + callback: Joi.string().default("callback"), + }; + + /** + * The JSONP callback name. + * @default "callback" + */ + public callback: number; + + + public constructor(config: Partial = content?.jsonp ?? {}) { + super(); + + const { callback } = config as Required; + + this.callback = callback; + } + +} diff --git a/packages/config/src/config/proxy.config.ts b/packages/config/src/config/proxy.config.ts new file mode 100644 index 0000000..7099dfc --- /dev/null +++ b/packages/config/src/config/proxy.config.ts @@ -0,0 +1,52 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +import Joi from "joi"; +import content from "#src/config-content"; +import { Node, Schema } from "#src/utils/index"; + +export class ProxyConfig extends Node { + + public static SCHEMA: Schema = { + trust: Joi.function().default(() => ((): boolean => false)), + }; + + /** + * Indicates whether the app is behind a front-facing proxy, + * and to use the X-Forwarded-* headers to determine the connection and the IP address of the client. + * @default () => false + */ + public trust: (ip: string, hopIndex: number) => boolean; + + + public constructor(config: Partial = content?.proxy ?? {}) { + super(); + + const { trust } = config as Required; + + this.trust = trust; + } + +} diff --git a/packages/config/src/config/query.config.ts b/packages/config/src/config/query.config.ts new file mode 100644 index 0000000..1469d44 --- /dev/null +++ b/packages/config/src/config/query.config.ts @@ -0,0 +1,53 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +import qs from "node:querystring"; +import Joi from "joi"; +import content from "#src/config-content"; +import { Node, Schema } from "#src/utils/index"; + +export class QueryConfig extends Node { + + public static SCHEMA: Schema = { + parser: Joi.function().default(() => qs.parse), + }; + + /** + * A custom query string parsing function will receive the complete query string, + * and must return an object of query keys and their values. + * @default qs.parse // node:querystring + */ + public parser: (str: string) => Record; + + + public constructor(config: Partial = content?.query ?? {}) { + super(); + + const { parser } = config as Required; + + this.parser = parser; + } + +} diff --git a/packages/config/src/config/server.config.ts b/packages/config/src/config/server.config.ts new file mode 100644 index 0000000..0d6c1d5 --- /dev/null +++ b/packages/config/src/config/server.config.ts @@ -0,0 +1,70 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +import Joi from "joi"; +import content from "#src/config-content"; +import { SERVER_PROTOCOL } from "#src/constants/index"; +import { Node, Schema } from "#src/utils/index"; + +export class ServerConfig extends Node { + + public static SCHEMA: Schema = { + hostname: Joi.string().hostname() + .default("localhost"), + port: Joi.number().port() + .default(3_000), + protocol: Joi.string().valid(...Object.values(SERVER_PROTOCOL)) + .default(SERVER_PROTOCOL.HTTP), + }; + + /** + * Server hostname. + * @default "localhost" + */ + public hostname: string; + + /** + * Server port. + * @default 3000 + */ + public port: number; + + /** + * Server protocol. + * @default "http" + */ + public protocol: SERVER_PROTOCOL; + + public constructor(config: Partial = content?.server ?? {}) { + super(); + + const { hostname, port, protocol } = config as Required; + + this.hostname = hostname; + this.port = port; + this.protocol = protocol; + } + +} diff --git a/packages/config/src/config/subdomain.config.ts b/packages/config/src/config/subdomain.config.ts new file mode 100644 index 0000000..71dde90 --- /dev/null +++ b/packages/config/src/config/subdomain.config.ts @@ -0,0 +1,52 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +import Joi from "joi"; +import content from "#src/config-content"; +import { Node, Schema } from "#src/utils/index"; + +export class SubdomainConfig extends Node { + + public static SCHEMA: Schema = { + offset: Joi.number().integer() + .min(0) + .default(2), + }; + + /** + * The number of dot-separated parts of the host to remove to access subdomain. + * @default 2 + */ + public offset: number; + + public constructor(config: Partial = content?.subdomain ?? {}) { + super(); + + const { offset } = config as Required; + + this.offset = offset; + } + +} diff --git a/packages/config/src/constants.ts b/packages/config/src/constants.ts deleted file mode 100644 index c5e32d6..0000000 --- a/packages/config/src/constants.ts +++ /dev/null @@ -1,173 +0,0 @@ -import qs from "node:querystring"; - -export const CONFIG_FILEPATH = `${ process.cwd() }/foxify.config.js`; - -export enum ENV { - DEVELOPMENT = "development", - PRODUCTION = "production", - TEST = "test", -} - -export enum SERVER_PROTOCOL { - HTTP = "http", - HTTPS = "https", -} - -export const DEFAULT: ConfigI = { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - env : process.env.NODE_ENV as ENV ?? ENV.DEVELOPMENT, - xPoweredBy: true, - workers : 1, - server : { - protocol: SERVER_PROTOCOL.HTTP, - hostname: "localhost", - port : 3000, - }, - subdomain: { - offset: 2, - }, - json: { - escape: false, - spaces: 0, - }, - jsonp: { - callback: "callback", - }, - query: { - parser: qs.parse, - }, - proxy: { - trust: () => false, - }, -}; - -/* ------------------------- Interfaces ------------------------- */ - -export interface ConfigI { - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readonly [config: string]: any; - - /** - * Node.js environment. - */ - readonly env: ENV; - - /** - * JSON config. - */ - readonly json: JsonConfigI; - - /** - * JSONP config. - */ - readonly jsonp: JsonpConfigI; - - /** - * Proxy config. - */ - readonly proxy: ProxyConfigI; - - /** - * Request query string config. - */ - readonly query: QueryConfigI; - - /** - * Server config. - */ - readonly server: ServerConfigI; - - /** - * Subdomain config. - */ - readonly subdomain: SubdomainConfigI; - - /** - * Number of Node.js cluster workers to be created. - * In case of `1` Node.js cluster workers won't be used. - */ - readonly workers: number; - - /** - * Indicates whether the "X-Powered-By" header should be present or not. - */ - readonly xPoweredBy: boolean; - - /** - * ETag response header value generator. - */ - etag?(body: Buffer | string, encoding?: BufferEncoding): string; -} - -export interface ServerConfigI { - - /** - * Server hostname. - */ - readonly hostname: string; - - /** - * Server port. - */ - readonly port: number; - - /** - * Server protocol. - */ - readonly protocol: SERVER_PROTOCOL; -} - -export interface SubdomainConfigI { - - /** - * The number of dot-separated parts of the host to remove to access subdomain. - */ - readonly offset: number; -} - -export interface JsonConfigI { - - /** - * Enable escaping JSON responses from the `res.json`, `res.jsonp`, and `res.send` APIs. - */ - readonly escape: boolean; - - /** - * The `space` argument used by `JSON.stringify`. - * This is typically set to the number of spaces to use to indent prettified JSON. - */ - readonly spaces: number; - - /** - * The `replacer` argument used by `JSON.stringify`. - */ - replacer?(key: string, value: unknown): unknown; -} - -export interface JsonpConfigI { - - /** - * The JSONP callback name. - */ - readonly callback: string; -} - -export interface QueryConfigI { - - /** - * A custom query string parsing function will receive the complete query string, - * and must return an object of query keys and their values. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - parser(str: string): Record; -} - -export interface ProxyConfigI { - - /** - * Indicates whether the app is behind a front-facing proxy, - * and to use the X-Forwarded-* headers to determine the connection and the IP address of the client. - */ - trust(ip: string, hopIndex: number): boolean; -} diff --git a/packages/config/src/constants/CONFIG_FILEPATH.ts b/packages/config/src/constants/CONFIG_FILEPATH.ts new file mode 100644 index 0000000..40fb255 --- /dev/null +++ b/packages/config/src/constants/CONFIG_FILEPATH.ts @@ -0,0 +1,28 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +import { cwd } from "node:process"; + +export const CONFIG_FILEPATH = `${ cwd() }/foxify.config.js`; diff --git a/packages/config/src/constants/ENV.ts b/packages/config/src/constants/ENV.ts new file mode 100644 index 0000000..f14af86 --- /dev/null +++ b/packages/config/src/constants/ENV.ts @@ -0,0 +1,30 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +export enum ENV { + DEVELOPMENT = "development", + PRODUCTION = "production", + TEST = "test", +} diff --git a/packages/config/src/constants/SERVER_PROTOCOL.ts b/packages/config/src/constants/SERVER_PROTOCOL.ts new file mode 100644 index 0000000..a5a3fde --- /dev/null +++ b/packages/config/src/constants/SERVER_PROTOCOL.ts @@ -0,0 +1,29 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +export enum SERVER_PROTOCOL { + HTTP = "http", + HTTPS = "https", +} diff --git a/packages/config/src/constants/index.ts b/packages/config/src/constants/index.ts new file mode 100644 index 0000000..2c5904a --- /dev/null +++ b/packages/config/src/constants/index.ts @@ -0,0 +1,28 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +export * from "./CONFIG_FILEPATH.js"; +export * from "./ENV.js"; +export * from "./SERVER_PROTOCOL.js"; diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index a7769df..bc8acce 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -1,116 +1,3 @@ -import os from "node:os"; -import Joi from "joi"; -import content from "#src/config"; -import { - ConfigI, - DEFAULT, - ENV, - JsonConfigI, - JsonpConfigI, - ProxyConfigI, - QueryConfigI, - SERVER_PROTOCOL, - ServerConfigI, - SubdomainConfigI, -} from "#src/constants"; - -/* ------------------------- Validate the config data ------------------------- */ - -const { value: config, error } = Joi - .object() - .keys({ - env: Joi.string().valid(...Object.values(ENV)) - .default(DEFAULT.env), - xPoweredBy: Joi.boolean().default(DEFAULT.xPoweredBy), - workers : Joi.number().integer() - .min(1) - .max(os.cpus().length) - .default(DEFAULT.workers), - etag : Joi.function(), - server: Joi - .object() - .keys({ - protocol: Joi.string().valid(...Object.values(SERVER_PROTOCOL)) - .default(DEFAULT.server.protocol), - hostname: Joi.string().hostname() - .default(DEFAULT.server.hostname), - port: Joi.number().port() - .default(DEFAULT.server.port), - }) - .default(DEFAULT.server), - subdomain: Joi - .object() - .keys({ - offset: Joi.number().integer() - .min(0) - .default(DEFAULT.subdomain.offset), - }) - .default(DEFAULT.subdomain), - json: Joi - .object() - .keys({ - escape : Joi.boolean().default(DEFAULT.json.escape), - replacer: Joi.function(), - spaces : Joi.number().integer() - .min(0) - .default(DEFAULT.json.spaces), - }) - .default(DEFAULT.json), - jsonp: Joi - .object() - .keys({ - callback: Joi.string().default(DEFAULT.jsonp.callback), - }) - .default(DEFAULT.jsonp), - query: Joi - .object() - .keys({ - parser: Joi.function().default(DEFAULT.query.parser), - }) - .default(DEFAULT.query), - proxy: Joi - .object() - .keys({ - trust: Joi.function().default(DEFAULT.proxy.trust), - }) - .default(DEFAULT.proxy), - }) - .unknown() - .validate(content); - -if (error != null) throw error; - -/* ------------------------- Freeze the config data ------------------------- */ - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function freeze>(obj: T): T { - for (const key in obj) { - if (!Object.hasOwn(obj, key)) continue; - - const value = obj[key]; - - if (typeof value !== "object" || value == null) continue; - - obj[key] = freeze(value); - } - - return Object.freeze(obj); -} - -const CONFIG: ConfigI = config.env === ENV.TEST ? config : freeze(config); - -/* ------------------------- Exports ------------------------- */ - -export default CONFIG; - -export { ENV, SERVER_PROTOCOL }; - -export type { - ConfigI, - ServerConfigI, - SubdomainConfigI, - JsonConfigI, - JsonpConfigI, - QueryConfigI, - ProxyConfigI, -}; +export { config as default } from "#src/config/index"; +export * from "#src/config/index"; +export * from "#src/constants/index"; diff --git a/packages/config/src/utils/Node.ts b/packages/config/src/utils/Node.ts new file mode 100644 index 0000000..9c2108d --- /dev/null +++ b/packages/config/src/utils/Node.ts @@ -0,0 +1,61 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +import Joi from "joi"; + +export type DirectConfigKey = { + [K in keyof T]: T[K] extends object ? never : K; +}[keyof T]; + +export type Schema = Record, Joi.Schema>; + +export abstract class Node { + + public static SCHEMA: Record; + + protected constructor() { + const SCHEMA = (this.constructor as typeof Node).SCHEMA; + + // eslint-disable-next-line no-constructor-return + return new Proxy(this, { + // eslint-disable-next-line max-params + set(target: Node, p: string, newValue: unknown, receiver: unknown): boolean { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((target as any)[p] instanceof Node) throw new TypeError(`Config override for "${ p }" is not allowed.`); + + if (p in SCHEMA) { + const { value, error } = SCHEMA[p].validate(newValue); + + if (error != null) throw error; + + newValue = value; + } + + return Reflect.set(target, p, newValue, receiver); + }, + }); + } + +} diff --git a/packages/config/src/utils/index.ts b/packages/config/src/utils/index.ts new file mode 100644 index 0000000..329bd8c --- /dev/null +++ b/packages/config/src/utils/index.ts @@ -0,0 +1,26 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +export * from "./Node.js"; diff --git a/packages/config/tests/index.test.ts b/packages/config/tests/index.test.ts index a86c08b..8f95675 100644 --- a/packages/config/tests/index.test.ts +++ b/packages/config/tests/index.test.ts @@ -1,6 +1,61 @@ -import { DEFAULT } from "#src/constants"; -import CONFIG from "@foxify/config"; +import qs from "node:querystring"; +import { config } from "@foxify/config"; it("should use default config", () => { - expect(CONFIG).toEqual(DEFAULT); + expect(config).toMatchObject({ + env : "test", + // eslint-disable-next-line no-undefined + etag: undefined, + json: { + escape : false, + // eslint-disable-next-line no-undefined + replacer: undefined, + spaces : 0, + }, + jsonp: { + callback: "callback", + }, + proxy: { + }, + query: { + parser: qs.parse, + }, + server: { + hostname: "localhost", + port : 3000, + protocol: "http", + }, + subdomain: { + offset: 2, + }, + workers : 1, + xPoweredBy: true, + }); + + expect(config.proxy.trust).toBeInstanceOf(Function); + expect(config.proxy.trust.toString()).toBe("() => false"); +}); + +it("should update json.escape", () => { + expect(config.json.escape).toBe(false); + + config.json.escape = true; + + expect(config.json.escape).toBe(true); + + config.json.escape = false; + + expect(config.json).toEqual({ + escape : false, + // eslint-disable-next-line no-undefined + replacer: undefined, + spaces : 0, + }); +}); + +it("shouldn't allow sub-config updates", () => { + expect(() => { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + config.json = {} as never; + }).toThrowError("Config override for \"json\" is not allowed."); }); From 04ea7e52fa3c2d7c2a78306fbd6f97db201050aa Mon Sep 17 00:00:00 2001 From: Ardalan Amini Date: Sat, 1 Jul 2023 15:42:35 +0330 Subject: [PATCH 2/6] feat(config): move etag property from Config to ServerConfig This change moves the `etag` property from `Config` to `ServerConfig` class. --- packages/config/src/config/config.ts | 9 +-------- packages/config/src/config/server.config.ts | 9 ++++++++- packages/config/tests/index.test.ts | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/config/src/config/config.ts b/packages/config/src/config/config.ts index dacae0a..97d6a71 100644 --- a/packages/config/src/config/config.ts +++ b/packages/config/src/config/config.ts @@ -40,7 +40,6 @@ export class Config extends Node { public static SCHEMA: Schema = { env: Joi.string().valid(...Object.values(ENV)) .default(process.env.NODE_ENV as ENV | undefined ?? ENV.DEVELOPMENT), - etag : Joi.function(), workers: Joi.number().integer() .min(1) .max(os.cpus().length) @@ -54,11 +53,6 @@ export class Config extends Node { */ public env: ENV; - /** - * ETag response header value generator. - */ - public etag?: (body: Buffer | string, encoding?: BufferEncoding) => string; - /** * JSON config. */ @@ -105,10 +99,9 @@ export class Config extends Node { public constructor(config: Partial = content ?? {}) { super(); - const { env, etag, workers, xPoweredBy } = config as Required; + const { env, workers, xPoweredBy } = config as Required; this.env = env; - this.etag = etag; this.workers = workers; this.xPoweredBy = xPoweredBy; } diff --git a/packages/config/src/config/server.config.ts b/packages/config/src/config/server.config.ts index 0d6c1d5..f63aed8 100644 --- a/packages/config/src/config/server.config.ts +++ b/packages/config/src/config/server.config.ts @@ -31,6 +31,7 @@ import { Node, Schema } from "#src/utils/index"; export class ServerConfig extends Node { public static SCHEMA: Schema = { + etag : Joi.function(), hostname: Joi.string().hostname() .default("localhost"), port: Joi.number().port() @@ -39,6 +40,11 @@ export class ServerConfig extends Node { .default(SERVER_PROTOCOL.HTTP), }; + /** + * ETag response header value generator. + */ + public etag?: (body: Buffer | string, encoding?: BufferEncoding) => string; + /** * Server hostname. * @default "localhost" @@ -60,8 +66,9 @@ export class ServerConfig extends Node { public constructor(config: Partial = content?.server ?? {}) { super(); - const { hostname, port, protocol } = config as Required; + const { etag, hostname, port, protocol } = config as Required; + this.etag = etag; this.hostname = hostname; this.port = port; this.protocol = protocol; diff --git a/packages/config/tests/index.test.ts b/packages/config/tests/index.test.ts index 8f95675..7b03f4a 100644 --- a/packages/config/tests/index.test.ts +++ b/packages/config/tests/index.test.ts @@ -4,8 +4,6 @@ import { config } from "@foxify/config"; it("should use default config", () => { expect(config).toMatchObject({ env : "test", - // eslint-disable-next-line no-undefined - etag: undefined, json: { escape : false, // eslint-disable-next-line no-undefined @@ -21,6 +19,8 @@ it("should use default config", () => { parser: qs.parse, }, server: { + // eslint-disable-next-line no-undefined + etag : undefined, hostname: "localhost", port : 3000, protocol: "http", From daf97a0ef801f30e438e30909f8ebc855074c515 Mon Sep 17 00:00:00 2001 From: Ardalan Amini Date: Sat, 1 Jul 2023 15:46:57 +0330 Subject: [PATCH 3/6] feat(config): add event emitter for config changes This commit adds an event emitter to the config module that emits a `change` event whenever a configuration value is modified. The event includes the name of the modified property, the new value, and the old value. This allows other parts of the application to listen for these events and react accordingly. --- packages/config/src/config/json.config.ts | 2 +- packages/config/src/config/jsonp.config.ts | 2 +- packages/config/src/config/proxy.config.ts | 2 +- packages/config/src/config/query.config.ts | 2 +- packages/config/src/config/server.config.ts | 2 +- .../config/src/config/subdomain.config.ts | 2 +- packages/config/src/events.ts | 28 +++++++++++++++++++ packages/config/src/index.ts | 1 + packages/config/src/utils/Node.ts | 15 ++++++++-- packages/config/tests/index.test.ts | 16 ++++++++++- 10 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 packages/config/src/events.ts diff --git a/packages/config/src/config/json.config.ts b/packages/config/src/config/json.config.ts index d72a622..f416e90 100644 --- a/packages/config/src/config/json.config.ts +++ b/packages/config/src/config/json.config.ts @@ -56,7 +56,7 @@ export class JsonConfig extends Node { public spaces: number; public constructor(config: Partial = content?.json ?? {}) { - super(); + super("json"); const { escape, replacer, spaces } = config as Required; diff --git a/packages/config/src/config/jsonp.config.ts b/packages/config/src/config/jsonp.config.ts index 4ddf873..8cb547b 100644 --- a/packages/config/src/config/jsonp.config.ts +++ b/packages/config/src/config/jsonp.config.ts @@ -42,7 +42,7 @@ export class JsonpConfig extends Node { public constructor(config: Partial = content?.jsonp ?? {}) { - super(); + super("jsonp"); const { callback } = config as Required; diff --git a/packages/config/src/config/proxy.config.ts b/packages/config/src/config/proxy.config.ts index 7099dfc..bbca539 100644 --- a/packages/config/src/config/proxy.config.ts +++ b/packages/config/src/config/proxy.config.ts @@ -42,7 +42,7 @@ export class ProxyConfig extends Node { public constructor(config: Partial = content?.proxy ?? {}) { - super(); + super("proxy"); const { trust } = config as Required; diff --git a/packages/config/src/config/query.config.ts b/packages/config/src/config/query.config.ts index 1469d44..bc0bad6 100644 --- a/packages/config/src/config/query.config.ts +++ b/packages/config/src/config/query.config.ts @@ -43,7 +43,7 @@ export class QueryConfig extends Node { public constructor(config: Partial = content?.query ?? {}) { - super(); + super("query"); const { parser } = config as Required; diff --git a/packages/config/src/config/server.config.ts b/packages/config/src/config/server.config.ts index f63aed8..aa20cef 100644 --- a/packages/config/src/config/server.config.ts +++ b/packages/config/src/config/server.config.ts @@ -64,7 +64,7 @@ export class ServerConfig extends Node { public protocol: SERVER_PROTOCOL; public constructor(config: Partial = content?.server ?? {}) { - super(); + super("server"); const { etag, hostname, port, protocol } = config as Required; diff --git a/packages/config/src/config/subdomain.config.ts b/packages/config/src/config/subdomain.config.ts index 71dde90..c6aa4f5 100644 --- a/packages/config/src/config/subdomain.config.ts +++ b/packages/config/src/config/subdomain.config.ts @@ -42,7 +42,7 @@ export class SubdomainConfig extends Node { public offset: number; public constructor(config: Partial = content?.subdomain ?? {}) { - super(); + super("subdomain"); const { offset } = config as Required; diff --git a/packages/config/src/events.ts b/packages/config/src/events.ts new file mode 100644 index 0000000..e668ece --- /dev/null +++ b/packages/config/src/events.ts @@ -0,0 +1,28 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +import { EventEmitter } from "node:events"; + +export const events = new EventEmitter; diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index bc8acce..eb63afe 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -1,3 +1,4 @@ export { config as default } from "#src/config/index"; export * from "#src/config/index"; export * from "#src/constants/index"; +export * from "#src/events"; diff --git a/packages/config/src/utils/Node.ts b/packages/config/src/utils/Node.ts index 9c2108d..0e7006b 100644 --- a/packages/config/src/utils/Node.ts +++ b/packages/config/src/utils/Node.ts @@ -24,6 +24,7 @@ */ import Joi from "joi"; +import { events } from "#src/events"; export type DirectConfigKey = { [K in keyof T]: T[K] extends object ? never : K; @@ -35,15 +36,19 @@ export abstract class Node { public static SCHEMA: Record; - protected constructor() { + protected constructor(prefix = "") { const SCHEMA = (this.constructor as typeof Node).SCHEMA; + if (prefix !== "") prefix += "."; + // eslint-disable-next-line no-constructor-return return new Proxy(this, { // eslint-disable-next-line max-params set(target: Node, p: string, newValue: unknown, receiver: unknown): boolean { // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((target as any)[p] instanceof Node) throw new TypeError(`Config override for "${ p }" is not allowed.`); + const oldValue = (target as any)[p]; + + if (oldValue instanceof Node) throw new TypeError(`Config override for "${ p }" is not allowed.`); if (p in SCHEMA) { const { value, error } = SCHEMA[p].validate(newValue); @@ -53,7 +58,11 @@ export abstract class Node { newValue = value; } - return Reflect.set(target, p, newValue, receiver); + const result = Reflect.set(target, p, newValue, receiver); + + if (result) events.emit("change", `${ prefix }${ p }`, newValue, oldValue); + + return result; }, }); } diff --git a/packages/config/tests/index.test.ts b/packages/config/tests/index.test.ts index 7b03f4a..df8a8be 100644 --- a/packages/config/tests/index.test.ts +++ b/packages/config/tests/index.test.ts @@ -1,5 +1,5 @@ import qs from "node:querystring"; -import { config } from "@foxify/config"; +import { config, events } from "@foxify/config"; it("should use default config", () => { expect(config).toMatchObject({ @@ -37,14 +37,28 @@ it("should use default config", () => { }); it("should update json.escape", () => { + const listener = jest.fn(); + + events.on("change", listener); + expect(config.json.escape).toBe(false); config.json.escape = true; + expect(listener).toBeCalledTimes(1); + expect(listener).toBeCalledWith("json.escape", true, false); + + listener.mockReset(); + expect(config.json.escape).toBe(true); config.json.escape = false; + expect(listener).toBeCalledTimes(1); + expect(listener).toBeCalledWith("json.escape", false, true); + + events.off("change", listener); + expect(config.json).toEqual({ escape : false, // eslint-disable-next-line no-undefined From 7f72a221869f08ce7ee73934aa79b2ffd987a543 Mon Sep 17 00:00:00 2001 From: Ardalan Amini Date: Sun, 2 Jul 2023 16:59:55 +0330 Subject: [PATCH 4/6] feat(config): Add RouterConfig and update other config classes - Added `RouterConfig` class to handle router configuration. - Updated `Config` class to include `router` property. - Updated `index.ts` to export `Config` type. - Updated `JsonpConfig` class to fix the type of the `callback` property from number to string. - Updated `ProxyConfig` class to fix the type of the `trust` property and added validation for different types of values. - Updated `QueryConfig` class to fix the type of the `parser` property and added validation for different types of values. - Updated `ServerConfig` class to add support for ETag generation and HTTP2/HTTPS certificates. --- packages/config/package.json | 10 ++- packages/config/src/config/config.ts | 6 ++ packages/config/src/config/index.ts | 2 + packages/config/src/config/jsonp.config.ts | 2 +- packages/config/src/config/proxy.config.ts | 29 +++++++-- packages/config/src/config/query.config.ts | 40 ++++++++++-- packages/config/src/config/router.config.ts | 72 +++++++++++++++++++++ packages/config/src/config/server.config.ts | 60 +++++++++++++++-- packages/config/tests/index.test.ts | 4 +- pnpm-lock.yaml | 21 +++++- 10 files changed, 226 insertions(+), 20 deletions(-) create mode 100644 packages/config/src/config/router.config.ts diff --git a/packages/config/package.json b/packages/config/package.json index b836f23..5cf1ada 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -61,7 +61,15 @@ "@types/node": ">=16" }, "dependencies": { - "joi": "^17.9.2" + "etag": "^1.8.1", + "joi": "^17.9.2", + "proxy-addr": "^2.0.7", + "qs": "^6.11.2" + }, + "devDependencies": { + "@types/etag": "^1.8.1", + "@types/proxy-addr": "^2.0.0", + "@types/qs": "^6.9.7" }, "publishConfig": { "access": "public", diff --git a/packages/config/src/config/config.ts b/packages/config/src/config/config.ts index 97d6a71..aced21c 100644 --- a/packages/config/src/config/config.ts +++ b/packages/config/src/config/config.ts @@ -32,6 +32,7 @@ import { JsonConfig } from "./json.config.js"; import { JsonpConfig } from "./jsonp.config.js"; import { ProxyConfig } from "./proxy.config.js"; import { QueryConfig } from "./query.config.js"; +import { RouterConfig } from "./router.config.js"; import { ServerConfig } from "./server.config.js"; import { SubdomainConfig } from "./subdomain.config.js"; @@ -73,6 +74,11 @@ export class Config extends Node { */ public query = new QueryConfig; + /** + * Router config. + */ + public router = new RouterConfig; + /** * Server config. */ diff --git a/packages/config/src/config/index.ts b/packages/config/src/config/index.ts index a8088ab..c2e2ad2 100644 --- a/packages/config/src/config/index.ts +++ b/packages/config/src/config/index.ts @@ -25,4 +25,6 @@ import { Config } from "./config.js"; +export type { Config }; + export const config = new Config; diff --git a/packages/config/src/config/jsonp.config.ts b/packages/config/src/config/jsonp.config.ts index 8cb547b..52f5ec0 100644 --- a/packages/config/src/config/jsonp.config.ts +++ b/packages/config/src/config/jsonp.config.ts @@ -38,7 +38,7 @@ export class JsonpConfig extends Node { * The JSONP callback name. * @default "callback" */ - public callback: number; + public callback: string; public constructor(config: Partial = content?.jsonp ?? {}) { diff --git a/packages/config/src/config/proxy.config.ts b/packages/config/src/config/proxy.config.ts index bbca539..cce9d82 100644 --- a/packages/config/src/config/proxy.config.ts +++ b/packages/config/src/config/proxy.config.ts @@ -24,21 +24,40 @@ */ import Joi from "joi"; +import { compile } from "proxy-addr"; import content from "#src/config-content"; import { Node, Schema } from "#src/utils/index"; -export class ProxyConfig extends Node { +export interface ProxyConfig { - public static SCHEMA: Schema = { - trust: Joi.function().default(() => ((): boolean => false)), - }; + /** + * Indicates whether the app is behind a front-facing proxy, + * and to use the X-Forwarded-* headers to determine the connection and the IP address of the client. + * @default () => false + */ + get trust(): (ip: string, hopIndex: number) => boolean; /** * Indicates whether the app is behind a front-facing proxy, * and to use the X-Forwarded-* headers to determine the connection and the IP address of the client. * @default () => false */ - public trust: (ip: string, hopIndex: number) => boolean; + set trust(value: boolean | number | string | ((ip: string, hopIndex: number) => boolean)); +} + +export class ProxyConfig extends Node { + + public static SCHEMA: Schema = { + trust: Joi.alternatives().try( + Joi.function(), + Joi.boolean().custom(value => ((): boolean => value)), + Joi.string().custom(value => compile(value.split(/ *, */))), + Joi.number().integer() + .positive() + .custom(value => ((ip: string, hopIndex: number): boolean => hopIndex < value)), + ) + .default(() => ((): boolean => false)), + }; public constructor(config: Partial = content?.proxy ?? {}) { diff --git a/packages/config/src/config/query.config.ts b/packages/config/src/config/query.config.ts index bc0bad6..ed2b468 100644 --- a/packages/config/src/config/query.config.ts +++ b/packages/config/src/config/query.config.ts @@ -23,23 +23,51 @@ * */ -import qs from "node:querystring"; import Joi from "joi"; +import { parse, ParsedQs } from "qs"; import content from "#src/config-content"; import { Node, Schema } from "#src/utils/index"; -export class QueryConfig extends Node { +export interface QueryConfig { - public static SCHEMA: Schema = { - parser: Joi.function().default(() => qs.parse), - }; + /** + * A custom query string parsing function will receive the complete query string, + * and must return an object of query keys and their values. + * @default qs.parse // node:querystring + */ + get parser(): (str: string) => ParsedQs; /** * A custom query string parsing function will receive the complete query string, * and must return an object of query keys and their values. * @default qs.parse // node:querystring */ - public parser: (str: string) => Record; + set parser(parser: boolean | "extended" | "simple" | ((str: string) => ParsedQs)); +} + +export class QueryConfig extends Node { + + public static SCHEMA: Schema = { + parser: Joi.alternatives().try( + Joi.function(), + Joi.boolean().custom((value) => { + if (value) return parse; + + return (): ParsedQs => ({}); + }), + Joi.string().custom((value) => { + switch (value) { + case "simple": + return parse; + case "extended": + return (str: string): ParsedQs => parse(str, { allowPrototypes: true }); + default: + throw new Error(`Unexpected value: ${ value }`); + } + }), + ) + .default(() => parse), + }; public constructor(config: Partial = content?.query ?? {}) { diff --git a/packages/config/src/config/router.config.ts b/packages/config/src/config/router.config.ts new file mode 100644 index 0000000..24fed20 --- /dev/null +++ b/packages/config/src/config/router.config.ts @@ -0,0 +1,72 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +import Joi from "joi"; +import content from "#src/config-content"; +import { Node, Schema } from "#src/utils/index"; + +export class RouterConfig extends Node { + + public static SCHEMA: Schema = { + allowUnsafeRegex : Joi.boolean().default(false), + caseSensitive : Joi.boolean().default(true), + ignoreTrailingSlash: Joi.boolean().default(false), + maxParamLength : Joi.number().integer() + .positive() + .default(100), + }; + + /** + * @default false + */ + public allowUnsafeRegex: boolean; + + /** + * @default true + */ + public caseSensitive: boolean; + + /** + * @default false + */ + public ignoreTrailingSlash: boolean; + + /** + * @default 100 + */ + public maxParamLength: number; + + public constructor(config: Partial = content?.subdomain ?? {}) { + super("router"); + + const { allowUnsafeRegex, caseSensitive, ignoreTrailingSlash, maxParamLength } = config as Required; + + this.allowUnsafeRegex = allowUnsafeRegex; + this.caseSensitive = caseSensitive; + this.ignoreTrailingSlash = ignoreTrailingSlash; + this.maxParamLength = maxParamLength; + } + +} diff --git a/packages/config/src/config/server.config.ts b/packages/config/src/config/server.config.ts index aa20cef..6369a26 100644 --- a/packages/config/src/config/server.config.ts +++ b/packages/config/src/config/server.config.ts @@ -23,17 +23,62 @@ * */ +import createETag from "etag"; import Joi from "joi"; import content from "#src/config-content"; import { SERVER_PROTOCOL } from "#src/constants/index"; import { Node, Schema } from "#src/utils/index"; +const createETagGenerator = (weak: boolean) => function generateETag( + body: Buffer | string, + encoding?: BufferEncoding, +): string { + return createETag(Buffer.isBuffer(body) ? body : Buffer.from(body, encoding), { + weak, + }); +}; + +export interface ServerConfig { + + /** + * ETag response header value generator. + */ + get etag(): ((body: Buffer | string, encoding?: BufferEncoding) => string) | undefined; + + /** + * ETag response header value generator. + */ + set etag( + etag: boolean | "strong" | "weak" | ((body: Buffer | string, encoding?: BufferEncoding) => string) | undefined, + ); +} + export class ServerConfig extends Node { public static SCHEMA: Schema = { - etag : Joi.function(), + cert: Joi.string(), + etag: Joi.alternatives( + Joi.function(), + Joi.boolean().custom((value) => { + if (value) return createETagGenerator(true); + + // eslint-disable-next-line no-undefined + return undefined; + }), + Joi.string().custom((value) => { + switch (value) { + case "weak": + return createETagGenerator(true); + case "strong": + return createETagGenerator(false); + default: + throw new Error(`Unexpected value: ${ value }`); + } + }), + ), hostname: Joi.string().hostname() .default("localhost"), + key : Joi.string(), port: Joi.number().port() .default(3_000), protocol: Joi.string().valid(...Object.values(SERVER_PROTOCOL)) @@ -41,9 +86,9 @@ export class ServerConfig extends Node { }; /** - * ETag response header value generator. + * HTTP2/HTTPS cert chain in PEM format. */ - public etag?: (body: Buffer | string, encoding?: BufferEncoding) => string; + public cert?: string; /** * Server hostname. @@ -51,6 +96,11 @@ export class ServerConfig extends Node { */ public hostname: string; + /** + * HTTP2/HTTPS private key in PEM format. + */ + public key?: string; + /** * Server port. * @default 3000 @@ -66,10 +116,12 @@ export class ServerConfig extends Node { public constructor(config: Partial = content?.server ?? {}) { super("server"); - const { etag, hostname, port, protocol } = config as Required; + const { cert, etag, hostname, key, port, protocol } = config as Required; + this.cert = cert; this.etag = etag; this.hostname = hostname; + this.key = key; this.port = port; this.protocol = protocol; } diff --git a/packages/config/tests/index.test.ts b/packages/config/tests/index.test.ts index df8a8be..23e2119 100644 --- a/packages/config/tests/index.test.ts +++ b/packages/config/tests/index.test.ts @@ -1,4 +1,4 @@ -import qs from "node:querystring"; +import { parse } from "qs"; import { config, events } from "@foxify/config"; it("should use default config", () => { @@ -16,7 +16,7 @@ it("should use default config", () => { proxy: { }, query: { - parser: qs.parse, + parser: parse, }, server: { // eslint-disable-next-line no-undefined diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a527ee4..cccf68f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -325,9 +325,28 @@ importers: '@types/node': specifier: '>=16' version: 18.11.9 + etag: + specifier: ^1.8.1 + version: 1.8.1 joi: specifier: ^17.9.2 version: 17.9.2 + proxy-addr: + specifier: ^2.0.7 + version: 2.0.7 + qs: + specifier: ^6.11.2 + version: 6.11.2 + devDependencies: + '@types/etag': + specifier: ^1.8.1 + version: 1.8.1 + '@types/proxy-addr': + specifier: ^2.0.0 + version: 2.0.0 + '@types/qs': + specifier: ^6.9.7 + version: 6.9.7 packages/foxify: dependencies: @@ -8984,7 +9003,7 @@ packages: once: 1.4.0 pidusage: 3.0.2 pino: 8.7.0 - qs: 6.11.0 + qs: 6.11.2 restify-errors: 8.0.2 semver: 7.3.8 send: 0.18.0 From a1ce0e538be253651275757abff601470e0b21fd Mon Sep 17 00:00:00 2001 From: Ardalan Amini Date: Sun, 2 Jul 2023 18:44:14 +0330 Subject: [PATCH 5/6] feat(config): add config interface for initial config file usage Signed-off-by: Ardalan Amini --- packages/config/src/config/config.ts | 77 ++++++++++++++++--- packages/config/src/config/index.ts | 4 +- packages/config/src/config/json.config.ts | 21 +++++ packages/config/src/config/jsonp.config.ts | 9 +++ packages/config/src/config/proxy.config.ts | 10 +++ packages/config/src/config/query.config.ts | 14 +++- packages/config/src/config/router.config.ts | 31 ++++++++ packages/config/src/config/server.config.ts | 36 +++++++++ .../config/src/config/subdomain.config.ts | 9 +++ packages/config/tests/index.test.ts | 14 +++- 10 files changed, 207 insertions(+), 18 deletions(-) diff --git a/packages/config/src/config/config.ts b/packages/config/src/config/config.ts index aced21c..f1c6eaf 100644 --- a/packages/config/src/config/config.ts +++ b/packages/config/src/config/config.ts @@ -23,18 +23,75 @@ * */ -import os from "node:os"; +import { cpus } from "node:os"; import Joi from "joi"; import content from "#src/config-content"; import { ENV } from "#src/constants/index"; -import { Node, Schema } from "#src/utils/index"; -import { JsonConfig } from "./json.config.js"; -import { JsonpConfig } from "./jsonp.config.js"; -import { ProxyConfig } from "./proxy.config.js"; -import { QueryConfig } from "./query.config.js"; -import { RouterConfig } from "./router.config.js"; -import { ServerConfig } from "./server.config.js"; -import { SubdomainConfig } from "./subdomain.config.js"; +import { Node, type Schema } from "#src/utils/index"; +import { JsonConfig, type JsonConfigI } from "./json.config.js"; +import { JsonpConfig, type JsonpConfigI } from "./jsonp.config.js"; +import { ProxyConfig, type ProxyConfigI } from "./proxy.config.js"; +import { QueryConfig, type QueryConfigI } from "./query.config.js"; +import { RouterConfig, type RouterConfigI } from "./router.config.js"; +import { ServerConfig, type ServerConfigI } from "./server.config.js"; +import { SubdomainConfig, type SubdomainConfigI } from "./subdomain.config.js"; + +export interface ConfigI { + + /** + * Node.js environment. + * @default process.env.NODE_ENV ?? "development" + */ + env?: ENV; + + /** + * JSON config. + */ + readonly json?: JsonConfigI; + + /** + * JSONP config. + */ + readonly jsonp?: JsonpConfigI; + + /** + * Proxy config. + */ + readonly proxy?: ProxyConfigI; + + /** + * Request query string config. + */ + readonly query?: QueryConfigI; + + /** + * Router config. + */ + readonly router?: RouterConfigI; + + /** + * Server config. + */ + readonly server?: ServerConfigI; + + /** + * Subdomain config. + */ + readonly subdomain?: SubdomainConfigI; + + /** + * Number of Node.js cluster workers to be created. + * In case of `1` Node.js cluster workers won't be used. + * @default 1 + */ + workers?: number; + + /** + * Indicates whether the "X-Powered-By" header should be present or not. + * @default true + */ + xPoweredBy?: boolean; +} export class Config extends Node { @@ -43,7 +100,7 @@ export class Config extends Node { .default(process.env.NODE_ENV as ENV | undefined ?? ENV.DEVELOPMENT), workers: Joi.number().integer() .min(1) - .max(os.cpus().length) + .max(cpus().length) .default(1), xPoweredBy: Joi.boolean().default(true), }; diff --git a/packages/config/src/config/index.ts b/packages/config/src/config/index.ts index c2e2ad2..6f54b98 100644 --- a/packages/config/src/config/index.ts +++ b/packages/config/src/config/index.ts @@ -23,8 +23,8 @@ * */ -import { Config } from "./config.js"; +import { Config, ConfigI } from "./config.js"; -export type { Config }; +export type { Config, ConfigI }; export const config = new Config; diff --git a/packages/config/src/config/json.config.ts b/packages/config/src/config/json.config.ts index f416e90..7d97168 100644 --- a/packages/config/src/config/json.config.ts +++ b/packages/config/src/config/json.config.ts @@ -27,6 +27,27 @@ import Joi from "joi"; import content from "#src/config-content"; import { Node, Schema } from "#src/utils/index"; +export interface JsonConfigI { + + /** + * Enable escaping JSON responses from the `res.json`, `res.jsonp`, and `res.send` APIs. + * @default false + */ + escape?: boolean; + + /** + * The `space` argument used by `JSON.stringify`. + * This is typically set to the number of spaces to use to indent prettified JSON. + * @default 0 + */ + spaces?: number; + + /** + * The `replacer` argument used by `JSON.stringify`. + */ + replacer?(key: string, value: unknown): unknown; +} + export class JsonConfig extends Node { public static SCHEMA: Schema = { diff --git a/packages/config/src/config/jsonp.config.ts b/packages/config/src/config/jsonp.config.ts index 52f5ec0..3ea482d 100644 --- a/packages/config/src/config/jsonp.config.ts +++ b/packages/config/src/config/jsonp.config.ts @@ -27,6 +27,15 @@ import Joi from "joi"; import content from "#src/config-content"; import { Node, Schema } from "#src/utils/index"; +export interface JsonpConfigI { + + /** + * The JSONP callback name. + * @default "callback" + */ + callback?: string; +} + export class JsonpConfig extends Node { diff --git a/packages/config/src/config/proxy.config.ts b/packages/config/src/config/proxy.config.ts index cce9d82..75aa0f0 100644 --- a/packages/config/src/config/proxy.config.ts +++ b/packages/config/src/config/proxy.config.ts @@ -28,6 +28,16 @@ import { compile } from "proxy-addr"; import content from "#src/config-content"; import { Node, Schema } from "#src/utils/index"; +export interface ProxyConfigI { + + /** + * Indicates whether the app is behind a front-facing proxy, + * and to use the X-Forwarded-* headers to determine the connection and the IP address of the client. + * @default () => false + */ + trust?: boolean | number | string | ((ip: string, hopIndex: number) => boolean); +} + export interface ProxyConfig { /** diff --git a/packages/config/src/config/query.config.ts b/packages/config/src/config/query.config.ts index ed2b468..2d8dfe1 100644 --- a/packages/config/src/config/query.config.ts +++ b/packages/config/src/config/query.config.ts @@ -28,19 +28,29 @@ import { parse, ParsedQs } from "qs"; import content from "#src/config-content"; import { Node, Schema } from "#src/utils/index"; +export interface QueryConfigI { + + /** + * A custom query string parsing function will receive the complete query string, + * and must return an object of query keys and their values. + * @default qs.parse + */ + parser?: boolean | "extended" | "simple" | ((str: string) => ParsedQs); +} + export interface QueryConfig { /** * A custom query string parsing function will receive the complete query string, * and must return an object of query keys and their values. - * @default qs.parse // node:querystring + * @default qs.parse */ get parser(): (str: string) => ParsedQs; /** * A custom query string parsing function will receive the complete query string, * and must return an object of query keys and their values. - * @default qs.parse // node:querystring + * @default qs.parse */ set parser(parser: boolean | "extended" | "simple" | ((str: string) => ParsedQs)); } diff --git a/packages/config/src/config/router.config.ts b/packages/config/src/config/router.config.ts index 24fed20..f471ad6 100644 --- a/packages/config/src/config/router.config.ts +++ b/packages/config/src/config/router.config.ts @@ -27,6 +27,33 @@ import Joi from "joi"; import content from "#src/config-content"; import { Node, Schema } from "#src/utils/index"; +export interface RouterConfigI { + + /** + * Indicates whether the router should allow unsafe regex or not. + * @default false + */ + allowUnsafeRegex?: boolean; + + /** + * Indicates whether the router paths should be case-sensitive or not. + * @default true + */ + caseSensitive?: boolean; + + /** + * Indicates whether the router should ignore trailing slashes or not. + * @default false + */ + ignoreTrailingSlash?: boolean; + + /** + * Maximum allowed length for router parameter values. + * @default 100 + */ + maxParamLength?: number; +} + export class RouterConfig extends Node { public static SCHEMA: Schema = { @@ -39,21 +66,25 @@ export class RouterConfig extends Node { }; /** + * Indicates whether the router should allow unsafe regex or not. * @default false */ public allowUnsafeRegex: boolean; /** + * Indicates whether the router paths should be case-sensitive or not. * @default true */ public caseSensitive: boolean; /** + * Indicates whether the router should ignore trailing slashes or not. * @default false */ public ignoreTrailingSlash: boolean; /** + * Maximum allowed length for router parameter values. * @default 100 */ public maxParamLength: number; diff --git a/packages/config/src/config/server.config.ts b/packages/config/src/config/server.config.ts index 6369a26..1a3c1cf 100644 --- a/packages/config/src/config/server.config.ts +++ b/packages/config/src/config/server.config.ts @@ -38,6 +38,42 @@ const createETagGenerator = (weak: boolean) => function generateETag( }); }; +export interface ServerConfigI { + + /** + * HTTP2/HTTPS cert chain in PEM format. + */ + cert?: string; + + /** + * ETag response header value generator. + */ + etag?: boolean | "strong" | "weak" | ((body: Buffer | string, encoding?: BufferEncoding) => string); + + /** + * Server hostname. + * @default "localhost" + */ + hostname?: string; + + /** + * HTTP2/HTTPS private key in PEM format. + */ + key?: string; + + /** + * Server port. + * @default 3000 + */ + port?: number; + + /** + * Server protocol. + * @default "http" + */ + protocol?: SERVER_PROTOCOL; +} + export interface ServerConfig { /** diff --git a/packages/config/src/config/subdomain.config.ts b/packages/config/src/config/subdomain.config.ts index c6aa4f5..28a173b 100644 --- a/packages/config/src/config/subdomain.config.ts +++ b/packages/config/src/config/subdomain.config.ts @@ -27,6 +27,15 @@ import Joi from "joi"; import content from "#src/config-content"; import { Node, Schema } from "#src/utils/index"; +export interface SubdomainConfigI { + + /** + * The number of dot-separated parts of the host to remove to access subdomain. + * @default 2 + */ + offset?: number; +} + export class SubdomainConfig extends Node { public static SCHEMA: Schema = { diff --git a/packages/config/tests/index.test.ts b/packages/config/tests/index.test.ts index 23e2119..e042207 100644 --- a/packages/config/tests/index.test.ts +++ b/packages/config/tests/index.test.ts @@ -1,9 +1,9 @@ import { parse } from "qs"; -import { config, events } from "@foxify/config"; +import { config, ConfigI, ENV, events, SERVER_PROTOCOL } from "@foxify/config"; it("should use default config", () => { expect(config).toMatchObject({ - env : "test", + env : "test" as ENV, json: { escape : false, // eslint-disable-next-line no-undefined @@ -23,14 +23,20 @@ it("should use default config", () => { etag : undefined, hostname: "localhost", port : 3000, - protocol: "http", + protocol: "http" as SERVER_PROTOCOL, }, subdomain: { offset: 2, }, + router: { + allowUnsafeRegex : false, + caseSensitive : true, + ignoreTrailingSlash: false, + maxParamLength : 100, + }, workers : 1, xPoweredBy: true, - }); + } satisfies ConfigI); expect(config.proxy.trust).toBeInstanceOf(Function); expect(config.proxy.trust.toString()).toBe("() => false"); From 923e1281a293887ce0c97818da5a6dfe1f5f4bc7 Mon Sep 17 00:00:00 2001 From: Ardalan Amini Date: Sun, 16 Jul 2023 14:49:43 +0330 Subject: [PATCH 6/6] feat(config): Add support for view configuration This commit adds support for configuring views in the Foxify framework. It introduces a new `ViewConfig` class that allows users to specify the directory containing view templates, the view template file extension, and a custom renderer function. The `Foxify` class now has methods to enable/disable view rendering and set the view engine. The changes also include updates to the `Server` class to pass the view configuration settings when creating instances of `Request` and `Response`. --- package.json | 2 +- packages/config/src/config/config.ts | 11 ++ packages/config/src/config/index.ts | 2 + packages/config/src/config/query.config.ts | 2 +- packages/config/src/config/server.config.ts | 2 +- packages/config/src/config/view.config.ts | 94 +++++++++++++ packages/config/src/utils/Node.ts | 2 +- packages/foxify/package.json | 2 + packages/foxify/src/Foxify.ts | 35 ++--- packages/foxify/src/Server.ts | 38 ++---- packages/foxify/src/view/Engine.ts | 32 ----- packages/foxify/src/view/index.ts | 1 - packages/http/package.json | 2 + packages/http/src/Request.ts | 45 ++----- packages/http/src/Response.ts | 76 +++-------- packages/http/src/index.ts | 6 - packages/http/src/view/Engine.ts | 27 ---- packages/http/src/view/index.ts | 1 - pnpm-lock.yaml | 140 ++++++++++---------- 19 files changed, 238 insertions(+), 282 deletions(-) create mode 100644 packages/config/src/config/view.config.ts delete mode 100644 packages/foxify/src/view/Engine.ts delete mode 100644 packages/foxify/src/view/index.ts delete mode 100644 packages/http/src/view/Engine.ts delete mode 100644 packages/http/src/view/index.ts diff --git a/package.json b/package.json index 4dbc33b..e6ebcb6 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "typedoc": "^0.24.8", - "typescript": "^5.1.3" + "typescript": "^5.1.6" }, "workspaces": [ "benchmarks/*", diff --git a/packages/config/src/config/config.ts b/packages/config/src/config/config.ts index f1c6eaf..cdd44bc 100644 --- a/packages/config/src/config/config.ts +++ b/packages/config/src/config/config.ts @@ -35,6 +35,7 @@ import { QueryConfig, type QueryConfigI } from "./query.config.js"; import { RouterConfig, type RouterConfigI } from "./router.config.js"; import { ServerConfig, type ServerConfigI } from "./server.config.js"; import { SubdomainConfig, type SubdomainConfigI } from "./subdomain.config.js"; +import { ViewConfig, type ViewConfigI } from "./view.config.js"; export interface ConfigI { @@ -79,6 +80,11 @@ export interface ConfigI { */ readonly subdomain?: SubdomainConfigI; + /** + * View config. + */ + readonly view?: ViewConfigI; + /** * Number of Node.js cluster workers to be created. * In case of `1` Node.js cluster workers won't be used. @@ -146,6 +152,11 @@ export class Config extends Node { */ public subdomain = new SubdomainConfig; + /** + * View config. + */ + public view = new ViewConfig; + /** * Number of Node.js cluster workers to be created. * In case of `1` Node.js cluster workers won't be used. diff --git a/packages/config/src/config/index.ts b/packages/config/src/config/index.ts index 6f54b98..4a42dd9 100644 --- a/packages/config/src/config/index.ts +++ b/packages/config/src/config/index.ts @@ -25,6 +25,8 @@ import { Config, ConfigI } from "./config.js"; +export { type ViewRendererCallbackT, type ViewRendererT } from "./view.config.js"; + export type { Config, ConfigI }; export const config = new Config; diff --git a/packages/config/src/config/query.config.ts b/packages/config/src/config/query.config.ts index 2d8dfe1..91e95d1 100644 --- a/packages/config/src/config/query.config.ts +++ b/packages/config/src/config/query.config.ts @@ -72,7 +72,7 @@ export class QueryConfig extends Node { case "extended": return (str: string): ParsedQs => parse(str, { allowPrototypes: true }); default: - throw new Error(`Unexpected value: ${ value }`); + throw new TypeError(`Unexpected value: ${ value }`); } }), ) diff --git a/packages/config/src/config/server.config.ts b/packages/config/src/config/server.config.ts index 1a3c1cf..e737684 100644 --- a/packages/config/src/config/server.config.ts +++ b/packages/config/src/config/server.config.ts @@ -108,7 +108,7 @@ export class ServerConfig extends Node { case "strong": return createETagGenerator(false); default: - throw new Error(`Unexpected value: ${ value }`); + throw new TypeError(`Unexpected value: ${ value }`); } }), ), diff --git a/packages/config/src/config/view.config.ts b/packages/config/src/config/view.config.ts new file mode 100644 index 0000000..e10a62a --- /dev/null +++ b/packages/config/src/config/view.config.ts @@ -0,0 +1,94 @@ +/* + * MIT License + * + * Copyright (c) 2023 FoxifyJS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +import { statSync } from "node:fs"; +import Joi from "joi"; +import content from "#src/config-content"; +import { Node, Schema } from "#src/utils/index"; + +export type ViewRendererCallbackT = (error: Error, result: string) => void; + +export type ViewRendererT = ( + filepath: string, + options: Record, + callback?: ViewRendererCallbackT, +) => void; + +export interface ViewConfigI { + + /** + * The directory containing view templates. + */ + directory?: string; + + /** + * The view template file extension. + */ + extension?: string; + + /** + * The function to render the view templates. + */ + renderer?: ViewRendererT; +} + +export class ViewConfig extends Node { + + public static SCHEMA: Schema = { + directory: Joi.string().custom((value) => { + if (statSync(value).isDirectory()) return value; + + throw new TypeError(`Unexpected value: ${ value }`); + }), + extension: Joi.string(), + renderer : Joi.function(), + }; + + /** + * The directory containing view templates. + */ + public directory: string; + + /** + * The view template file extension. + */ + public extension?: string; + + /** + * The function to render the view templates. + */ + public renderer: ViewRendererT; + + public constructor(config: Partial = content?.view ?? {}) { + super("view"); + + const { directory, extension, renderer } = config as Required; + + this.directory = directory; + this.extension = extension; + this.renderer = renderer; + } + +} diff --git a/packages/config/src/utils/Node.ts b/packages/config/src/utils/Node.ts index 0e7006b..80f0a9a 100644 --- a/packages/config/src/utils/Node.ts +++ b/packages/config/src/utils/Node.ts @@ -27,7 +27,7 @@ import Joi from "joi"; import { events } from "#src/events"; export type DirectConfigKey = { - [K in keyof T]: T[K] extends object ? never : K; + [K in keyof T]: T[K] extends object ? T[K] extends (...args: any[]) => any ? K : never : K; }[keyof T]; export type Schema = Record, Joi.Schema>; diff --git a/packages/foxify/package.json b/packages/foxify/package.json index f93c190..ccfeaf2 100644 --- a/packages/foxify/package.json +++ b/packages/foxify/package.json @@ -68,6 +68,7 @@ "./package.json": "./package.json" }, "peerDependencies": { + "@foxify/config": "^1", "@foxify/http": "^1", "@foxify/router": "^1", "@types/node": ">=16", @@ -85,6 +86,7 @@ "serve-static": "^1.15.0" }, "devDependencies": { + "@foxify/config": "workspace:^", "@foxify/http": "workspace:^", "@foxify/router": "workspace:^", "@types/cookie": "^0.5.1", diff --git a/packages/foxify/src/Foxify.ts b/packages/foxify/src/Foxify.ts index a019815..83136bf 100644 --- a/packages/foxify/src/Foxify.ts +++ b/packages/foxify/src/Foxify.ts @@ -1,10 +1,5 @@ import assert from "node:assert"; -import { - Request, - requestSettings, - Response, - responseSettings, -} from "@foxify/http"; +import { Request, Response } from "@foxify/http"; import inject, { InjectResultI, OptionsI as InjectOptionsI } from "@foxify/inject"; import Router from "@foxify/router"; import * as proxyAddr from "proxy-addr"; @@ -12,7 +7,6 @@ import * as qs from "qs"; import serveStatic from "serve-static"; import Server from "./Server.js"; import * as utils from "./utils/index.js"; -import { Engine } from "./view/index.js"; const SETTINGS: Array = [ "env", @@ -120,8 +114,6 @@ class Foxify extends Router { etag : undefined as any, }; - private _view?: Engine; - public constructor() { super(); @@ -129,6 +121,7 @@ class Foxify extends Router { this.disable("trust.proxy"); } + // TODO: remove in favor of @foxify/config public static dotenv(path: string): void { utils.assertType("path", "string", path); @@ -136,22 +129,27 @@ class Foxify extends Router { require("dotenv").config({ path }); } + // TODO: remove in favor of @foxify/config public disable(setting: keyof Foxify.Settings): this { return this.set(setting, false); } + // TODO: remove in favor of @foxify/config public disabled(setting: keyof Foxify.Settings): boolean { return !this.setting(setting); } + // TODO: remove in favor of @foxify/config public enable(setting: keyof Foxify.Settings): this { return this.set(setting, true); } + // TODO: remove in favor of @foxify/config public enabled(setting: keyof Foxify.Settings): boolean { return !this.disabled(setting); } + // TODO: remove in favor of @foxify/config /** * Handle view * @param extension view template file extension @@ -159,8 +157,6 @@ class Foxify extends Router { * @param handler */ public engine(extension: string, path: string, handler: () => void): this { - this._view = new Engine(path, extension, handler); - return this; } @@ -172,12 +168,6 @@ class Foxify extends Router { if (typeof options === "string") options = { url: options }; - requestSettings(this._settings as any); - responseSettings({ - ...this._settings, - view: this._view, - } as any); - // TODO: fix typescript issues return inject(this.lookup.bind(this), { ...options, @@ -190,6 +180,7 @@ class Foxify extends Router { }); } + // TODO: remove in favor of @foxify/config public set( setting: T, value: Foxify.UserSettings[T], @@ -285,6 +276,7 @@ class Foxify extends Router { return this; } + // TODO: remove in favor of @foxify/config public setting(setting: T): Foxify.Settings[T] { assert( SETTINGS.includes(setting), @@ -297,17 +289,10 @@ class Foxify extends Router { public start(callback?: Server.Callback): Server { if (callback != null) utils.assertType("callback", "function", callback); - /* Set node env */ process.env.NODE_ENV = this.setting("env"); - const server = new Server( - { - ...this._settings, - view: this._view, - }, - this.lookup.bind(this), - ); + const server = new Server(this.lookup.bind(this)); return server.start(callback); } diff --git a/packages/foxify/src/Server.ts b/packages/foxify/src/Server.ts index 87932fd..9fd29a5 100644 --- a/packages/foxify/src/Server.ts +++ b/packages/foxify/src/Server.ts @@ -1,21 +1,11 @@ import cluster from "node:cluster"; import http from "node:http"; import https from "node:https"; -import { - Request, - requestSettings, - Response, - responseSettings, -} from "@foxify/http"; -import { Engine } from "./view/index.js"; -// eslint-disable-next-line @typescript-eslint/consistent-type-imports -import type Foxify from "./index.js"; +import { config, SERVER_PROTOCOL } from "@foxify/config"; +import { Request, Response } from "@foxify/http"; // eslint-disable-next-line @typescript-eslint/no-namespace namespace Server { - export interface Settings extends Foxify.Settings { - view?: Engine; - } export type Listener = (request: Request, response: Response) => void; @@ -24,35 +14,25 @@ namespace Server { class Server { - protected _host: string; - protected _listening = false; - protected _port: number; - private readonly _instance?: http.Server | https.Server; - public constructor(settings: Server.Settings, listener: Server.Listener) { - this._host = settings.url; - this._port = settings.port; - - const isHttps = settings.https; + public constructor(listener: Server.Listener) { + const isHttps = config.server.protocol === SERVER_PROTOCOL.HTTPS; const SERVER: any = isHttps ? https : http; - requestSettings(settings as any); - responseSettings(settings as any); - const OPTIONS: any = { IncomingMessage: Request, ServerResponse : Response, }; if (isHttps) { - OPTIONS.cert = settings["https.cert"]; - OPTIONS.key = settings["https.key"]; + OPTIONS.cert = config.server.cert; + OPTIONS.key = config.server.key; } - const workers = settings.workers; + const workers = config.workers; if (workers > 1) { if (cluster.isPrimary) { @@ -86,8 +66,8 @@ class Server { if (instance) { instance.listen( - this._port, - this._host, + config.server.port, + config.server.hostname, callback && ((): unknown => callback(this)), ); } diff --git a/packages/foxify/src/view/Engine.ts b/packages/foxify/src/view/Engine.ts deleted file mode 100644 index 145f8ca..0000000 --- a/packages/foxify/src/view/Engine.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { join } from "node:path"; - -// eslint-disable-next-line @typescript-eslint/no-namespace -namespace Engine { - export type Callback = (err: Error, str: string) => any; -} - -class Engine { - - protected _ext: string; - - protected _handler: (...args: any[]) => void; - - protected _path: string; - - public constructor(path: string, ext: string, handler: () => void) { - this._path = path; - this._ext = ext; - this._handler = handler; - } - - public render( - filename: string, - opts: Record = {}, - cb?: Engine.Callback, - ): void { - this._handler(join(this._path, `${ filename }.${ this._ext }`), opts, cb); - } - -} - -export default Engine; diff --git a/packages/foxify/src/view/index.ts b/packages/foxify/src/view/index.ts deleted file mode 100644 index d6c6206..0000000 --- a/packages/foxify/src/view/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Engine } from "./Engine.js"; diff --git a/packages/http/package.json b/packages/http/package.json index 98bc76d..f7ea60b 100644 --- a/packages/http/package.json +++ b/packages/http/package.json @@ -56,6 +56,7 @@ "./package.json": "./package.json" }, "peerDependencies": { + "@foxify/config": "workspace:^", "@types/node": ">=16" }, "dependencies": { @@ -75,6 +76,7 @@ "type-is": "^1.6.18" }, "devDependencies": { + "@foxify/config": "workspace:^", "@foxify/inject": "workspace:^", "@types/content-disposition": "^0.5.5", "@types/content-type": "^1.1.5", diff --git a/packages/http/src/Request.ts b/packages/http/src/Request.ts index 9ea0414..8dfed72 100644 --- a/packages/http/src/Request.ts +++ b/packages/http/src/Request.ts @@ -1,26 +1,11 @@ import { IncomingHttpHeaders, IncomingMessage } from "node:http"; import { isIP } from "node:net"; import { UrlWithStringQuery } from "node:url"; -import proxyAddr, { compile as proxyAddrCompile, all as proxyAddrAll } from "proxy-addr"; -import * as qs from "qs"; +import { config } from "@foxify/config"; +import proxyAddr, { all as proxyAddrAll } from "proxy-addr"; import typeIs from "type-is"; import { MethodT, ProtocolT } from "./constants/index.js"; -import { - Accepts, - parseUrl, - RANGE_PARSER_RESULT, - rangeParser, - RangeParserRangesI, -} from "./utils/index.js"; - -// eslint-disable-next-line import/exports-last -export const DEFAULT_SETTINGS: SettingsI = { - "query.parser" : qs.parse, - "trust.proxy" : proxyAddrCompile([]), - "subdomain.offset": 2, -}; - -const SETTINGS: SettingsI = { ...DEFAULT_SETTINGS }; +import { Accepts, parseUrl, RANGE_PARSER_RESULT, rangeParser, RangeParserRangesI } from "./utils/index.js"; export default class Request extends IncomingMessage { @@ -52,7 +37,7 @@ export default class Request extends IncomingMessage { public get hostname(): string | undefined { let host = this.get("x-forwarded-host"); - if (!host || !SETTINGS["trust.proxy"](this.socket.remoteAddress!, 0)) { + if (!host || !config.proxy.trust(this.socket.remoteAddress!, 0)) { host = this.get("host")!; } else if (host.includes(",")) { // Note: X-Forwarded-Host is normally only ever a @@ -77,7 +62,7 @@ export default class Request extends IncomingMessage { * "trust.proxy" is set. */ public get ip(): string { - return proxyAddr(this, SETTINGS["trust.proxy"]); + return proxyAddr(this, config.proxy.trust); } /** @@ -89,7 +74,7 @@ export default class Request extends IncomingMessage { * "proxy2" were trusted. */ public get ips(): string[] { - const addresses = proxyAddrAll(this, SETTINGS["trust.proxy"]); + const addresses = proxyAddrAll(this, config.proxy.trust); // Reverse the order (to farthest -> closest) // and remove socket address @@ -118,8 +103,7 @@ export default class Request extends IncomingMessage { public get protocol(): ProtocolT { const proto = (this.socket as any).encrypted ? "https" : "http"; - if (!SETTINGS["trust.proxy"](this.socket.remoteAddress!, 0)) return proto; - + if (!config.proxy.trust(this.socket.remoteAddress!, 0)) return proto; // Note: X-Forwarded-Proto is normally only ever a // single value, but this is to be safe. @@ -132,7 +116,7 @@ export default class Request extends IncomingMessage { public get query(): Record { return ( - this._queryCache ??= SETTINGS["query.parser"]((this._parsedUrl ??= parseUrl(this.url)).query!) + this._queryCache ??= config.query.parser((this._parsedUrl ??= parseUrl(this.url)).query!) ); } @@ -161,7 +145,7 @@ export default class Request extends IncomingMessage { if (!hostname) return []; - return (isIP(hostname) ? [hostname] : hostname.split(".").reverse()).slice(SETTINGS["subdomain.offset"]); + return (isIP(hostname) ? [hostname] : hostname.split(".").reverse()).slice(config.subdomain.offset); } /** @@ -318,20 +302,9 @@ export default class Request extends IncomingMessage { } -// eslint-disable-next-line @typescript-eslint/no-shadow -export function settings(settings: Partial = DEFAULT_SETTINGS): void { - Object.assign(SETTINGS, settings); -} - export interface HeadersI extends IncomingHttpHeaders { referrer: IncomingHttpHeaders["referer"]; "x-forwarded-host"?: string; "x-forwarded-proto"?: string; "x-requested-with"?: string; } - -export interface SettingsI { - "subdomain.offset": number; - "query.parser"(str: string): Record; - "trust.proxy"(ip: string, hopIndex: number): boolean; -} diff --git a/packages/http/src/Response.ts b/packages/http/src/Response.ts index 58eaaa6..65e4a13 100644 --- a/packages/http/src/Response.ts +++ b/packages/http/src/Response.ts @@ -1,6 +1,7 @@ import assert from "node:assert"; import { OutgoingHttpHeaders, ServerResponse, STATUS_CODES } from "node:http"; import { extname, resolve } from "node:path"; +import { config, type ViewRendererCallbackT } from "@foxify/config"; import fresh from "@foxify/fresh"; import contentDisposition from "content-disposition"; import * as contentType from "content-type"; @@ -11,8 +12,7 @@ import onFinished from "on-finished"; import send, { mime as sendMime } from "send"; import Request from "./Request.js"; import { ENCODING_UTF8, JsonT, METHOD, STATUS, StatusT, StringifyT } from "./constants/index.js"; -import { createETagGenerator, encodeUrl, vary } from "./utils/index.js"; -import { CallbackT as EngineCallbackT, Engine } from "./view/index.js"; +import { encodeUrl, vary } from "./utils/index.js"; /** * Set the charset in a given Content-Type string. @@ -278,15 +278,6 @@ const normalizeTypes = (types: string[]): any[] => { return ret; }; -// eslint-disable-next-line import/exports-last -export const DEFAULT_SETTINGS: SettingsI = { - etag : createETagGenerator(true), - "json.escape" : false, - "jsonp.callback": "callback", -}; - -const SETTINGS: SettingsI = { ...DEFAULT_SETTINGS }; - const hasOwnProperty = Object.prototype.hasOwnProperty; const charsetRegExp = /;\s*charset\s*=/; @@ -744,9 +735,9 @@ class Response extends ServerResponse { stringify( this.stringify[this.statusCode], body, - SETTINGS["json.replacer"], - SETTINGS["json.spaces"], - SETTINGS["json.escape"], + config.json.replacer, + config.json.spaces, + config.json.escape, ), ENCODING_UTF8, ); @@ -762,11 +753,11 @@ class Response extends ServerResponse { let str = stringify( this.stringify[this.statusCode], body, - SETTINGS["json.replacer"], - SETTINGS["json.spaces"], - SETTINGS["json.escape"], + config.json.replacer, + config.json.spaces, + config.json.escape, ); - let callback = this.req.query[SETTINGS["jsonp.callback"]]; + let callback = this.req.query[config.jsonp.callback]; // Content-type if (!this.get("Content-Type")) { @@ -886,31 +877,30 @@ class Response extends ServerResponse { public render( view: string, - data?: EngineCallbackT | Record, - callback?: EngineCallbackT, + data: Record | ViewRendererCallbackT = {}, + callback?: ViewRendererCallbackT, ): void { - const { view: engine } = SETTINGS; - - assert(engine, "View engine is not specified"); + assert(config.view.renderer, "View renderer is not specified"); if (typeof data === "function") { callback = data; - // eslint-disable-next-line no-undefined - data = undefined; + data = {}; } - callback ??= (err, str): void => { + callback ??= (error: Error, result: string): void => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (err != null) { - this.next(err); + if (error != null) { + this.next(error); return; } - this.send(str); + this.send(result); }; - engine.render(view, data, callback); + if (config.view.extension) view += `.${ config.view.extension }`; + + config.view.renderer(resolve(config.view.directory, view), data, callback); } /** @@ -1082,10 +1072,8 @@ class Response extends ServerResponse { private $end(body?: Buffer | string, encoding?: BufferEncoding): this { // eslint-disable-next-line no-undefined if (body !== undefined) { - const { etag } = SETTINGS; - - if (etag && !this.hasHeader("ETag")) { - const generatedETag = etag(body, encoding); + if (config.server.etag && !this.hasHeader("ETag")) { + const generatedETag = config.server.etag(body, encoding); if (generatedETag) this.setHeader("ETag", generatedETag); } @@ -1123,26 +1111,6 @@ Response.prototype.get = Response.prototype.getHeader; export default Response; -// eslint-disable-next-line @typescript-eslint/no-shadow -export function settings(settings: Partial = DEFAULT_SETTINGS): void { - Object.assign(SETTINGS, settings); -} - export interface HeadersI extends OutgoingHttpHeaders { "Content-Type"?: string; } - -export interface SettingsI { - "json.escape": boolean; - "json.spaces"?: number; - "jsonp.callback": string; - view?: Engine; - - etag?( - body: Buffer | string, - encoding?: BufferEncoding, - ): string | undefined; - - "json.replacer"?(key: string, value: any): any; - -} diff --git a/packages/http/src/index.ts b/packages/http/src/index.ts index f0343e9..a9b2ab1 100644 --- a/packages/http/src/index.ts +++ b/packages/http/src/index.ts @@ -2,15 +2,9 @@ export * from "./constants/index.js"; export * from "./errors/index.js"; export { default as Request, - settings as requestSettings, - DEFAULT_SETTINGS as REQUEST_DEFAULT_SETTINGS, type HeadersI as RequestHeadersI, - type SettingsI as RequestSettingsI, } from "./Request.js"; export { default as Response, - settings as responseSettings, - DEFAULT_SETTINGS as RESPONSE_DEFAULT_SETTINGS, type HeadersI as ResponseHeadersI, - type SettingsI as ResponseSettingsI, } from "./Response.js"; diff --git a/packages/http/src/view/Engine.ts b/packages/http/src/view/Engine.ts deleted file mode 100644 index d4a0037..0000000 --- a/packages/http/src/view/Engine.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { join } from "node:path"; - -export default class Engine { - - protected _ext: string; - - protected _handler: (...args: any[]) => void; - - protected _path: string; - - public constructor(path: string, ext: string, handler: () => void) { - this._path = path; - this._ext = ext; - this._handler = handler; - } - - public render( - filename: string, - opts: Record = {}, - cb?: CallbackT, - ): void { - this._handler(join(this._path, `${ filename }.${ this._ext }`), opts, cb); - } - -} - -export type CallbackT = (err: Error, str: string) => void; diff --git a/packages/http/src/view/index.ts b/packages/http/src/view/index.ts deleted file mode 100644 index 3eace5a..0000000 --- a/packages/http/src/view/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Engine, type CallbackT } from "./Engine.js"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cccf68f..488777e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,10 +22,10 @@ importers: version: 17.6.5 '@nrwl/jest': specifier: ^16.3.2 - version: 16.3.2(@types/node@20.3.1)(nx@16.3.2)(ts-node@10.9.1)(typescript@5.1.3) + version: 16.3.2(@types/node@20.3.1)(nx@16.3.2)(ts-node@10.9.1)(typescript@5.1.6) '@nrwl/linter': specifier: ^16.3.2 - version: 16.3.2(eslint@8.43.0)(nx@16.3.2)(typescript@5.1.3) + version: 16.3.2(eslint@8.43.0)(nx@16.3.2)(typescript@5.1.6) '@nrwl/workspace': specifier: ^16.3.2 version: 16.3.2 @@ -37,10 +37,10 @@ importers: version: 20.3.1 '@typescript-eslint/eslint-plugin': specifier: ^5.59.11 - version: 5.59.11(@typescript-eslint/parser@5.59.11)(eslint@8.43.0)(typescript@5.1.3) + version: 5.59.11(@typescript-eslint/parser@5.59.11)(eslint@8.43.0)(typescript@5.1.6) '@typescript-eslint/parser': specifier: ^5.59.11 - version: 5.59.11(eslint@8.43.0)(typescript@5.1.3) + version: 5.59.11(eslint@8.43.0)(typescript@5.1.6) eslint: specifier: ^8.43.0 version: 8.43.0 @@ -73,16 +73,16 @@ importers: version: 16.0.5 ts-jest: specifier: ^29.1.0 - version: 29.1.0(@babel/core@7.19.6)(jest@29.5.0)(typescript@5.1.3) + version: 29.1.0(@babel/core@7.19.6)(jest@29.5.0)(typescript@5.1.6) ts-node: specifier: ^10.9.1 - version: 10.9.1(@types/node@20.3.1)(typescript@5.1.3) + version: 10.9.1(@types/node@20.3.1)(typescript@5.1.6) typedoc: specifier: ^0.24.8 - version: 0.24.8(typescript@5.1.3) + version: 0.24.8(typescript@5.1.6) typescript: - specifier: ^5.1.3 - version: 5.1.3 + specifier: ^5.1.6 + version: 5.1.6 benchmarks/benchmark: dependencies: @@ -378,6 +378,9 @@ importers: specifier: ^1.15.0 version: 1.15.0 devDependencies: + '@foxify/config': + specifier: workspace:^ + version: link:../config '@foxify/http': specifier: workspace:^ version: link:../http @@ -472,6 +475,9 @@ importers: specifier: ^1.6.18 version: 1.6.18 devDependencies: + '@foxify/config': + specifier: workspace:^ + version: link:../config '@foxify/inject': specifier: workspace:^ version: link:../inject @@ -2387,13 +2393,13 @@ packages: '@types/node': 20.3.1 chalk: 4.1.2 cosmiconfig: 8.2.0 - cosmiconfig-typescript-loader: 4.3.0(@types/node@20.3.1)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.1.3) + cosmiconfig-typescript-loader: 4.3.0(@types/node@20.3.1)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.1.6) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 resolve-from: 5.0.0 - ts-node: 10.9.1(@types/node@20.3.1)(typescript@5.1.3) - typescript: 5.1.3 + ts-node: 10.9.1(@types/node@20.3.1)(typescript@5.1.6) + typescript: 5.1.6 transitivePeerDependencies: - '@swc/core' - '@swc/wasm' @@ -3131,10 +3137,10 @@ packages: - nx dev: true - /@nrwl/jest@16.3.2(@types/node@20.3.1)(nx@16.3.2)(ts-node@10.9.1)(typescript@5.1.3): + /@nrwl/jest@16.3.2(@types/node@20.3.1)(nx@16.3.2)(ts-node@10.9.1)(typescript@5.1.6): resolution: {integrity: sha512-vhwrgjIn1XG3zDSlc6CSfCKBtgDEYQUWG69MdfaqrNInmmsiPkspv7eM99Xh8MGN5HMC2Epzy2todD3J2zZZuQ==} dependencies: - '@nx/jest': 16.3.2(@types/node@20.3.1)(nx@16.3.2)(ts-node@10.9.1)(typescript@5.1.3) + '@nx/jest': 16.3.2(@types/node@20.3.1)(nx@16.3.2)(ts-node@10.9.1)(typescript@5.1.6) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -3149,10 +3155,10 @@ packages: - verdaccio dev: true - /@nrwl/js@16.3.2(nx@16.3.2)(typescript@5.1.3): + /@nrwl/js@16.3.2(nx@16.3.2)(typescript@5.1.6): resolution: {integrity: sha512-UMmdA4vXy2/VWNMlpBDruT9XwGmLw/MpUaKoN2KLkai/fYN6MvB3mabc9WQ8qsNvDWshmOJ6TqAHReR25BjugQ==} dependencies: - '@nx/js': 16.3.2(nx@16.3.2)(typescript@5.1.3) + '@nx/js': 16.3.2(nx@16.3.2)(typescript@5.1.6) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -3164,10 +3170,10 @@ packages: - verdaccio dev: true - /@nrwl/linter@16.3.2(eslint@8.43.0)(nx@16.3.2)(typescript@5.1.3): + /@nrwl/linter@16.3.2(eslint@8.43.0)(nx@16.3.2)(typescript@5.1.6): resolution: {integrity: sha512-sUDQNlmRIGQnhdDmpQkJgpF9LZWKBoqXr2g9Y4yq0QlpTamxTbx8/GxMICotA52kayEx1cKbU1xvjJWPchSrlw==} dependencies: - '@nx/linter': 16.3.2(eslint@8.43.0)(nx@16.3.2)(typescript@5.1.3) + '@nx/linter': 16.3.2(eslint@8.43.0)(nx@16.3.2)(typescript@5.1.6) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -3223,15 +3229,15 @@ packages: tslib: 2.4.1 dev: true - /@nx/jest@16.3.2(@types/node@20.3.1)(nx@16.3.2)(ts-node@10.9.1)(typescript@5.1.3): + /@nx/jest@16.3.2(@types/node@20.3.1)(nx@16.3.2)(ts-node@10.9.1)(typescript@5.1.6): resolution: {integrity: sha512-aO8Rc+wwSXLh1jJYd2cxOT5R9BQfqjAXWZOPcvAQQonFNNfwMHrw0+YsqjWgiFtFrxzSX5RrhzVG44cOWpAdqQ==} dependencies: '@jest/reporters': 29.5.0 '@jest/test-result': 29.5.0 - '@nrwl/jest': 16.3.2(@types/node@20.3.1)(nx@16.3.2)(ts-node@10.9.1)(typescript@5.1.3) + '@nrwl/jest': 16.3.2(@types/node@20.3.1)(nx@16.3.2)(ts-node@10.9.1)(typescript@5.1.6) '@nx/devkit': 16.3.2(nx@16.3.2) - '@nx/js': 16.3.2(nx@16.3.2)(typescript@5.1.3) - '@phenomnomnominal/tsquery': 5.0.1(typescript@5.1.3) + '@nx/js': 16.3.2(nx@16.3.2)(typescript@5.1.6) + '@phenomnomnominal/tsquery': 5.0.1(typescript@5.1.6) chalk: 4.1.2 dotenv: 10.0.0 identity-obj-proxy: 3.0.0 @@ -3254,7 +3260,7 @@ packages: - verdaccio dev: true - /@nx/js@16.3.2(nx@16.3.2)(typescript@5.1.3): + /@nx/js@16.3.2(nx@16.3.2)(typescript@5.1.6): resolution: {integrity: sha512-bumLGMduNm221Sh3/wkEMEkJOC1kTlqmpx6wamDSsPlAFq0ePgoaNJjoYqC9XH7n7wXtgy9bgKhHJPnek8NKow==} peerDependencies: verdaccio: ^5.0.4 @@ -3269,10 +3275,10 @@ packages: '@babel/preset-env': 7.22.5(@babel/core@7.19.6) '@babel/preset-typescript': 7.22.5(@babel/core@7.19.6) '@babel/runtime': 7.20.1 - '@nrwl/js': 16.3.2(nx@16.3.2)(typescript@5.1.3) + '@nrwl/js': 16.3.2(nx@16.3.2)(typescript@5.1.6) '@nx/devkit': 16.3.2(nx@16.3.2) '@nx/workspace': 16.3.2 - '@phenomnomnominal/tsquery': 5.0.1(typescript@5.1.3) + '@phenomnomnominal/tsquery': 5.0.1(typescript@5.1.6) babel-plugin-const-enum: 1.2.0(@babel/core@7.19.6) babel-plugin-macros: 2.8.0 babel-plugin-transform-typescript-metadata: 0.3.2(@babel/core@7.19.6) @@ -3295,7 +3301,7 @@ packages: - typescript dev: true - /@nx/linter@16.3.2(eslint@8.43.0)(nx@16.3.2)(typescript@5.1.3): + /@nx/linter@16.3.2(eslint@8.43.0)(nx@16.3.2)(typescript@5.1.6): resolution: {integrity: sha512-hVCU6ZIMd+yTMLrC3PbjaHuD3yU+sB/lABTaWuUx2klT0cqKhiTp0KnDLcFWtzQmnNtGEaUjfPKxvA92xon0CA==} peerDependencies: eslint: ^8.0.0 @@ -3303,10 +3309,10 @@ packages: eslint: optional: true dependencies: - '@nrwl/linter': 16.3.2(eslint@8.43.0)(nx@16.3.2)(typescript@5.1.3) + '@nrwl/linter': 16.3.2(eslint@8.43.0)(nx@16.3.2)(typescript@5.1.6) '@nx/devkit': 16.3.2(nx@16.3.2) - '@nx/js': 16.3.2(nx@16.3.2)(typescript@5.1.3) - '@phenomnomnominal/tsquery': 5.0.1(typescript@5.1.3) + '@nx/js': 16.3.2(nx@16.3.2)(typescript@5.1.6) + '@phenomnomnominal/tsquery': 5.0.1(typescript@5.1.6) eslint: 8.43.0 tmp: 0.2.1 tslib: 2.4.1 @@ -3449,13 +3455,13 @@ packages: node-gyp-build: 4.5.0 dev: true - /@phenomnomnominal/tsquery@5.0.1(typescript@5.1.3): + /@phenomnomnominal/tsquery@5.0.1(typescript@5.1.6): resolution: {integrity: sha512-3nVv+e2FQwsW8Aw6qTU6f+1rfcJ3hrcnvH/mu9i8YhxO+9sqbOfpL8m6PbET5+xKOlz/VSbp0RoYWYCtIsnmuA==} peerDependencies: typescript: ^3 || ^4 || ^5 dependencies: esquery: 1.4.0 - typescript: 5.1.3 + typescript: 5.1.6 dev: true /@pkgr/utils@2.3.1: @@ -3940,7 +3946,7 @@ packages: '@types/yargs-parser': 21.0.0 dev: true - /@typescript-eslint/eslint-plugin@5.59.11(@typescript-eslint/parser@5.59.11)(eslint@8.43.0)(typescript@5.1.3): + /@typescript-eslint/eslint-plugin@5.59.11(@typescript-eslint/parser@5.59.11)(eslint@8.43.0)(typescript@5.1.6): resolution: {integrity: sha512-XxuOfTkCUiOSyBWIvHlUraLw/JT/6Io1365RO6ZuI88STKMavJZPNMU0lFcUTeQXEhHiv64CbxYxBNoDVSmghg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -3952,23 +3958,23 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.5.1 - '@typescript-eslint/parser': 5.59.11(eslint@8.43.0)(typescript@5.1.3) + '@typescript-eslint/parser': 5.59.11(eslint@8.43.0)(typescript@5.1.6) '@typescript-eslint/scope-manager': 5.59.11 - '@typescript-eslint/type-utils': 5.59.11(eslint@8.43.0)(typescript@5.1.3) - '@typescript-eslint/utils': 5.59.11(eslint@8.43.0)(typescript@5.1.3) + '@typescript-eslint/type-utils': 5.59.11(eslint@8.43.0)(typescript@5.1.6) + '@typescript-eslint/utils': 5.59.11(eslint@8.43.0)(typescript@5.1.6) debug: 4.3.4 eslint: 8.43.0 grapheme-splitter: 1.0.4 ignore: 5.2.0 natural-compare-lite: 1.4.0 semver: 7.3.8 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 + tsutils: 3.21.0(typescript@5.1.6) + typescript: 5.1.6 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@5.59.11(eslint@8.43.0)(typescript@5.1.3): + /@typescript-eslint/parser@5.59.11(eslint@8.43.0)(typescript@5.1.6): resolution: {integrity: sha512-s9ZF3M+Nym6CAZEkJJeO2TFHHDsKAM3ecNkLuH4i4s8/RCPnF5JRip2GyviYkeEAcwGMJxkqG9h2dAsnA1nZpA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -3980,10 +3986,10 @@ packages: dependencies: '@typescript-eslint/scope-manager': 5.59.11 '@typescript-eslint/types': 5.59.11 - '@typescript-eslint/typescript-estree': 5.59.11(typescript@5.1.3) + '@typescript-eslint/typescript-estree': 5.59.11(typescript@5.1.6) debug: 4.3.4 eslint: 8.43.0 - typescript: 5.1.3 + typescript: 5.1.6 transitivePeerDependencies: - supports-color dev: true @@ -3996,7 +4002,7 @@ packages: '@typescript-eslint/visitor-keys': 5.59.11 dev: true - /@typescript-eslint/type-utils@5.59.11(eslint@8.43.0)(typescript@5.1.3): + /@typescript-eslint/type-utils@5.59.11(eslint@8.43.0)(typescript@5.1.6): resolution: {integrity: sha512-LZqVY8hMiVRF2a7/swmkStMYSoXMFlzL6sXV6U/2gL5cwnLWQgLEG8tjWPpaE4rMIdZ6VKWwcffPlo1jPfk43g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -4006,12 +4012,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 5.59.11(typescript@5.1.3) - '@typescript-eslint/utils': 5.59.11(eslint@8.43.0)(typescript@5.1.3) + '@typescript-eslint/typescript-estree': 5.59.11(typescript@5.1.6) + '@typescript-eslint/utils': 5.59.11(eslint@8.43.0)(typescript@5.1.6) debug: 4.3.4 eslint: 8.43.0 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 + tsutils: 3.21.0(typescript@5.1.6) + typescript: 5.1.6 transitivePeerDependencies: - supports-color dev: true @@ -4021,7 +4027,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/typescript-estree@5.59.11(typescript@5.1.3): + /@typescript-eslint/typescript-estree@5.59.11(typescript@5.1.6): resolution: {integrity: sha512-YupOpot5hJO0maupJXixi6l5ETdrITxeo5eBOeuV7RSKgYdU3G5cxO49/9WRnJq9EMrB7AuTSLH/bqOsXi7wPA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -4036,13 +4042,13 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.3.8 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 + tsutils: 3.21.0(typescript@5.1.6) + typescript: 5.1.6 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils@5.59.11(eslint@8.43.0)(typescript@5.1.3): + /@typescript-eslint/utils@5.59.11(eslint@8.43.0)(typescript@5.1.6): resolution: {integrity: sha512-didu2rHSOMUdJThLk4aZ1Or8IcO3HzCw/ZvEjTTIfjIrcdd5cvSIwwDy2AOlE7htSNp7QIZ10fLMyRCveesMLg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -4053,7 +4059,7 @@ packages: '@types/semver': 7.3.13 '@typescript-eslint/scope-manager': 5.59.11 '@typescript-eslint/types': 5.59.11 - '@typescript-eslint/typescript-estree': 5.59.11(typescript@5.1.3) + '@typescript-eslint/typescript-estree': 5.59.11(typescript@5.1.6) eslint: 8.43.0 eslint-scope: 5.1.1 semver: 7.3.8 @@ -5069,7 +5075,7 @@ packages: vary: 1.1.2 dev: true - /cosmiconfig-typescript-loader@4.3.0(@types/node@20.3.1)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.1.3): + /cosmiconfig-typescript-loader@4.3.0(@types/node@20.3.1)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.1.6): resolution: {integrity: sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==} engines: {node: '>=12', npm: '>=6'} peerDependencies: @@ -5080,8 +5086,8 @@ packages: dependencies: '@types/node': 20.3.1 cosmiconfig: 8.2.0 - ts-node: 10.9.1(@types/node@20.3.1)(typescript@5.1.3) - typescript: 5.1.3 + ts-node: 10.9.1(@types/node@20.3.1)(typescript@5.1.6) + typescript: 5.1.6 dev: true /cosmiconfig@6.0.0: @@ -5546,7 +5552,7 @@ packages: eslint-plugin-import: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.59.11(@typescript-eslint/parser@5.59.11)(eslint@8.43.0)(typescript@5.1.3) + '@typescript-eslint/eslint-plugin': 5.59.11(@typescript-eslint/parser@5.59.11)(eslint@8.43.0)(typescript@5.1.6) eslint: 8.43.0 eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.11)(eslint-plugin-import@2.27.5)(eslint@8.43.0) eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.11)(eslint-import-resolver-typescript@3.5.5)(eslint@8.43.0) @@ -5607,7 +5613,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.59.11(eslint@8.43.0)(typescript@5.1.3) + '@typescript-eslint/parser': 5.59.11(eslint@8.43.0)(typescript@5.1.6) debug: 3.2.7(supports-color@5.5.0) eslint: 8.43.0 eslint-import-resolver-node: 0.3.7 @@ -5626,7 +5632,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.59.11(eslint@8.43.0)(typescript@5.1.3) + '@typescript-eslint/parser': 5.59.11(eslint@8.43.0)(typescript@5.1.6) array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 @@ -7079,7 +7085,7 @@ packages: pretty-format: 29.5.0 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1(@types/node@20.3.1)(typescript@5.1.3) + ts-node: 10.9.1(@types/node@20.3.1)(typescript@5.1.6) transitivePeerDependencies: - supports-color dev: true @@ -9830,7 +9836,7 @@ packages: matchit: 1.1.0 dev: false - /ts-jest@29.1.0(@babel/core@7.19.6)(jest@29.5.0)(typescript@5.1.3): + /ts-jest@29.1.0(@babel/core@7.19.6)(jest@29.5.0)(typescript@5.1.6): resolution: {integrity: sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -9860,11 +9866,11 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.3.8 - typescript: 5.1.3 + typescript: 5.1.6 yargs-parser: 21.1.1 dev: true - /ts-node@10.9.1(@types/node@20.3.1)(typescript@5.1.3): + /ts-node@10.9.1(@types/node@20.3.1)(typescript@5.1.6): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -9890,7 +9896,7 @@ packages: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.1.3 + typescript: 5.1.6 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: true @@ -9929,14 +9935,14 @@ packages: engines: {node: '>=0.6.x'} dev: false - /tsutils@3.21.0(typescript@5.1.3): + /tsutils@3.21.0(typescript@5.1.6): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 5.1.3 + typescript: 5.1.6 dev: true /tty-table@4.1.6: @@ -10017,7 +10023,7 @@ packages: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} dev: false - /typedoc@0.24.8(typescript@5.1.3): + /typedoc@0.24.8(typescript@5.1.6): resolution: {integrity: sha512-ahJ6Cpcvxwaxfu4KtjA8qZNqS43wYt6JL27wYiIgl1vd38WW/KWX11YuAeZhuz9v+ttrutSsgK+XO1CjL1kA3w==} engines: {node: '>= 14.14'} hasBin: true @@ -10028,11 +10034,11 @@ packages: marked: 4.3.0 minimatch: 9.0.1 shiki: 0.14.2 - typescript: 5.1.3 + typescript: 5.1.6 dev: true - /typescript@5.1.3: - resolution: {integrity: sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==} + /typescript@5.1.6: + resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} engines: {node: '>=14.17'} hasBin: true dev: true