From a310cef11dce49561140575bc9d2974c2b3c7c26 Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Wed, 2 Oct 2024 14:24:56 +0200 Subject: [PATCH 01/19] Started working on #9742 --- modules/lib/core/index.d.ts | 143 ++- modules/lib/package-lock.json | 1790 ++++++++++++++++++++++++++- modules/lib/package.json | 8 +- modules/lib/test/Request.test-d.ts | 747 +++++++++++ modules/lib/test/Response.test-d.ts | 109 ++ 5 files changed, 2786 insertions(+), 11 deletions(-) create mode 100644 modules/lib/test/Request.test-d.ts create mode 100644 modules/lib/test/Response.test-d.ts diff --git a/modules/lib/core/index.d.ts b/modules/lib/core/index.d.ts index 2eb7986fc45..64fea22084e 100644 --- a/modules/lib/core/index.d.ts +++ b/modules/lib/core/index.d.ts @@ -130,9 +130,9 @@ export interface ScriptValue { hasMember(key: string): boolean; - getMember(key: string): ScriptValueDefinition; + getMember(key: string): ScriptValue; - getArray(): ScriptValueDefinition[]; + getArray(): ScriptValue[]; getMap(): Record; @@ -142,6 +142,121 @@ export interface ScriptValue { export type XpRequire = (path: Key) => Key extends keyof XpLibraries ? XpLibraries[Key] : unknown; +export interface DefaultCookies { + [key: string]: string|undefined + enonic_xp_tour?: string + JSESSIONID?: string +} + +export interface DefaultRequestHeaders { + [headerName: string]: string|undefined + Accept?: string + 'Accept-Charset'?: string + 'Accept-Encoding'?: string + 'Accept-Language'?: string + Authorization?: string + 'Cache-Control'?: string + Connection?: string + 'Content-Length'?: string + 'Content-Type'?: string + Cookie?: string + Language?: string + Host?: string + 'If-None-Match'?: string + 'Referer'?: string + 'sec-ch-ua'?: string + 'sec-ch-ua-mobile'?: string + 'sec-ch-ua-platform'?: string + 'Sec-Fetch-Dest'?: string + 'Sec-Fetch-Mode'?: string + 'Sec-Fetch-Site'?: string + 'Sec-Fetch-User'?: string + 'Upgrade-Insecure-Requests'?: string + 'User-Agent'?: string + 'X-Forwarded-For'?: string + 'X-Forwarded-Host'?: string + 'X-Forwarded-Proto'?: string + 'X-Forwarded-Server'?: string +} + +export type LowercaseKeys = { + [K in keyof T as Lowercase]: T[K] +}; + +export interface DefaultOptionalRequestProperties { + [index: string]: unknown|undefined; // Custom properties are allowed + body?: string + contextPath?: string + contentType?: string + cookies?: DefaultCookies + followRedirects?: boolean + headers?: DefaultRequestHeaders + params?: Record + pathParams?: Record + rawPath?: string + repositoryId?: string + remoteAddress?: string + validTicket?: boolean + webSocket?: boolean +} + +type LiteralUnion = T | (U & Record); + +export type Request< + T extends Record = DefaultOptionalRequestProperties +> = { + branch: LiteralUnion<'draft'|'master'> + host: string + method: LiteralUnion<'GET'|'POST'|'PUT'|'DELETE'|'HEAD'|'OPTIONS'|'PATCH'|'TRACE'|'CONNECT'> + mode: LiteralUnion<'edit'|'inline'|'live'|'preview'|'admin'> + path: string + port: number // |string // TODO Where have I seen this as string? + scheme: LiteralUnion<'http'|'https'> + url: string +} & T; + +export interface ComplexCookie { + value: string + path?: string + domain?: string + comment?: string + maxAge?: number + secure?: boolean + httpOnly?: boolean + sameSite?: string +} + +export interface DefaultResponseHeaders extends Record { + 'Cache-Control'?: string + 'Content-Encoding'?: string + 'Content-Type'?: string + 'Content-Security-Policy'?: string + 'Date'?: string + Etag?: string|number + Location?: string +} + +export interface PageContributions { + headBegin?: string[] + headEnd?: string[] + bodyBegin?: string[] + bodyEnd?: string[] +} + +export type Response< + T extends Record = { + applyFilters?: boolean + body?: string|ByteSource + contentType?: string + cookies?: Record + headers?: DefaultResponseHeaders + pageContributions?: PageContributions + postProcess?: boolean + redirect?: string + status?: number + } +> = T; + export type UserKey = `user:${string}:${string}`; export type GroupKey = `group:${string}:${string}`; export type RoleKey = `role:${string}`; @@ -263,7 +378,13 @@ export interface TextComponent { export type Component< Descriptor extends ComponentDescriptor = LayoutDescriptor | PageDescriptor | PartDescriptor, Config extends NestedRecord = NestedRecord, - Regions extends Record = Record, + Regions extends ( + Descriptor extends LayoutDescriptor + ? Record> + : Record + ) = Descriptor extends LayoutDescriptor + ? Record> + : Record, > = | FragmentComponent | LayoutComponent @@ -289,10 +410,26 @@ export interface LayoutRegion< components: Components; } +// type HasLayout = T extends [infer F, ...infer R] +// ? F extends LayoutComponent | Layout +// ? true +// : HasLayout +// : false; + export type Region< Components extends + // ( + // HasLayout extends true + // ? (FragmentComponent | LayoutComponent | PartComponent | TextComponent)[] + // : (FragmentComponent | PartComponent | TextComponent)[] + // ) = ( + // HasLayout extends true + // ? (FragmentComponent | Layout | Part | TextComponent)[] + // : (FragmentComponent | Part | TextComponent)[] + // ) (FragmentComponent | LayoutComponent | PartComponent | TextComponent)[] = (FragmentComponent | Layout | Part | TextComponent)[] +// @ts-expect-error TODO LayoutRegion can't eat LayoutComponent nor Layout!!! > = PageRegion | LayoutRegion; export interface Content< diff --git a/modules/lib/package-lock.json b/modules/lib/package-lock.json index ca458b7c1a9..bd0efb6ccc8 100644 --- a/modules/lib/package-lock.json +++ b/modules/lib/package-lock.json @@ -14,6 +14,7 @@ "@typescript-eslint/parser": "^5.23.0", "eslint": "^8.15.0", "jsdoc": "^3.6.11", + "tsd": "^0.31.2", "typescript": "^4.8.4" } }, @@ -22,6 +23,124 @@ "version": "0.0.1", "extraneous": true }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/parser": { "version": "7.18.11", "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==", @@ -90,6 +209,19 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", @@ -122,6 +254,41 @@ "node": ">= 8" } }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsd/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-saiCxzHRhUrRxQV2JhH580aQUZiKQUXI38FcAcikcfOomAil4G4lxT0RfrrKywoAYP/rqAdYXYmNRLppcd+hQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@types/eslint": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", + "integrity": "sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.11", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", @@ -146,6 +313,20 @@ "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", "dev": true }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.31.0", "integrity": "sha512-VKW4JPHzG5yhYQrQ1AzXgVgX8ZAJEvCz0QI6mLRX4tf7rnFfh5D8SKm0Pq6w5PyNfAWJk6sv313+nEt3ohWMBQ==", @@ -356,6 +537,35 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", @@ -391,6 +601,16 @@ "node": ">=8" } }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", @@ -430,6 +650,34 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/catharsis": { "version": "0.9.0", "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", @@ -506,11 +754,58 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", @@ -533,6 +828,13 @@ "node": ">=6.0.0" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/entities": { "version": "2.1.0", "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", @@ -541,6 +843,16 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", @@ -607,6 +919,36 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-formatter-pretty": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-formatter-pretty/-/eslint-formatter-pretty-4.1.0.tgz", + "integrity": "sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "^7.2.13", + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "eslint-rule-docs": "^1.1.5", + "log-symbols": "^4.0.0", + "plur": "^4.0.0", + "string-width": "^4.2.0", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-rule-docs": { + "version": "1.1.235", + "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.235.tgz", + "integrity": "sha512-+TQ+x4JdTnDoFEXXb3fDvfGOwnyNV7duH8fXWTPD1ieaBmB8omj7Gw/pMBBu4uI2uJCCU8APDaQJzWuXnTsH4A==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint-scope": { "version": "5.1.1", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", @@ -851,6 +1193,16 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/functional-red-black-tree": { "version": "1.0.1", "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", @@ -910,6 +1262,16 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/has-flag": { "version": "4.0.0", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", @@ -918,6 +1280,32 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ignore": { "version": "5.2.0", "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", @@ -949,6 +1337,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", @@ -963,6 +1361,39 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/irregular-plurals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", + "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", @@ -971,6 +1402,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", @@ -991,11 +1432,67 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.0", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", @@ -1051,6 +1548,13 @@ "node": ">=8" } }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", @@ -1061,6 +1565,16 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/klaw": { "version": "3.0.0", "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", @@ -1081,6 +1595,13 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/linkify-it": { "version": "3.0.3", "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", @@ -1113,9 +1634,26 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/lru-cache": { - "version": "6.0.0", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "dependencies": { "yallist": "^4.0.0" @@ -1124,6 +1662,19 @@ "node": ">=10" } }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/markdown-it": { "version": "12.3.2", "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", @@ -1164,6 +1715,46 @@ "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", "dev": true }, + "node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", @@ -1184,6 +1775,16 @@ "node": ">=8.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", @@ -1195,6 +1796,21 @@ "node": "*" } }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", @@ -1216,6 +1832,22 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/once": { "version": "1.4.0", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", @@ -1268,6 +1900,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", @@ -1279,6 +1921,25 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", @@ -1303,6 +1964,13 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-type": { "version": "4.0.0", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", @@ -1311,6 +1979,13 @@ "node": ">=8" } }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", @@ -1322,6 +1997,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/plur": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", + "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "irregular-plurals": "^3.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", @@ -1330,6 +2021,34 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/punycode": { "version": "2.1.1", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", @@ -1357,6 +2076,177 @@ } ] }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/regexpp": { "version": "3.2.0", "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", @@ -1376,6 +2266,24 @@ "lodash": "^4.17.14" } }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", @@ -1489,6 +2397,57 @@ "node": ">=8" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", @@ -1500,6 +2459,19 @@ "node": ">=8" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", @@ -1522,6 +2494,33 @@ "node": ">=8" } }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/taffydb": { "version": "2.6.2", "integrity": "sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA==", @@ -1544,6 +2543,38 @@ "node": ">=8.0" } }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tsd": { + "version": "0.31.2", + "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.31.2.tgz", + "integrity": "sha512-VplBAQwvYrHzVihtzXiUVXu5bGcr7uH1juQZ1lmKgkuGNGT+FechUCqmx9/zk7wibcqR2xaNEwCkDyKh+VVZnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tsd/typescript": "~5.4.3", + "eslint-formatter-pretty": "^4.1.0", + "globby": "^11.0.1", + "jest-diff": "^29.0.3", + "meow": "^9.0.0", + "path-exists": "^4.0.0", + "read-pkg-up": "^7.0.0" + }, + "bin": { + "tsd": "dist/cli.js" + }, + "engines": { + "node": ">=14.16" + } + }, "node_modules/tslib": { "version": "1.14.1", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", @@ -1621,6 +2652,17 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", @@ -1659,6 +2701,16 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", @@ -1672,6 +2724,92 @@ } }, "dependencies": { + "@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true + }, + "@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "@babel/parser": { "version": "7.18.11", "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==", @@ -1719,6 +2857,15 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", @@ -1738,10 +2885,38 @@ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "@tsd/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-saiCxzHRhUrRxQV2JhH580aQUZiKQUXI38FcAcikcfOomAil4G4lxT0RfrrKywoAYP/rqAdYXYmNRLppcd+hQQ==", + "dev": true + }, + "@types/eslint": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", + "integrity": "sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" } }, + "@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, "@types/json-schema": { "version": "7.0.11", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", @@ -1766,6 +2941,18 @@ "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", "dev": true }, + "@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true + }, + "@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "5.31.0", "integrity": "sha512-VKW4JPHzG5yhYQrQ1AzXgVgX8ZAJEvCz0QI6mLRX4tf7rnFfh5D8SKm0Pq6w5PyNfAWJk6sv313+nEt3ohWMBQ==", @@ -1875,6 +3062,23 @@ "uri-js": "^4.2.2" } }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } + } + }, "ansi-regex": { "version": "5.0.1", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", @@ -1898,6 +3102,12 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true + }, "balanced-match": { "version": "1.0.2", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", @@ -1931,6 +3141,23 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + } + }, "catharsis": { "version": "0.9.0", "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", @@ -1984,11 +3211,41 @@ "ms": "2.1.2" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + }, + "decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true + } + } + }, "deep-is": { "version": "0.1.4", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, "dir-glob": { "version": "3.0.1", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", @@ -2005,11 +3262,26 @@ "esutils": "^2.0.2" } }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "entities": { "version": "2.1.0", "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", "dev": true }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, "escape-string-regexp": { "version": "4.0.0", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", @@ -2077,6 +3349,28 @@ } } }, + "eslint-formatter-pretty": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-formatter-pretty/-/eslint-formatter-pretty-4.1.0.tgz", + "integrity": "sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ==", + "dev": true, + "requires": { + "@types/eslint": "^7.2.13", + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "eslint-rule-docs": "^1.1.5", + "log-symbols": "^4.0.0", + "plur": "^4.0.0", + "string-width": "^4.2.0", + "supports-hyperlinks": "^2.0.0" + } + }, + "eslint-rule-docs": { + "version": "1.1.235", + "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.235.tgz", + "integrity": "sha512-+TQ+x4JdTnDoFEXXb3fDvfGOwnyNV7duH8fXWTPD1ieaBmB8omj7Gw/pMBBu4uI2uJCCU8APDaQJzWuXnTsH4A==", + "dev": true + }, "eslint-scope": { "version": "5.1.1", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", @@ -2246,6 +3540,12 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, "functional-red-black-tree": { "version": "1.0.1", "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", @@ -2290,11 +3590,35 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true + }, "has-flag": { "version": "4.0.0", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, "ignore": { "version": "5.2.0", "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", @@ -2314,6 +3638,12 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, "inflight": { "version": "1.0.6", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", @@ -2328,11 +3658,38 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "irregular-plurals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", + "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "requires": { + "hasown": "^2.0.2" + } + }, "is-extglob": { "version": "2.1.1", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, "is-glob": { "version": "4.0.3", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", @@ -2347,11 +3704,47 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, "isexe": { "version": "2.0.0", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "js-yaml": { "version": "4.1.0", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", @@ -2397,6 +3790,12 @@ } } }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "json-schema-traverse": { "version": "0.4.1", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", @@ -2407,6 +3806,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, "klaw": { "version": "3.0.0", "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", @@ -2424,6 +3829,12 @@ "type-check": "~0.4.0" } }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, "linkify-it": { "version": "3.0.3", "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", @@ -2450,6 +3861,16 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, "lru-cache": { "version": "6.0.0", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", @@ -2458,6 +3879,12 @@ "yallist": "^4.0.0" } }, + "map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true + }, "markdown-it": { "version": "12.3.2", "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", @@ -2486,6 +3913,34 @@ "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", "dev": true }, + "meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "requires": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "dependencies": { + "type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true + } + } + }, "merge2": { "version": "1.4.1", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", @@ -2500,6 +3955,12 @@ "picomatch": "^2.3.1" } }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, "minimatch": { "version": "3.1.2", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", @@ -2508,6 +3969,17 @@ "brace-expansion": "^1.1.7" } }, + "minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + } + }, "mkdirp": { "version": "1.0.4", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", @@ -2523,6 +3995,18 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "requires": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + } + }, "once": { "version": "1.4.0", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", @@ -2560,6 +4044,12 @@ "p-limit": "^3.0.2" } }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, "parent-module": { "version": "1.0.1", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", @@ -2568,6 +4058,18 @@ "callsites": "^3.0.0" } }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, "path-exists": { "version": "4.0.0", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", @@ -2583,21 +4085,61 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, "path-type": { "version": "4.0.0", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, "picomatch": { "version": "2.3.1", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "plur": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", + "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", + "dev": true, + "requires": { + "irregular-plurals": "^3.2.0" + } + }, "prelude-ls": { "version": "1.2.1", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, "punycode": { "version": "2.1.1", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", @@ -2608,6 +4150,128 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true + }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, "regexpp": { "version": "3.2.0", "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", @@ -2621,6 +4285,17 @@ "lodash": "^4.17.14" } }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, "resolve-from": { "version": "4.0.0", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", @@ -2688,6 +4363,49 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "strip-ansi": { "version": "6.0.1", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", @@ -2696,6 +4414,15 @@ "ansi-regex": "^5.0.1" } }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, "strip-json-comments": { "version": "3.1.1", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", @@ -2709,6 +4436,22 @@ "has-flag": "^4.0.0" } }, + "supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, "taffydb": { "version": "2.6.2", "integrity": "sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA==", @@ -2728,6 +4471,27 @@ "is-number": "^7.0.0" } }, + "trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true + }, + "tsd": { + "version": "0.31.2", + "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.31.2.tgz", + "integrity": "sha512-VplBAQwvYrHzVihtzXiUVXu5bGcr7uH1juQZ1lmKgkuGNGT+FechUCqmx9/zk7wibcqR2xaNEwCkDyKh+VVZnQ==", + "dev": true, + "requires": { + "@tsd/typescript": "~5.4.3", + "eslint-formatter-pretty": "^4.1.0", + "globby": "^11.0.1", + "jest-diff": "^29.0.3", + "meow": "^9.0.0", + "path-exists": "^4.0.0", + "read-pkg-up": "^7.0.0" + } + }, "tslib": { "version": "1.14.1", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", @@ -2783,6 +4547,16 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "which": { "version": "2.0.2", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", @@ -2812,6 +4586,12 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + }, "yocto-queue": { "version": "0.1.0", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", diff --git a/modules/lib/package.json b/modules/lib/package.json index bf90c731e81..2a58b52ed05 100644 --- a/modules/lib/package.json +++ b/modules/lib/package.json @@ -7,7 +7,8 @@ "build": "tsc --declaration --project tsconfig.build.json", "lint": "eslint . --quiet --cache", "fix": "eslint . --fix --cache", - "jsdoc": "jsdoc --configure jsdoc/conf.json" + "jsdoc": "jsdoc --configure jsdoc/conf.json", + "test:types": "tsd -t build/npm/global.d.ts -f test/**/*.test-d.ts" }, "repository": { "type": "git", @@ -26,7 +27,8 @@ "@typescript-eslint/eslint-plugin": "^5.23.0", "@typescript-eslint/parser": "^5.23.0", "eslint": "^8.15.0", - "typescript": "^4.8.4", - "jsdoc": "^3.6.11" + "jsdoc": "^3.6.11", + "tsd": "^0.31.2", + "typescript": "^4.8.4" } } diff --git a/modules/lib/test/Request.test-d.ts b/modules/lib/test/Request.test-d.ts new file mode 100644 index 00000000000..bd076d306d2 --- /dev/null +++ b/modules/lib/test/Request.test-d.ts @@ -0,0 +1,747 @@ +import type {Request} from '../core/index'; + +import { + expectAssignable, + // expectDeprecated, + // expectDocCommentIncludes, + // expectError, + // expectNever, + expectNotAssignable, + // expectNotDeprecated, + // expectNotType, + // expectType, + // printType, +} from 'tsd'; + + +// Scenario: When implementing a idprovider login function the Request may have a validTicket property +// Scenario: When implementing a idprovider logout function the Request may have a validTicket property +const idproviderLogoutRequest = { + branch: 'draft', + contextPath: '/_/idprovider/system', + cookies: { + 'app.browse.RecentItemsList': 'base%3Afolder%7Cportal%3Asite', + JSESSIONID: '90g5qk9dul0uwl4r023jb08z0', + }, + headers: { + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + // ... + }, + host: 'localhost', + method: 'GET', + mode: 'live', + params: { + redirect: 'http://localhost:8080/admin', + }, + path: '/_/idprovider/system/logout', + port: 8080, + rawPath: '/_/idprovider/system/logout', + remoteAddress: '127.0.0.1', + url: 'http://localhost:8080/_/idprovider/system/logout?redirect=http%3A%2F%2Flocalhost%3A8080%2Fadmin&_ticket=6ce7eadaff4dffcb8c8930a4179af5c47680613a', + scheme: 'http', + validTicket: true, + webSocket: false, +}; + +// printType(idproviderLogoutRequest); + +expectAssignable(idproviderLogoutRequest); + +// Scenario: Webapp request + +const webappControllerGetRequest = { + 'method': 'GET', + 'scheme': 'http', + 'host': 'localhost', + 'port': 8080, + 'path': '/webapp/com.acme.example.tsup', + 'rawPath': '/webapp/com.acme.example.tsup/', + 'url': 'http://localhost:8080/webapp/com.acme.example.tsup', + 'remoteAddress': '127.0.0.1', + 'mode': 'live', + 'webSocket': false, + 'repositoryId': 'com.enonic.cms.default', + 'branch': 'draft', + 'contextPath': '/webapp/com.acme.example.tsup', + 'params': {}, + 'headers': { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', + 'Connection': 'keep-alive', + 'Cookie': 'JSESSIONID=15hb7cb69ai5vrbg9msvtjmur0; app.browse.RecentItemsList=com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', + 'Host': 'localhost:8080', + 'Referer': 'http://localhost:8080/admin/tool/com.enonic.xp.app.applications/main', + 'sec-ch-ua': '\'Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128\'', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '\'macOS\'', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'same-origin', + 'Sec-Fetch-User': '?1', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + 'cookies': { + 'app.browse.RecentItemsList': 'com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', + 'JSESSIONID': '15hb7cb69ai5vrbg9msvtjmur0', + }, + 'pathParams': {}, +}; +expectAssignable(webappControllerGetRequest); + +// Scenario: A page controller get request does not have a contextPath property +const pageRequest = { + method: 'HEAD', + scheme: 'http', + host: 'localhost', + port: 8080, + path: '/admin/site/inline/my-project/draft/my-site', + rawPath: '/admin/site/inline/my-project/draft/my-site', + url: 'http://localhost:8080/admin/site/inline/my-project/draft/my-site', + remoteAddress: '127.0.0.1', + mode: 'inline', + webSocket: false, + repositoryId: 'com.enonic.cms.my-project', + branch: 'draft', + params: {}, + headers: { + Accept: '*/*', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', + Connection: 'keep-alive', + Cookie: 'app.browse.RecentItemsList=base%3Afolder%7Cportal%3Asite; JSESSIONID=15hb7cb69ai5vrbg9msvtjmur0', + Host: 'localhost:8080', + Referer: 'http://localhost:8080/admin/tool/com.enonic.app.contentstudio/main', + 'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + cookies: { + 'app.browse.RecentItemsList': 'base%3Afolder%7Cportal%3Asite', + JSESSIONID: '15hb7cb69ai5vrbg9msvtjmur0', + }, +}; + +expectAssignable(pageRequest); + +// Scenario: A layout controller get request +const layoutControllerGetRequest = { + 'method': 'GET', + 'scheme': 'http', + 'host': 'localhost', + 'port': 8080, + 'path': '/admin/site/edit/my-project/draft/ce47cc0b-2502-4df5-add1-826204b3fc72/_/component/main/0', + 'rawPath': '/admin/site/edit/my-project/draft/ce47cc0b-2502-4df5-add1-826204b3fc72/_/component/main/0', + 'url': 'http://localhost:8080/admin/site/edit/my-project/draft/ce47cc0b-2502-4df5-add1-826204b3fc72/_/component/main/0', + 'remoteAddress': '127.0.0.1', + 'mode': 'edit', + 'webSocket': false, + 'repositoryId': 'com.enonic.cms.my-project', + 'branch': 'draft', + 'params': {}, + 'headers': { + 'Accept': '*/*', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', + 'Connection': 'keep-alive', + 'Cookie': 'JSESSIONID=15hb7cb69ai5vrbg9msvtjmur0; app.browse.RecentItemsList=base%3Afolder%7Ccom.example.myproject%3Aperson%7Cportal%3Asite', + 'Host': 'localhost:8080', + 'Referer': 'http://localhost:8080/admin/site/edit/my-project/draft/ce47cc0b-2502-4df5-add1-826204b3fc72', + 'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + 'X-Requested-With': 'XMLHttpRequest', + }, + 'cookies': { + 'app.browse.RecentItemsList': 'base%3Afolder%7Ccom.example.myproject%3Aperson%7Cportal%3Asite', + 'JSESSIONID': '15hb7cb69ai5vrbg9msvtjmur0', + }, +}; +expectAssignable(layoutControllerGetRequest); + +// Scenario: A layout fragment request +const layoutFragmentRequest ={ + 'method': 'GET', + 'scheme': 'http', + 'host': 'localhost', + 'port': 8080, + 'path': '/admin/site/preview/my-project/draft/my-site/page-with-layout/fragment-sample-layout', + 'rawPath': '/admin/site/preview/my-project/draft/my-site/page-with-layout/fragment-sample-layout', + 'url': 'http://localhost:8080/admin/site/preview/my-project/draft/my-site/page-with-layout/fragment-sample-layout', + 'remoteAddress': '127.0.0.1', + 'mode': 'preview', + 'webSocket': false, + 'repositoryId': 'com.enonic.cms.my-project', + 'branch': 'draft', + 'params': {}, + 'headers': { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', + 'Connection': 'keep-alive', + 'Cookie': 'JSESSIONID=15hb7cb69ai5vrbg9msvtjmur0; app.browse.RecentItemsList=base%3Afolder%7Ccom.example.myproject%3Aperson%7Cportal%3Asite', + 'Host': 'localhost:8080', + 'Referer': 'http://localhost:8080/admin/tool/com.enonic.app.contentstudio/main/my-project/edit/d0ea0d64-1736-423c-83cd-3e4987757df6', + 'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'same-origin', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + 'cookies': { + 'app.browse.RecentItemsList': 'base%3Afolder%7Ccom.example.myproject%3Aperson%7Cportal%3Asite', + 'JSESSIONID': '15hb7cb69ai5vrbg9msvtjmur0', + }, +}; +expectAssignable(layoutFragmentRequest); + +// Scenario: A layout componentUrl request + +// Scenario: A part controller get request +const partControllerGetRequest = { + 'method': 'GET', + 'scheme': 'http', + 'host': 'localhost', + 'port': 8080, + 'path': '/admin/site/edit/my-project/draft/419f1109-85b4-4336-916d-416877687b65/_/component/main/0', + 'rawPath': '/admin/site/edit/my-project/draft/419f1109-85b4-4336-916d-416877687b65/_/component/main/0', + 'url': 'http://localhost:8080/admin/site/edit/my-project/draft/419f1109-85b4-4336-916d-416877687b65/_/component/main/0', + 'remoteAddress': '127.0.0.1', + 'mode': 'edit', + 'webSocket': false, + 'repositoryId': 'com.enonic.cms.my-project', + 'branch': 'draft', + 'params': {}, + 'headers': { + 'Accept': '*/*', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', + 'Connection': 'keep-alive', + 'Cookie': 'JSESSIONID=15hb7cb69ai5vrbg9msvtjmur0; app.browse.RecentItemsList=base%3Afolder%7Ccom.example.myproject%3Aperson%7Cportal%3Asite', + 'Host': 'localhost:8080', + 'Referer': 'http://localhost:8080/admin/site/edit/my-project/draft/419f1109-85b4-4336-916d-416877687b65', + 'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + 'X-Requested-With': 'XMLHttpRequest', + }, + 'cookies': { + 'app.browse.RecentItemsList': 'base%3Afolder%7Ccom.example.myproject%3Aperson%7Cportal%3Asite', + 'JSESSIONID': '15hb7cb69ai5vrbg9msvtjmur0', + }, +}; +expectAssignable(partControllerGetRequest); + +// Scenario: A part fragment request +const partFragmentRequest = { + 'method': 'GET', + 'scheme': 'http', + 'host': 'localhost', + 'port': 8080, + 'path': '/admin/site/preview/my-project/draft/my-site/page-with-part/fragment-sample-part', + 'rawPath': '/admin/site/preview/my-project/draft/my-site/page-with-part/fragment-sample-part', + 'url': 'http://localhost:8080/admin/site/preview/my-project/draft/my-site/page-with-part/fragment-sample-part', + 'remoteAddress': '127.0.0.1', + 'mode': 'preview', + 'webSocket': false, + 'repositoryId': 'com.enonic.cms.my-project', + 'branch': 'draft', + 'params': {}, + 'headers': { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', + 'Connection': 'keep-alive', + 'Cookie': 'JSESSIONID=15hb7cb69ai5vrbg9msvtjmur0; app.browse.RecentItemsList=base%3Afolder%7Ccom.example.myproject%3Aperson%7Cportal%3Asite', + 'Host': 'localhost:8080', + 'Referer': 'http://localhost:8080/admin/tool/com.enonic.app.contentstudio/main/my-project/edit/78eda2c1-3e8b-4356-b619-497bc8302a7b', + 'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'same-origin', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + 'cookies': { + 'app.browse.RecentItemsList': 'base%3Afolder%7Ccom.example.myproject%3Aperson%7Cportal%3Asite', + 'JSESSIONID': '15hb7cb69ai5vrbg9msvtjmur0', + }, +}; +expectAssignable(partFragmentRequest); + +// Scenario: A part componentUrl request + + +// Scenario: The request inside an error controller handleError function +// This particaular error is a 404, and doesn't include contextPath either. +const error = { + status: 404, + message: 'Page [/my-site/asdadfs] not found', + request: { + method: 'GET', + scheme: 'http', + host: 'localhost', + port: 8080, + path: '/admin/site/preview/my-project/draft/my-site/asdadfs', + rawPath: '/admin/site/preview/my-project/draft/my-site/asdadfs', + url: 'http://localhost:8080/admin/site/preview/my-project/draft/my-site/asdadfs', + remoteAddress: '127.0.0.1', + mode: 'preview', + webSocket: false, + repositoryId: 'com.enonic.cms.my-project', + branch: 'draft', + params: {}, + headers: { + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', + Connection: 'keep-alive', + Cookie: 'app.browse.RecentItemsList=base%3Afolder%7Cportal%3Asite; JSESSIONID=15hb7cb69ai5vrbg9msvtjmur0', + Host: 'localhost:8080', + 'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-User': '?1', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + cookies: { + 'app.browse.RecentItemsList': 'base%3Afolder%7Cportal%3Asite', + JSESSIONID: '15hb7cb69ai5vrbg9msvtjmur0', + }, + }, +}; + +expectAssignable(error.request); + + +// Scenario: Service request +const serviceRequest ={ + method: 'GET', + scheme: 'http', + host: 'localhost', + port: 8080, + path: '/admin/site/inline/my-project/draft/my-site/folder-using-dynamicpage/_/service/com.example.app.lib.static.page/myStatic/css/style.css', + rawPath: '/admin/site/inline/my-project/draft/my-site/folder-using-dynamicpage/_/service/com.example.app.lib.static.page/myStatic/css/style.css', + url: 'http://localhost:8080/admin/site/inline/my-project/draft/my-site/folder-using-dynamicpage/_/service/com.example.app.lib.static.page/myStatic/css/style.css', + remoteAddress: '127.0.0.1', + mode: 'inline', + webSocket: false, + repositoryId: 'com.enonic.cms.my-project', + branch: 'draft', + contextPath: '/admin/site/inline/my-project/draft/my-site/folder-using-dynamicpage/_/service/com.example.app.lib.static.page/myStatic', + params: {}, + headers: { + Accept: 'text/css,*/*;q=0.1', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', + Connection: 'keep-alive', + Cookie: 'app.browse.RecentItemsList=base%3Afolder%7Cportal%3Asite; JSESSIONID=15hb7cb69ai5vrbg9msvtjmur0', + Host: 'localhost:8080', + Referer: 'http://localhost:8080/admin/site/inline/my-project/draft/my-site/folder-using-dynamicpage', + 'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'Sec-Fetch-Dest': 'style', + 'Sec-Fetch-Mode': 'no-cors', + 'Sec-Fetch-Site': 'same-origin', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + cookies: { + 'app.browse.RecentItemsList': 'base%3Afolder%7Cportal%3Asite', + JSESSIONID: '15hb7cb69ai5vrbg9msvtjmur0', + }, +}; + +expectAssignable(serviceRequest); + +const siteMappingRequest = { + method: 'GET', + scheme: 'http', + host: 'localhost', + port: 8080, + path: '/admin/site/preview/my-project/draft/my-site/_static/css/style.css', + rawPath: '/admin/site/preview/my-project/draft/my-site/_static/css/style.css', + url: 'http://localhost:8080/admin/site/preview/my-project/draft/my-site/_static/css/style.css', + remoteAddress: '127.0.0.1', + mode: 'preview', + webSocket: false, + repositoryId: 'com.enonic.cms.my-project', + branch: 'draft', + contextPath: '/admin/site/preview/my-project/draft/my-site', + params: {}, + headers: { + Accept: 'text/css,*/*;q=0.1', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', + Connection: 'keep-alive', + Cookie: 'app.browse.RecentItemsList=base%3Afolder%7Cportal%3Asite; JSESSIONID=15hb7cb69ai5vrbg9msvtjmur0', + Host: 'localhost:8080', + Referer: 'http://localhost:8080/admin/site/preview/my-project/draft/my-site/_static/', + 'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'Sec-Fetch-Dest': 'style', + 'Sec-Fetch-Mode': 'no-cors', + 'Sec-Fetch-Site': 'same-origin', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + cookies: { + 'app.browse.RecentItemsList': 'base%3Afolder%7Cportal%3Asite', + JSESSIONID: '15hb7cb69ai5vrbg9msvtjmur0', + }, + pathParams: { + path: '/_static/css/style.css', + }, +}; + +expectAssignable(siteMappingRequest); + +// Scenario: Custom selector request + +const customSelectorRequest = { + method: 'GET', + scheme: 'http', + host: 'localhost', + port: 8080, + path: '/admin/site/edit/my-project/draft/022f32d3-df5b-4a0b-ac1a-fea778b128fa/_/service/com.example.myproject/countries', + rawPath: '/admin/site/edit/my-project/draft/022f32d3-df5b-4a0b-ac1a-fea778b128fa/_/service/com.example.myproject/countries', + url: 'http://localhost:8080/admin/site/edit/my-project/draft/022f32d3-df5b-4a0b-ac1a-fea778b128fa/_/service/com.example.myproject/countries?count=10', + remoteAddress: '127.0.0.1', + mode: 'edit', + webSocket: false, + repositoryId: 'com.enonic.cms.my-project', + branch: 'draft', + contextPath: '/admin/site/edit/my-project/draft/022f32d3-df5b-4a0b-ac1a-fea778b128fa/_/service/com.example.myproject/countries', + params: { + count: '10', + }, + headers: { + Accept: 'application/json', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', + Connection: 'keep-alive', + Cookie: 'JSESSIONID=15hb7cb69ai5vrbg9msvtjmur0; app.browse.RecentItemsList=com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', + Host: 'localhost:8080', + Referer: 'http://localhost:8080/admin/tool/com.enonic.app.contentstudio/main/my-project/edit/022f32d3-df5b-4a0b-ac1a-fea778b128fa', + 'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + cookies: { + 'app.browse.RecentItemsList': 'com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', + JSESSIONID: '15hb7cb69ai5vrbg9msvtjmur0', + }, +}; + +expectAssignable(customSelectorRequest); + +// TODO https://developer.enonic.com/docs/xp/stable/framework/filters +// TODO processors + +const responseProcessorRequest = { + 'method': 'HEAD', + 'scheme': 'http', + 'host': 'localhost', + 'port': 8080, + 'path': '/admin/site/inline/my-project/draft/my-site', + 'rawPath': '/admin/site/inline/my-project/draft/my-site', + 'url': 'http://localhost:8080/admin/site/inline/my-project/draft/my-site', + 'remoteAddress': '127.0.0.1', + 'mode': 'inline', + 'webSocket': false, + 'repositoryId': 'com.enonic.cms.my-project', + 'branch': 'draft', + 'params': {}, + 'headers': { + 'Accept': '*/*', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', + 'Connection': 'keep-alive', + 'Cookie': 'JSESSIONID=15hb7cb69ai5vrbg9msvtjmur0; app.browse.RecentItemsList=base%3Afolder%7Ccom.example.myproject%3Aperson%7Cportal%3Asite', + 'Host': 'localhost:8080', + 'Referer': 'http://localhost:8080/admin/tool/com.enonic.app.contentstudio/main', + 'sec-ch-ua': '\'Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128\'', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '\'macOS\'', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + 'cookies': { + 'app.browse.RecentItemsList': 'base%3Afolder%7Ccom.example.myproject%3Aperson%7Cportal%3Asite', + 'JSESSIONID': '15hb7cb69ai5vrbg9msvtjmur0', + }, +}; +expectAssignable(responseProcessorRequest); + +// Scenario: Admin tool request +const adminToolRequest = { + method: 'GET', + scheme: 'http', + host: 'localhost', + port: 8080, + path: '/admin/tool/com.acme.example.tsup/sample', + rawPath: '/admin/tool/com.acme.example.tsup/sample', + url: 'http://localhost:8080/admin/tool/com.acme.example.tsup/sample', + remoteAddress: '127.0.0.1', + mode: 'admin', + webSocket: false, + repositoryId: 'com.enonic.cms.default', + branch: 'draft', + contextPath: '/admin/tool/com.acme.example.tsup/sample', + params: {}, + headers: { + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', + Connection: 'keep-alive', + Cookie: 'JSESSIONID=15hb7cb69ai5vrbg9msvtjmur0; app.browse.RecentItemsList=com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', + Host: 'localhost:8080', + Referer: 'http://localhost:8080/admin/tool/com.enonic.app.contentstudio/main', + 'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'same-origin', + 'Sec-Fetch-User': '?1', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + cookies: { + 'app.browse.RecentItemsList': 'com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', + JSESSIONID: '15hb7cb69ai5vrbg9msvtjmur0', + }, +}; +expectAssignable(adminToolRequest); + +// Scenario: Admin context panel widget request +const contextpanelWidgetRequest = { + method: 'GET', + scheme: 'http', + host: 'localhost', + port: 8080, + path: '/admin/site/admin/my-project/draft/_/widgets/com.acme.example.tsup/contextPanel', + rawPath: '/admin/site/admin/my-project/draft/_/widgets/com.acme.example.tsup/contextPanel', + url: 'http://localhost:8080/admin/site/admin/my-project/draft/_/widgets/com.acme.example.tsup/contextPanel?repository=com.enonic.cms.my-project&branch=draft&t=1727866860019', + remoteAddress: '127.0.0.1', + mode: 'admin', + webSocket: false, + repositoryId: 'com.enonic.cms.my-project', + branch: 'draft', + contextPath: '/admin/site/admin/my-project/draft/_/widgets/com.acme.example.tsup/contextPanel', + params: { + repository: 'com.enonic.cms.my-project', + branch: 'draft', + t: '1727866860019', + }, + headers: { + Accept: '*/*', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', + Connection: 'keep-alive', + Cookie: 'JSESSIONID=15hb7cb69ai5vrbg9msvtjmur0; app.browse.RecentItemsList=com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', + Host: 'localhost:8080', + Referer: 'http://localhost:8080/admin/tool/com.enonic.app.contentstudio/main', + 'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + cookies: { + 'app.browse.RecentItemsList': 'com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', + JSESSIONID: '15hb7cb69ai5vrbg9msvtjmur0', + }, +}; +expectAssignable(contextpanelWidgetRequest); + +// Scenario: Admin dashboard widget request +const dashboardWidgetRequest = { + method: 'GET', + scheme: 'http', + host: 'localhost', + port: 8080, + path: '/admin/tool/_/widgets/com.acme.example.tsup/dashboard', + rawPath: '/admin/tool/_/widgets/com.acme.example.tsup/dashboard', + url: 'http://localhost:8080/admin/tool/_/widgets/com.acme.example.tsup/dashboard', + remoteAddress: '127.0.0.1', + mode: 'admin', + webSocket: false, + repositoryId: 'com.enonic.cms.default', + branch: 'draft', + contextPath: '/admin/tool/_/widgets/com.acme.example.tsup/dashboard', + params: {}, + headers: { + Accept: '*/*', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', + Connection: 'keep-alive', + Cookie: 'JSESSIONID=15hb7cb69ai5vrbg9msvtjmur0; app.browse.RecentItemsList=com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', + Host: 'localhost:8080', + Referer: 'http://localhost:8080/admin/tool', + 'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + cookies: { + 'app.browse.RecentItemsList': 'com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', + JSESSIONID: '15hb7cb69ai5vrbg9msvtjmur0', + }, +}; +expectAssignable(dashboardWidgetRequest); + +// Scenario: Admin menuitem widget request +const menuitemWidgetRequest = { + method: 'GET', + scheme: 'http', + host: 'localhost', + port: 8080, + path: '/admin/site/admin/my-project/draft/_/widgets/com.acme.example.tsup/menuItem', + rawPath: '/admin/site/admin/my-project/draft/_/widgets/com.acme.example.tsup/menuItem', + url: 'http://localhost:8080/admin/site/admin/my-project/draft/_/widgets/com.acme.example.tsup/menuItem', + remoteAddress: '127.0.0.1', + mode: 'admin', + webSocket: false, + repositoryId: 'com.enonic.cms.my-project', + branch: 'draft', + contextPath: '/admin/site/admin/my-project/draft/_/widgets/com.acme.example.tsup/menuItem', + params: {}, + headers: { + Accept: '*/*', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', + Connection: 'keep-alive', + Cookie: 'JSESSIONID=15hb7cb69ai5vrbg9msvtjmur0; app.browse.RecentItemsList=com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', + Host: 'localhost:8080', + Referer: 'http://localhost:8080/admin/tool/com.enonic.app.contentstudio/main', + 'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + cookies: { + 'app.browse.RecentItemsList': 'com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', + JSESSIONID: '15hb7cb69ai5vrbg9msvtjmur0', + }, +}; +expectAssignable(menuitemWidgetRequest); + +// Scenario: Illegal values for properties +const requiredProperties = { + branch: 'draft', + host: 'localhost', + method: 'GET', + mode: 'admin', + path: '/whatever', + port: 8080, + scheme: 'http', + url: 'http://localhost:8080/whatever', +}; +expectAssignable(requiredProperties); + +// LiteralUnion suggests 'draft'|'master', but allows string +expectAssignable({ + ...requiredProperties, + branch: 'string but not draft|master', +}); + +// LiteralUnion suggests 'GET'|'POST'|'PUT'|'DELETE'|'HEAD'|'OPTIONS'|'PATCH'|'TRACE'|'CONNECT', but allows string +expectAssignable({ + ...requiredProperties, + method: 'string but not GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|TRACE|CONNECT', +}); + +// LiteralUnion suggests 'edit'|'inline'|'live'|'preview'|'admin', but allows string +expectAssignable({ + ...requiredProperties, + mode: 'string but not edit|inline|live|preview|admin', +}); + +// LiteralUnion suggests 'http'|'https', but allows string +expectAssignable({ + ...requiredProperties, + scheme: 'string but not http|https', +}); + +expectNotAssignable({ + ...requiredProperties, + branch: 123, // not string +}); + +expectNotAssignable({ + ...requiredProperties, + host: 123, // not string +}); + +expectNotAssignable({ + ...requiredProperties, + method: 123, // not string +}); + +expectNotAssignable({ + ...requiredProperties, + mode: 123, // not string +}); + +expectNotAssignable({ + ...requiredProperties, + path: 123, // not string +}); + +expectNotAssignable({ + ...requiredProperties, + port: '8080', // not number +}); + +expectNotAssignable({ + ...requiredProperties, + scheme: 123, // not string +}); + +expectNotAssignable({ + ...requiredProperties, + url: 123, // not string +}); + +expectAssignable({ + ...requiredProperties, + custom: 'whatever', // Custom property allowed +}); + diff --git a/modules/lib/test/Response.test-d.ts b/modules/lib/test/Response.test-d.ts new file mode 100644 index 00000000000..9ea5c9f5699 --- /dev/null +++ b/modules/lib/test/Response.test-d.ts @@ -0,0 +1,109 @@ +import type {Response} from '../core/index'; + +import { + expectAssignable, + // expectDeprecated, + // expectDocCommentIncludes, + // expectError, + // expectNever, + expectNotAssignable, + // expectNotDeprecated, + // expectNotType, + // expectType, + // printType, +} from 'tsd'; + + +// Scenario: Empty OK response +const emptyOkResponse = {}; +expectAssignable(emptyOkResponse); + + +// Scenario: Response with just status +const notFoundResponse = { + status: 404, +}; +expectAssignable(notFoundResponse); + +// Scenario: JSON body? +// expectAssignable({ +// body: {}, // TODO +// contentType: 'application/json', +// }); + +expectAssignable({ + body: undefined, +}); + +// expectAssignable({ +// body: null, +// }); + +expectAssignable({ + cookies: {}, // Empty cookies object +}); + +expectAssignable({ + headers: {}, // Empty headers object +}); + +// Scenario: Response with "everything" +const fullResponse = { + applyFilters: true, + body: 'Hello, world!', + contentType: 'text/plain', + cookies: { + simpleCookie: 'string', + complexCookie: { + value: 'string', + path: 'string', + domain: 'string', + comment: 'string', + maxAge: 123, + secure: true, + httpOnly: true, + sameSite: 'string', + }, + }, + headers: { + 'Cache-Control': 'http1', + 'content-encoding': 'http2', + Etag: 123, + 'X-My-Header': 'my-value', + }, + postProcess: true, + redirect: '/some/other/url', + status: 200, +}; +expectAssignable(fullResponse); + +// Check illegal values +expectNotAssignable({ + applyFilters: 'notBoolean', +}); + +expectNotAssignable({ + contentType: true, // Not string! +}); + +expectNotAssignable({ + cookies: true, // Not Record! +}); + +expectNotAssignable({ + cookies: { + 'string': 123, // Not string|ComplexCookie ! + }, +}); + +expectNotAssignable({ + postProcess: 'notBoolean', +}); + +expectNotAssignable({ + redirect: true, // Not string! +}); + +expectNotAssignable({ + status: '200', // Not number! +}); \ No newline at end of file From 5af1edd89772d4622ef3f5ee9efd0e8f368344d2 Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Wed, 9 Oct 2024 11:12:47 +0200 Subject: [PATCH 02/19] StrictMergeInterfaces --- modules/lib/core/index.d.ts | 95 +++++++++++++++++------------ modules/lib/test/Request.test-d.ts | 26 +++++--- modules/lib/test/Response.test-d.ts | 38 ++++++++++-- 3 files changed, 106 insertions(+), 53 deletions(-) diff --git a/modules/lib/core/index.d.ts b/modules/lib/core/index.d.ts index 64fea22084e..9dea48ea0ca 100644 --- a/modules/lib/core/index.d.ts +++ b/modules/lib/core/index.d.ts @@ -143,13 +143,13 @@ export type XpRequire = (path: Key extends keyof XpLibraries ? XpLibraries[Key] : unknown; export interface DefaultCookies { - [key: string]: string|undefined + [key: string]: string | undefined enonic_xp_tour?: string JSESSIONID?: string } export interface DefaultRequestHeaders { - [headerName: string]: string|undefined + [headerName: string]: string | undefined Accept?: string 'Accept-Charset'?: string 'Accept-Encoding'?: string @@ -183,37 +183,37 @@ export type LowercaseKeys = { [K in keyof T as Lowercase]: T[K] }; -export interface DefaultOptionalRequestProperties { - [index: string]: unknown|undefined; // Custom properties are allowed +export type LiteralUnion = T | (U & Record); +// type LooseAutocomplete = T | Omit; +type StrictMergeInterfaces = B extends Record + ? Pick> & B + : A; + +export type Request< + T extends Record | undefined = undefined +> = StrictMergeInterfaces<{ body?: string - contextPath?: string + branch: LiteralUnion<'draft' | 'master'> contentType?: string + contextPath?: string cookies?: DefaultCookies followRedirects?: boolean headers?: DefaultRequestHeaders + host: string + method: LiteralUnion<'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'PATCH' | 'TRACE' | 'CONNECT'> + mode: LiteralUnion<'edit' | 'inline' | 'live' | 'preview' | 'admin'> params?: Record + path: string pathParams?: Record + port: number // |string // TODO Where have I seen this as string? rawPath?: string - repositoryId?: string remoteAddress?: string + repositoryId?: string + scheme: LiteralUnion<'http' | 'https'> + url: string validTicket?: boolean webSocket?: boolean -} - -type LiteralUnion = T | (U & Record); - -export type Request< - T extends Record = DefaultOptionalRequestProperties -> = { - branch: LiteralUnion<'draft'|'master'> - host: string - method: LiteralUnion<'GET'|'POST'|'PUT'|'DELETE'|'HEAD'|'OPTIONS'|'PATCH'|'TRACE'|'CONNECT'> - mode: LiteralUnion<'edit'|'inline'|'live'|'preview'|'admin'> - path: string - port: number // |string // TODO Where have I seen this as string? - scheme: LiteralUnion<'http'|'https'> - url: string -} & T; +},T>; export interface ComplexCookie { value: string @@ -226,36 +226,51 @@ export interface ComplexCookie { sameSite?: string } -export interface DefaultResponseHeaders extends Record { +export interface DefaultResponseHeaders extends Record { 'Cache-Control'?: string 'Content-Encoding'?: string 'Content-Type'?: string 'Content-Security-Policy'?: string 'Date'?: string - Etag?: string|number + Etag?: string | number Location?: string } export interface PageContributions { - headBegin?: string[] - headEnd?: string[] - bodyBegin?: string[] - bodyEnd?: string[] + headBegin?: string | string[] + headEnd?: string | string[] + bodyBegin?: string | string[] + bodyEnd?: string | string[] } export type Response< - T extends Record = { - applyFilters?: boolean - body?: string|ByteSource - contentType?: string - cookies?: Record - headers?: DefaultResponseHeaders - pageContributions?: PageContributions - postProcess?: boolean - redirect?: string - status?: number - } -> = T; + T extends Record | undefined = undefined +> = StrictMergeInterfaces<{ + applyFilters?: boolean + body?: string | ByteSource + contentType?: LiteralUnion<'text/html' | 'application/json'> + cookies?: Record + headers?: DefaultResponseHeaders + pageContributions?: PageContributions + postProcess?: boolean + redirect?: string + status?: number +}, T>; + +export type RequestHandler = (request: Request) => Response; + +export interface ControllerModule { + all: RequestHandler + // connect: RequestHandler + // delete: RequestHandler + get: RequestHandler + // head: RequestHandler + options: RequestHandler + // patch: RequestHandler + post: RequestHandler + // put: RequestHandler + // trace: RequestHandler +} export type UserKey = `user:${string}:${string}`; export type GroupKey = `group:${string}:${string}`; diff --git a/modules/lib/test/Request.test-d.ts b/modules/lib/test/Request.test-d.ts index bd076d306d2..93ebb335f50 100644 --- a/modules/lib/test/Request.test-d.ts +++ b/modules/lib/test/Request.test-d.ts @@ -4,13 +4,13 @@ import { expectAssignable, // expectDeprecated, // expectDocCommentIncludes, - // expectError, + expectError, // expectNever, expectNotAssignable, // expectNotDeprecated, // expectNotType, // expectType, - // printType, + printType, } from 'tsd'; @@ -700,10 +700,16 @@ expectAssignable({ scheme: 'string but not http|https', }); -expectNotAssignable({ +const branchNotString = { ...requiredProperties, - branch: 123, // not string -}); + branch: 123, +}; +// printType(branchNotString); + +expectNotAssignable(branchNotString); + +// Allowed to change type of branch to number +expectAssignable>(branchNotString); expectNotAssignable({ ...requiredProperties, @@ -740,8 +746,14 @@ expectNotAssignable({ url: 123, // not string }); -expectAssignable({ +expectNotAssignable({ ...requiredProperties, - custom: 'whatever', // Custom property allowed + custom: 'whatever', // Untyped custom property NOT allowed }); +expectAssignable>({ + ...requiredProperties, + custom: 'whatever', // Typed custom property allowed +}); diff --git a/modules/lib/test/Response.test-d.ts b/modules/lib/test/Response.test-d.ts index 9ea5c9f5699..2ecaa63c837 100644 --- a/modules/lib/test/Response.test-d.ts +++ b/modules/lib/test/Response.test-d.ts @@ -1,4 +1,7 @@ -import type {Response} from '../core/index'; +import type { + LiteralUnion, + Response, +} from '../core/index'; import { expectAssignable, @@ -7,6 +10,7 @@ import { // expectError, // expectNever, expectNotAssignable, + printType, // expectNotDeprecated, // expectNotType, // expectType, @@ -25,11 +29,33 @@ const notFoundResponse = { }; expectAssignable(notFoundResponse); -// Scenario: JSON body? -// expectAssignable({ -// body: {}, // TODO -// contentType: 'application/json', -// }); +// Scenario: JSON body +interface MyObject { + key: string +} +const object = { + key: 'value', +}; + +type JsonResponse = Response<{ + body: Body, + contentType: LiteralUnion<'application/json'> +}>; + +const jsonResponse = { + body: object, + // Changing contentType is allowed since it's a LiteralUnion + contentType: 'application/json;charset=utf-8', +}; + +// printType(jsonResponse); + +// Untyped body object not allowed +expectNotAssignable(jsonResponse); + +// Typed body object allowed +expectAssignable>(jsonResponse); + expectAssignable({ body: undefined, From 61aebaf8f3d666ad7281790ce54fd8f8f4388bf7 Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Wed, 9 Oct 2024 12:58:29 +0200 Subject: [PATCH 03/19] Minor improvements --- modules/lib/core/index.d.ts | 26 ++-- modules/lib/test/Controller.test-d.ts | 190 ++++++++++++++++++++++++++ modules/lib/test/Response.test-d.ts | 17 +++ modules/lib/test/tsconfig.json | 5 + 4 files changed, 225 insertions(+), 13 deletions(-) create mode 100644 modules/lib/test/Controller.test-d.ts create mode 100644 modules/lib/test/tsconfig.json diff --git a/modules/lib/core/index.d.ts b/modules/lib/core/index.d.ts index 9dea48ea0ca..af754040559 100644 --- a/modules/lib/core/index.d.ts +++ b/modules/lib/core/index.d.ts @@ -142,7 +142,7 @@ export interface ScriptValue { export type XpRequire = (path: Key) => Key extends keyof XpLibraries ? XpLibraries[Key] : unknown; -export interface DefaultCookies { +export interface DefaultRequestCookies { [key: string]: string | undefined enonic_xp_tour?: string JSESSIONID?: string @@ -185,7 +185,7 @@ export type LowercaseKeys = { export type LiteralUnion = T | (U & Record); // type LooseAutocomplete = T | Omit; -type StrictMergeInterfaces = B extends Record +export type StrictMergeInterfaces = B extends Record ? Pick> & B : A; @@ -196,7 +196,7 @@ export type Request< branch: LiteralUnion<'draft' | 'master'> contentType?: string contextPath?: string - cookies?: DefaultCookies + cookies?: DefaultRequestCookies followRedirects?: boolean headers?: DefaultRequestHeaders host: string @@ -260,16 +260,16 @@ export type Response< export type RequestHandler = (request: Request) => Response; export interface ControllerModule { - all: RequestHandler - // connect: RequestHandler - // delete: RequestHandler - get: RequestHandler - // head: RequestHandler - options: RequestHandler - // patch: RequestHandler - post: RequestHandler - // put: RequestHandler - // trace: RequestHandler + all?: RequestHandler + // connect?: RequestHandler + // delete?: RequestHandler + get?: RequestHandler + // head:? RequestHandler + options?: RequestHandler + // patch?: RequestHandler + post?: RequestHandler + // put?: RequestHandler + // trace?: RequestHandler } export type UserKey = `user:${string}:${string}`; diff --git a/modules/lib/test/Controller.test-d.ts b/modules/lib/test/Controller.test-d.ts new file mode 100644 index 00000000000..4a87005a33f --- /dev/null +++ b/modules/lib/test/Controller.test-d.ts @@ -0,0 +1,190 @@ +import type { + ControllerModule, + DefaultRequestCookies, + DefaultRequestHeaders, + Request, + Response, +} from '../core/index'; +import { + expectAssignable, + // expectDeprecated, + // expectDocCommentIncludes, + expectError, + // expectNever, + expectNotAssignable, + // expectNotDeprecated, + // expectNotType, + // expectType, + // printType, +} from 'tsd'; + +const log = { + info: (message?: string, ...optionalParams: string[]) => { /* no-op */ }, +}; + +const myControllerModule = { + all: (request: Request) => { + log.info('all request:%s', JSON.stringify(request, null, 4)); + return { + status: 200, + }; + }, + get: (request: Request) => { + log.info('get request:%s', JSON.stringify(request, null, 4)); + return { + status: 200, + }; + }, + options: (request: Request) => { + log.info('options request:%s', JSON.stringify(request, null, 4)); + return { + status: 200, + }; + }, + post: (request: Request) => { + log.info('post request:%s', JSON.stringify(request, null, 4)); + return { + status: 200, + }; + }, +}; + +expectAssignable(myControllerModule); + +type PageRequest = Omit + rawPath: string + remoteAddress: string + repositoryId: string + webSocket: boolean +}>, + // Omit/Disallow some optional properties + 'contextPath' | 'validTicket' +>; +type PageRequestHandler = (request: PageRequest) => Response; + +interface PageControllerModule { + all?: PageRequestHandler + // connect?: PageRequestHandler + // delete?: PageRequestHandler + get?: PageRequestHandler + // head:? PageRequestHandler + options?: PageRequestHandler + // patch?: PageRequestHandler + post?: PageRequestHandler + // put?: PageRequestHandler + // trace?: PageRequestHandler +} + +const pageRequest /* : PageRequest */ = { + method: 'GET' as PageRequest['method'], // Avoid flattening down to string + scheme: 'http' as PageRequest['scheme'], // Avoid flattening down to string + mode: 'preview' as PageRequest['mode'], // Avoid flattening down to string + branch: 'draft' as PageRequest['branch'], // Avoid flattening down to string + host: 'localhost', + port: 8080, + path: '/admin/site/preview/my-project/draft/my-site', + rawPath: '/admin/site/preview/my-project/draft/my-site', + url: 'http://localhost:8080/admin/site/preview/my-project/draft/my-site', + remoteAddress: '127.0.0.1', + webSocket: false, + repositoryId: 'com.enonic.cms.my-project', + params: {}, + headers: { + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', + Connection: 'keep-alive', + Cookie: 'app.browse.RecentItemsList=portal%3Asite; JSESSIONID=19g9dxfzwnyqo1i6ufom5ksuhg0', + Host: 'localhost:8080', + Referer: 'http://localhost:8080/admin/tool/com.enonic.app.contentstudio/main', + 'sec-ch-ua': 'Chromium;v=128, Not;A=Brand;v=24, Google Chrome;v=128', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': 'macOS', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'same-origin', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + cookies: { + 'app.browse.RecentItemsList': 'portal%3Asite', + JSESSIONID: '19g9dxfzwnyqo1i6ufom5ksuhg0', + }, +}; + +expectAssignable(pageRequest); + +expectNotAssignable({ + ...pageRequest, + contextPath: '/contextPath', +}); + +expectNotAssignable({ + ...pageRequest, + validTicket: true, +}); + +expectNotAssignable({ + ...pageRequest, + branch: 'stringButNotMatchingLiterals', +}); + +expectNotAssignable({ + ...pageRequest, + method: 'stringButNotMatchingLiterals', +}); + +expectNotAssignable({ + ...pageRequest, + mode: 'stringButNotMatchingLiterals', +}); + +expectNotAssignable({ + ...pageRequest, + scheme: 'stringButNotMatchingLiterals', +}); + +expectNotAssignable({ + ...pageRequest, + cookies: undefined, +}); + +expectNotAssignable({ + ...pageRequest, + headers: undefined, +}); + +expectNotAssignable({ + ...pageRequest, + params: undefined, +}); + +expectNotAssignable({ + ...pageRequest, + rawPath: undefined, +}); + +expectNotAssignable({ + ...pageRequest, + remoteAddress: undefined, +}); + +expectNotAssignable({ + ...pageRequest, + repositoryId: undefined, +}); + +expectNotAssignable({ + ...pageRequest, + webSocket: undefined, +}); \ No newline at end of file diff --git a/modules/lib/test/Response.test-d.ts b/modules/lib/test/Response.test-d.ts index 2ecaa63c837..f56d46b606b 100644 --- a/modules/lib/test/Response.test-d.ts +++ b/modules/lib/test/Response.test-d.ts @@ -29,7 +29,24 @@ const notFoundResponse = { }; expectAssignable(notFoundResponse); +// ──────────────────────────────────────────────────────────────────────────── +// Scenario: Strictify Response down to a specific Response +// ──────────────────────────────────────────────────────────────────────────── +type NotFoundResponse = Response<{ + status: 404 +}>; + +expectAssignable({ + status: 404 as const, +}); + +expectNotAssignable({ + status: 200, +}); + +// ──────────────────────────────────────────────────────────────────────────── // Scenario: JSON body +// ──────────────────────────────────────────────────────────────────────────── interface MyObject { key: string } diff --git a/modules/lib/test/tsconfig.json b/modules/lib/test/tsconfig.json new file mode 100644 index 00000000000..e07cce9ad1e --- /dev/null +++ b/modules/lib/test/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "lib": ["DOM"], + } +} \ No newline at end of file From 893bdbb0e7b378294177d407d56e29db7252521b Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Thu, 10 Oct 2024 16:23:43 +0200 Subject: [PATCH 04/19] Getting close --- modules/lib/core/index.d.ts | 247 ++++++++++++++++--- modules/lib/test/Controller.test-d.ts | 13 +- modules/lib/test/HttpFilter.test-d.ts | 106 ++++++++ modules/lib/test/Request.test-d.ts | 72 +++++- modules/lib/test/RequestImplementation.ts | 133 ++++++++++ modules/lib/test/Response.test-d.ts | 15 +- modules/lib/test/ResponseProcessor.test-d.ts | 49 ++++ 7 files changed, 576 insertions(+), 59 deletions(-) create mode 100644 modules/lib/test/HttpFilter.test-d.ts create mode 100644 modules/lib/test/RequestImplementation.ts create mode 100644 modules/lib/test/ResponseProcessor.test-d.ts diff --git a/modules/lib/core/index.d.ts b/modules/lib/core/index.d.ts index af754040559..0785ebb9ce1 100644 --- a/modules/lib/core/index.d.ts +++ b/modules/lib/core/index.d.ts @@ -142,12 +142,96 @@ export interface ScriptValue { export type XpRequire = (path: Key) => Key extends keyof XpLibraries ? XpLibraries[Key] : unknown; +// HTTP State Management Mechanism (February 1997) +// https://www.ietf.org/rfc/rfc2109.txt + +// HTTP State Management Mechanism (April 2011) +// This document defines the HTTP Cookie and Set-Cookie header fields. +// https://datatracker.ietf.org/doc/html/rfc6265 + +// Same-site Cookies (April 6, 2016) +// Expires: October 8, 2016 +// https://datatracker.ietf.org/doc/html/draft-west-first-party-cookies-07 + +// Same-Site Cookies (June 20, 2016) +// Expires: December 22, 2016 +// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-same-site-00 + +// Cookies: HTTP State Management Mechanism (10 October 2024) +// Expires: 13 April 2025 +// https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html +export interface ComplexCookie { + /** + * The value of the cookie (optional). + * + * @type string + */ + value: string + /** + * A comment (rfc2109) to document the cookie (optional). + * + * @type string + */ + comment?: string + /** + * The expiration date and time for the cookie (optional). + * + * @type string + */ + expires?: Date; + /** + * The domain name for which the cookie is set. + * + * @type string + */ + domain?: string + /** + * Indicates whether the cookie should not be accessible via JavaScript (optional). + * + * @type string + */ + httpOnly?: boolean + /** + * The maximum age of the cookie in seconds (optional). + * + * @type string + */ + maxAge?: number + /** + * The path on the server where the cookie should be available. + * + * @type string + */ + path?: string + /** + * Specifies the SameSite attribute (draft RFC) for the cookie (optional). + * + * @type string + */ + sameSite?: LiteralUnion<'lax' | 'strict' | 'none' >; + /** + * Indicates whether the cookie should only be sent over HTTPS (optional). + * + * @type string + */ + secure?: boolean +} + +export type RequestCookies = Record; +export type ResponseCookies = Record; + export interface DefaultRequestCookies { [key: string]: string | undefined enonic_xp_tour?: string JSESSIONID?: string } +// In com.enonic.xp.web.WebRequest the type is Map +// Undefined is added for convenience, so we can have optional default values for autocompletion. +export type RequestHeaders = Record; + +export type ResponseHeaders = Record; + export interface DefaultRequestHeaders { [headerName: string]: string | undefined Accept?: string @@ -179,6 +263,8 @@ export interface DefaultRequestHeaders { 'X-Forwarded-Server'?: string } +export type RequestParams = Record; + export type LowercaseKeys = { [K in keyof T as Lowercase]: T[K] }; @@ -189,41 +275,61 @@ export type StrictMergeInterfaces = B extends Record ? Pick> & B : A; -export type Request< - T extends Record | undefined = undefined -> = StrictMergeInterfaces<{ - body?: string - branch: LiteralUnion<'draft' | 'master'> - contentType?: string - contextPath?: string - cookies?: DefaultRequestCookies - followRedirects?: boolean - headers?: DefaultRequestHeaders +// When the header is not found, the function returns null. +export type RequestGetHeaderFunction = (headerName: string) => string | null; + +// This is the one sent from Java to JavaScript via PortalRequestMapper. +export interface Request { + // Required + cookies: DefaultRequestCookies + getHeader: RequestGetHeaderFunction + headers: DefaultRequestHeaders host: string - method: LiteralUnion<'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'PATCH' | 'TRACE' | 'CONNECT'> + method: LiteralUnion<'GET' | 'POST' |' HEAD' | 'OPTIONS' |' PUT' | 'DELETE' |' TRACE' | 'CONNECT' |' PATCH' | 'PROPFIND' |' PROPPATCH' | 'MKCOL' |' COPY' | 'MOVE' |' LOCK' | 'UNLOCK'> mode: LiteralUnion<'edit' | 'inline' | 'live' | 'preview' | 'admin'> - params?: Record + params: RequestParams path: string - pathParams?: Record - port: number // |string // TODO Where have I seen this as string? - rawPath?: string - remoteAddress?: string - repositoryId?: string + port: number + rawPath: string + remoteAddress: string scheme: LiteralUnion<'http' | 'https'> url: string + webSocket: boolean + // Optional + + // Required when contentType is present, but that's not easy to enforce here. + // This can only be string, binary data is only available via portal.getMultipartForm + body?: string + + branch?: LiteralUnion<'draft' | 'master'> + contextPath?: string + contentType?: string + repositoryId?: string validTicket?: boolean - webSocket?: boolean -},T>; +} -export interface ComplexCookie { - value: string - path?: string - domain?: string - comment?: string - maxAge?: number - secure?: boolean - httpOnly?: boolean - sameSite?: string +export type RequestConstructorParams = Omit; + +// This is the one sent from JavaScript into Java via PortalRequestSerializer. +export interface RequestToBeSerializedToJava { + branch?: LiteralUnion<'draft' | 'master'> + + // TODO: Maybe it's possible to pass a stream in a HttpFilter. + body?: unknown[] | Record | boolean | number | string | null // | ByteSource + + contentType?: string + cookies?: DefaultRequestCookies // TODO Maybe ComplexCookie is supported here? + headers?: DefaultRequestHeaders + host?: string + method?: LiteralUnion<'GET' | 'POST' |' HEAD' | 'OPTIONS' |' PUT' | 'DELETE' |' TRACE' | 'CONNECT' |' PATCH' | 'PROPFIND' |' PROPPATCH' | 'MKCOL' |' COPY' | 'MOVE' |' LOCK' | 'UNLOCK'> + mode?: LiteralUnion<'edit' | 'inline' | 'live' | 'preview' | 'admin'> + params?: RequestParams + path?: string + port?: number + remoteAddress?: string + scheme?: LiteralUnion<'http' | 'https'> + url?: string + validTicket?: boolean } export interface DefaultResponseHeaders extends Record { @@ -236,18 +342,39 @@ export interface DefaultResponseHeaders extends Record | undefined = undefined -> = StrictMergeInterfaces<{ +// export type NotFunction = boolean | number | string | null | undefined; +// // bigint +// // symbol +// export type RecordWithoutFunctions = Record; +// export type ArrayWithoutFunctions = Array; + +// Unfortunately, collapses to any +// export type ResponseBody = ArrayWithoutFunctions | RecordWithoutFunctions | NotFunction | ByteSource; +export type ResponseBody = unknown[] | Record | boolean | number | string | null | ByteSource; + +// This is the one sent from JavaScript into Java via PortalResponseSerializer. +export interface Response { applyFilters?: boolean - body?: string | ByteSource + body?: ResponseBody contentType?: LiteralUnion<'text/html' | 'application/json'> cookies?: Record headers?: DefaultResponseHeaders @@ -255,21 +382,59 @@ export type Response< postProcess?: boolean redirect?: string status?: number -}, T>; +} + +// This is the one sent from JavaScript into Java via PortalResponseSerializer. +// export type Response< +// T extends Record | undefined = undefined +// > = StrictMergeInterfaces; -export type RequestHandler = (request: Request) => Response; +// This is the one returned from Java to JavaScript via PortalResponseMapper. +export interface MappedResponse { + applyFilters: boolean + body?: ResponseBody + contentType: LiteralUnion<'text/html' | 'application/json'> + cookies: Record + headers: ResponseHeaders + pageContributions: MappedPageContributions + postProcess: boolean + status: number +} + +export type RequestHandler< + CustomRequest = Request, + CustomResponse = Response +> = (request: CustomRequest) => CustomResponse; export interface ControllerModule { all?: RequestHandler // connect?: RequestHandler - // delete?: RequestHandler + delete?: RequestHandler get?: RequestHandler - // head:? RequestHandler + head?: RequestHandler options?: RequestHandler // patch?: RequestHandler post?: RequestHandler - // put?: RequestHandler + put?: RequestHandler // trace?: RequestHandler + // TODO what about propfind, proppatch, mkcol, copy, move, lock and unlock? +} + +export interface HttpFilterControllerModule< + RequestFromJava = Request, // From java + RequestToNext extends RequestToBeSerializedToJava = Partial, // To java + ResponseFromNext extends Response = Response, // To java + ReturnedResponse extends Response = ResponseFromNext // To java +> { + filter: (request: RequestFromJava, next: RequestHandler) => ReturnedResponse +} + +export interface ResponseProcessorControllerModule< + RequestFromJava = Request, // From java + ResponseFromJava extends MappedResponse = MappedResponse, // From java + ReturnedResponse extends Response = Partial // To java +> { + responseProcessor: (request: RequestFromJava, response: ResponseFromJava) => ReturnedResponse } export type UserKey = `user:${string}:${string}`; diff --git a/modules/lib/test/Controller.test-d.ts b/modules/lib/test/Controller.test-d.ts index 4a87005a33f..b93cfd70aa2 100644 --- a/modules/lib/test/Controller.test-d.ts +++ b/modules/lib/test/Controller.test-d.ts @@ -4,6 +4,7 @@ import type { DefaultRequestHeaders, Request, Response, + StrictMergeInterfaces, } from '../core/index'; import { expectAssignable, @@ -51,19 +52,14 @@ const myControllerModule = { expectAssignable(myControllerModule); -type PageRequest = Omit - rawPath: string - remoteAddress: string repositoryId: string webSocket: boolean }>, @@ -115,6 +111,9 @@ const pageRequest /* : PageRequest */ = { 'Sec-Fetch-Site': 'same-origin', 'Upgrade-Insecure-Requests': '1', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + } as Record, + getHeader: (header: string): string => { + return pageRequest.headers[header]; }, cookies: { 'app.browse.RecentItemsList': 'portal%3Asite', diff --git a/modules/lib/test/HttpFilter.test-d.ts b/modules/lib/test/HttpFilter.test-d.ts new file mode 100644 index 00000000000..1f721765012 --- /dev/null +++ b/modules/lib/test/HttpFilter.test-d.ts @@ -0,0 +1,106 @@ +import type { + HttpFilterControllerModule, + Request, + RequestHandler, + Response, + StrictMergeInterfaces, +} from '../core/index'; + +import { + expectAssignable, + // expectDeprecated, + // expectDocCommentIncludes, + // expectError, + // expectNever, + // expectNotAssignable, + // printType, + // expectNotDeprecated, + // expectNotType, + // expectType, + // printType, +} from 'tsd'; + +const log = { + info: (message?: string, ...optionalParams: unknown[]) => { /* no-op */ }, +}; + +// Testing examples from HTTP Filters +// https://developer.enonic.com/docs/xp/stable/framework/filters + + +// ──────────────────────────────────────────────────────────────────────────── +// Minimal filter timing the subsequent request +// ──────────────────────────────────────────────────────────────────────────── +const httpFilterControllerModule1 = { + filter: function (req: Request, next: RequestHandler) { + const before = new Date().getTime(); + const response = next(req); // next(req) hands over the request to the engine pipeline and returns the response + const after = new Date().getTime(); + log.info('%sms', after - before); + return response; + }, +}; +expectAssignable(httpFilterControllerModule1); + +// ──────────────────────────────────────────────────────────────────────────── +// Filter manipulating the request and the response +// ──────────────────────────────────────────────────────────────────────────── +type RequestFromJava = StrictMergeInterfaces; + +type RequestToNext = RequestFromJava; + +type ResponseFromNext = StrictMergeInterfaces; + +type ReturnedResponse = ResponseFromNext; + +const httpFilterControllerModule2 = { + filter: function (req: RequestFromJava, next: RequestHandler) { + // ERROR: I don't think one can add custom properties to the request, only headers... + req.requestLogging = true; // Manipulate request + log.info('Request:%s', JSON.stringify(req, null, 2)); + const response = next(req); // Continue request pipeline + response.responseLogging = true; // Manipulate response + log.info('Response:%s', JSON.stringify(response, null, 2)); + return response as unknown as ReturnedResponse; + }, +}; +expectAssignable>(httpFilterControllerModule2); + +// ──────────────────────────────────────────────────────────────────────────── +// Filter intercepting the request +// ──────────────────────────────────────────────────────────────────────────── +const httpFilterControllerModule3 = { + filter: function (req: Request, next: RequestHandler) { + if (req.getHeader('X-Auth-Token') !== 'letMeIn') { + // intercept request pipeline + return { + status: 403, + } as Response; + } + + // req.headers['Authenticated'] = true; // ERROR: Wrong type in example + req.headers['Authenticated'] = 'true'; + return next(req); + }, +}; +expectAssignable(httpFilterControllerModule3); + +// ──────────────────────────────────────────────────────────────────────────── +// Filter changing request params +// ──────────────────────────────────────────────────────────────────────────── +const httpFilterControllerModule4 = { + filter: function (req: Request, next: RequestHandler) { + req.params = { + param1: 'val', // if param1 was not in the original request it will be added, otherwise the original value will be replaced + // param2: null, // remove param2 from the original request // ERROR: Wrong type in example + // param2: undefined, // remove param2 from the original request // ERROR: This is not allowed either + param3: [], // another way to remove a parameter + }; + return next(req); + }, +}; +expectAssignable(httpFilterControllerModule4); \ No newline at end of file diff --git a/modules/lib/test/Request.test-d.ts b/modules/lib/test/Request.test-d.ts index 93ebb335f50..911a35fa4a1 100644 --- a/modules/lib/test/Request.test-d.ts +++ b/modules/lib/test/Request.test-d.ts @@ -1,4 +1,8 @@ -import type {Request} from '../core/index'; +import type { + Request, + StrictMergeInterfaces, +} from '../core/index'; +import {RequestImplementation} from './RequestImplementation'; import { expectAssignable, @@ -14,9 +18,11 @@ import { } from 'tsd'; + + // Scenario: When implementing a idprovider login function the Request may have a validTicket property // Scenario: When implementing a idprovider logout function the Request may have a validTicket property -const idproviderLogoutRequest = { +const idproviderLogoutRequest: Request = new RequestImplementation({ branch: 'draft', contextPath: '/_/idprovider/system', cookies: { @@ -26,7 +32,7 @@ const idproviderLogoutRequest = { headers: { Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', // ... - }, + } as Record, host: 'localhost', method: 'GET', mode: 'live', @@ -41,7 +47,7 @@ const idproviderLogoutRequest = { scheme: 'http', validTicket: true, webSocket: false, -}; +}).toObject(); // printType(idproviderLogoutRequest); @@ -81,6 +87,9 @@ const webappControllerGetRequest = { 'Sec-Fetch-User': '?1', 'Upgrade-Insecure-Requests': '1', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + } as Record, + getHeader: function (header: string): string { + return this.headers[header]; }, 'cookies': { 'app.browse.RecentItemsList': 'com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', @@ -120,6 +129,9 @@ const pageRequest = { 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + } as Record, + getHeader: function (header: string): string { + return this.headers[header]; }, cookies: { 'app.browse.RecentItemsList': 'base%3Afolder%7Cportal%3Asite', @@ -160,6 +172,9 @@ const layoutControllerGetRequest = { 'Sec-Fetch-Site': 'same-origin', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', 'X-Requested-With': 'XMLHttpRequest', + } as Record, + getHeader: function (header: string): string { + return this.headers[header]; }, 'cookies': { 'app.browse.RecentItemsList': 'base%3Afolder%7Ccom.example.myproject%3Aperson%7Cportal%3Asite', @@ -199,6 +214,9 @@ const layoutFragmentRequest ={ 'Sec-Fetch-Site': 'same-origin', 'Upgrade-Insecure-Requests': '1', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + } as Record, + getHeader: function (header: string): string { + return this.headers[header]; }, 'cookies': { 'app.browse.RecentItemsList': 'base%3Afolder%7Ccom.example.myproject%3Aperson%7Cportal%3Asite', @@ -240,6 +258,9 @@ const partControllerGetRequest = { 'Sec-Fetch-Site': 'same-origin', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', 'X-Requested-With': 'XMLHttpRequest', + } as Record, + getHeader: function (header: string): string { + return this.headers[header]; }, 'cookies': { 'app.browse.RecentItemsList': 'base%3Afolder%7Ccom.example.myproject%3Aperson%7Cportal%3Asite', @@ -279,6 +300,9 @@ const partFragmentRequest = { 'Sec-Fetch-Site': 'same-origin', 'Upgrade-Insecure-Requests': '1', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + } as Record, + getHeader: function (header: string): string { + return this.headers[header]; }, 'cookies': { 'app.browse.RecentItemsList': 'base%3Afolder%7Ccom.example.myproject%3Aperson%7Cportal%3Asite', @@ -325,6 +349,9 @@ const error = { 'Sec-Fetch-User': '?1', 'Upgrade-Insecure-Requests': '1', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + } as Record, + getHeader: function (header: string): string { + return this.headers[header]; }, cookies: { 'app.browse.RecentItemsList': 'base%3Afolder%7Cportal%3Asite', @@ -367,6 +394,9 @@ const serviceRequest ={ 'Sec-Fetch-Mode': 'no-cors', 'Sec-Fetch-Site': 'same-origin', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + } as Record, + getHeader: function (header: string): string { + return this.headers[header]; }, cookies: { 'app.browse.RecentItemsList': 'base%3Afolder%7Cportal%3Asite', @@ -406,6 +436,9 @@ const siteMappingRequest = { 'Sec-Fetch-Mode': 'no-cors', 'Sec-Fetch-Site': 'same-origin', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + } as Record, + getHeader: function (header: string): string { + return this.headers[header]; }, cookies: { 'app.browse.RecentItemsList': 'base%3Afolder%7Cportal%3Asite', @@ -452,6 +485,9 @@ const customSelectorRequest = { 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + } as Record, + getHeader: function (header: string): string { + return this.headers[header]; }, cookies: { 'app.browse.RecentItemsList': 'com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', @@ -493,6 +529,9 @@ const responseProcessorRequest = { 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + } as Record, + getHeader: function (header: string): string { + return this.headers[header]; }, 'cookies': { 'app.browse.RecentItemsList': 'base%3Afolder%7Ccom.example.myproject%3Aperson%7Cportal%3Asite', @@ -534,6 +573,9 @@ const adminToolRequest = { 'Sec-Fetch-User': '?1', 'Upgrade-Insecure-Requests': '1', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + } as Record, + getHeader: function (header: string): string { + return this.headers[header]; }, cookies: { 'app.browse.RecentItemsList': 'com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', @@ -577,6 +619,9 @@ const contextpanelWidgetRequest = { 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + } as Record, + getHeader: function (header: string): string { + return this.headers[header]; }, cookies: { 'app.browse.RecentItemsList': 'com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', @@ -616,6 +661,9 @@ const dashboardWidgetRequest = { 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + } as Record, + getHeader: function (header: string): string { + return this.headers[header]; }, cookies: { 'app.browse.RecentItemsList': 'com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', @@ -655,6 +703,9 @@ const menuitemWidgetRequest = { 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + } as Record, + getHeader: function (header: string): string { + return this.headers[header]; }, cookies: { 'app.browse.RecentItemsList': 'com.example.myproject%3Aperson%7Cbase%3Afolder%7Cportal%3Asite', @@ -666,13 +717,22 @@ expectAssignable(menuitemWidgetRequest); // Scenario: Illegal values for properties const requiredProperties = { branch: 'draft', + cookies: {}, + getHeader: (header: string): string => { + return requiredProperties.headers[header]; + }, + headers: {} as Record, host: 'localhost', method: 'GET', mode: 'admin', + params: {}, + rawPath: '/whatever', + remoteAddress: '127.0.0.1', path: '/whatever', port: 8080, scheme: 'http', url: 'http://localhost:8080/whatever', + webSocket: false, }; expectAssignable(requiredProperties); @@ -709,7 +769,7 @@ const branchNotString = { expectNotAssignable(branchNotString); // Allowed to change type of branch to number -expectAssignable>(branchNotString); +expectAssignable>(branchNotString); expectNotAssignable({ ...requiredProperties, @@ -751,7 +811,7 @@ expectNotAssignable({ custom: 'whatever', // Untyped custom property NOT allowed }); -expectAssignable>({ ...requiredProperties, diff --git a/modules/lib/test/RequestImplementation.ts b/modules/lib/test/RequestImplementation.ts new file mode 100644 index 00000000000..5b385ff727a --- /dev/null +++ b/modules/lib/test/RequestImplementation.ts @@ -0,0 +1,133 @@ +import type { + Request, + RequestConstructorParams, + RequestCookies, + RequestHeaders, +} from '../core/index'; + +export class RequestImplementation implements Request { + cookies: RequestCookies; + headers: RequestHeaders; + host: string; + method: string; + mode: string; + params: Record; + path: string; + port: number; + rawPath: string; + remoteAddress: string; + scheme: string; + url: string; + webSocket: boolean; + + branch?: string; + contextPath?: string; + contentType?: string; + repositoryId?: string; + validTicket?: boolean; + body?: string; + + constructor({ + // Required + cookies, + headers, + host, + method, + mode, + params, + path, + port, + rawPath, + remoteAddress, + scheme, + url, + webSocket, + // Optionals + branch, + contextPath, + contentType, + repositoryId, + validTicket, + // Special + body, + }: RequestConstructorParams) { + this.cookies = cookies; + this.headers = headers; + this.host = host; + this.method = method; + this.mode = mode; + this.params = params; + this.path = path; + this.port = port; + this.rawPath = rawPath; + this.remoteAddress = remoteAddress; + this.scheme = scheme; + this.url = url; + this.webSocket = webSocket; + + this.branch = branch; + this.contextPath = contextPath; + this.contentType = contentType; + this.repositoryId = repositoryId; + this.validTicket = validTicket; + + this.body = body; + } + + getHeader(header: string): string | null { + const value = this.headers[header]; + if (value === undefined) { + return null; + } + return value; + } + + toString(): string { + return JSON.stringify(this); + } + + toObject(): Request { + const obj: Request = { + cookies: this.cookies, + headers: this.headers, + getHeader: (header: string): string | null => { + const value = obj.headers[header]; + if (value === undefined) { + return null; + } + return value; + }, + host: this.host, + method: this.method, + mode: this.mode, + params: this.params, + path: this.path, + port: this.port, + rawPath: this.rawPath, + remoteAddress: this.remoteAddress, + scheme: this.scheme, + url: this.url, + webSocket: this.webSocket, + }; + if (this.branch) { + obj.branch = this.branch; + } + if (this.contextPath) { + obj.contextPath = this.contextPath; + } + if (this.contentType) { + obj.contentType = this.contentType; + if (!this.body) { + throw new Error('body is requred when contentType is set'); + } + obj.body = this.body; + } + if (this.repositoryId) { + obj.repositoryId = this.repositoryId; + } + if (this.validTicket) { + obj.validTicket = this.validTicket; + } + return obj; + } +} \ No newline at end of file diff --git a/modules/lib/test/Response.test-d.ts b/modules/lib/test/Response.test-d.ts index f56d46b606b..63bba6cc5a7 100644 --- a/modules/lib/test/Response.test-d.ts +++ b/modules/lib/test/Response.test-d.ts @@ -1,6 +1,7 @@ import type { LiteralUnion, Response, + StrictMergeInterfaces, } from '../core/index'; import { @@ -19,7 +20,7 @@ import { // Scenario: Empty OK response -const emptyOkResponse = {}; +const emptyOkResponse = {}; expectAssignable(emptyOkResponse); @@ -32,7 +33,7 @@ expectAssignable(notFoundResponse); // ──────────────────────────────────────────────────────────────────────────── // Scenario: Strictify Response down to a specific Response // ──────────────────────────────────────────────────────────────────────────── -type NotFoundResponse = Response<{ +type NotFoundResponse = StrictMergeInterfaces; @@ -54,7 +55,7 @@ const object = { key: 'value', }; -type JsonResponse = Response<{ +type JsonResponse = StrictMergeInterfaces }>; @@ -68,7 +69,7 @@ const jsonResponse = { // printType(jsonResponse); // Untyped body object not allowed -expectNotAssignable(jsonResponse); +// expectNotAssignable(jsonResponse); // TODO // Typed body object allowed expectAssignable>(jsonResponse); @@ -149,4 +150,8 @@ expectNotAssignable({ expectNotAssignable({ status: '200', // Not number! -}); \ No newline at end of file +}); + +// ──────────────────────────────────────────────────────────────────────────── +// Scenario: Response processor +// ──────────────────────────────────────────────────────────────────────────── \ No newline at end of file diff --git a/modules/lib/test/ResponseProcessor.test-d.ts b/modules/lib/test/ResponseProcessor.test-d.ts new file mode 100644 index 00000000000..34aaf6fee56 --- /dev/null +++ b/modules/lib/test/ResponseProcessor.test-d.ts @@ -0,0 +1,49 @@ +import type { + Request, + MappedResponse, + Response, + ResponseProcessorControllerModule, + // StrictMergeInterfaces, +} from '../core/index'; + +import { + expectAssignable, + // expectDeprecated, + // expectDocCommentIncludes, + // expectError, + // expectNever, + // expectNotAssignable, + // printType, + // expectNotDeprecated, + // expectNotType, + // expectType, + // printType, +} from 'tsd'; + +// const log = { +// info: (message?: string, ...optionalParams: unknown[]) => { /* no-op */ }, +// }; + +// Testing examples from HTTP Filters +// https://developer.enonic.com/docs/xp/stable/framework/processors + +// ──────────────────────────────────────────────────────────────────────────── +// dynamically adds a bodyEnd page contribution to the response +// ──────────────────────────────────────────────────────────────────────────── +const responseProcessorControllerModule1 = { + responseProcessor: (_req: Request, res: MappedResponse) => { + const trackingScript = ''; + + // Check if contribution field exists, if not create it + const bodyEnd = res.pageContributions.bodyEnd; + if (!bodyEnd) { + res.pageContributions.bodyEnd = []; + } + + // Add contribution + (res.pageContributions.bodyEnd as string[]).push(trackingScript); + + return res as Response; + }, +}; +expectAssignable(responseProcessorControllerModule1); From cc161970aa7088f201bb9d8d3771aa262a2373f5 Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Fri, 11 Oct 2024 12:56:42 +0200 Subject: [PATCH 05/19] Better use of StrictMergeInterfaces --- modules/lib/core/index.d.ts | 222 ++++++++----------- modules/lib/test/Controller.test-d.ts | 24 +- modules/lib/test/HttpFilter.test-d.ts | 33 ++- modules/lib/test/Request.test-d.ts | 22 +- modules/lib/test/Response.test-d.ts | 16 +- modules/lib/test/ResponseProcessor.test-d.ts | 1 - 6 files changed, 134 insertions(+), 184 deletions(-) diff --git a/modules/lib/core/index.d.ts b/modules/lib/core/index.d.ts index 0785ebb9ce1..0beeefd1e9d 100644 --- a/modules/lib/core/index.d.ts +++ b/modules/lib/core/index.d.ts @@ -142,24 +142,15 @@ export interface ScriptValue { export type XpRequire = (path: Key) => Key extends keyof XpLibraries ? XpLibraries[Key] : unknown; -// HTTP State Management Mechanism (February 1997) -// https://www.ietf.org/rfc/rfc2109.txt - -// HTTP State Management Mechanism (April 2011) -// This document defines the HTTP Cookie and Set-Cookie header fields. -// https://datatracker.ietf.org/doc/html/rfc6265 - -// Same-site Cookies (April 6, 2016) -// Expires: October 8, 2016 -// https://datatracker.ietf.org/doc/html/draft-west-first-party-cookies-07 +export type LiteralUnion = T | (U & Record); -// Same-Site Cookies (June 20, 2016) -// Expires: December 22, 2016 -// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-same-site-00 +export type StrictMergeInterfaces< + A, + B = Record +> = B extends Record + ? A + : Pick> & B; -// Cookies: HTTP State Management Mechanism (10 October 2024) -// Expires: 13 April 2025 -// https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html export interface ComplexCookie { /** * The value of the cookie (optional). @@ -208,7 +199,7 @@ export interface ComplexCookie { * * @type string */ - sameSite?: LiteralUnion<'lax' | 'strict' | 'none' >; + sameSite?: LiteralUnion<'lax' | 'strict' | 'none'>; /** * Indicates whether the cookie should only be sent over HTTPS (optional). * @@ -217,23 +208,25 @@ export interface ComplexCookie { secure?: boolean } +export type RequestBranch = 'draft' | 'master'; +export type RequestGetHeaderFunction = (headerName: string) => string | null; +export type RequestMethod = 'GET' | 'POST' | ' HEAD' | 'OPTIONS' | ' PUT' | 'DELETE' | ' TRACE' | 'CONNECT' | ' PATCH' | 'PROPFIND' | ' PROPPATCH' | 'MKCOL' | ' COPY' | 'MOVE' | ' LOCK' | 'UNLOCK'; +export type RequestMode = 'edit' | 'inline' | 'live' | 'preview' | 'admin'; +export type RequestParams = Record; +export type RequestScheme = 'http' | 'https'; + export type RequestCookies = Record; export type ResponseCookies = Record; -export interface DefaultRequestCookies { - [key: string]: string | undefined +export type RequestHeaders = Record; +export type ResponseHeaders = Record; + +export interface DefaultRequestCookies extends RequestCookies { enonic_xp_tour?: string JSESSIONID?: string } -// In com.enonic.xp.web.WebRequest the type is Map -// Undefined is added for convenience, so we can have optional default values for autocompletion. -export type RequestHeaders = Record; - -export type ResponseHeaders = Record; - -export interface DefaultRequestHeaders { - [headerName: string]: string | undefined +export interface DefaultRequestHeaders extends RequestHeaders { Accept?: string 'Accept-Charset'?: string 'Accept-Encoding'?: string @@ -247,7 +240,7 @@ export interface DefaultRequestHeaders { Language?: string Host?: string 'If-None-Match'?: string - 'Referer'?: string + Referer?: string 'sec-ch-ua'?: string 'sec-ch-ua-mobile'?: string 'sec-ch-ua-platform'?: string @@ -263,76 +256,49 @@ export interface DefaultRequestHeaders { 'X-Forwarded-Server'?: string } -export type RequestParams = Record; - -export type LowercaseKeys = { - [K in keyof T as Lowercase]: T[K] -}; - -export type LiteralUnion = T | (U & Record); -// type LooseAutocomplete = T | Omit; -export type StrictMergeInterfaces = B extends Record - ? Pick> & B - : A; - -// When the header is not found, the function returns null. -export type RequestGetHeaderFunction = (headerName: string) => string | null; - -// This is the one sent from Java to JavaScript via PortalRequestMapper. -export interface Request { - // Required - cookies: DefaultRequestCookies - getHeader: RequestGetHeaderFunction - headers: DefaultRequestHeaders +export interface RequestConstructorParams { + body?: string + branch?: LiteralUnion + contentType?: string + contextPath?: string + cookies: RequestCookies + headers: RequestHeaders host: string - method: LiteralUnion<'GET' | 'POST' |' HEAD' | 'OPTIONS' |' PUT' | 'DELETE' |' TRACE' | 'CONNECT' |' PATCH' | 'PROPFIND' |' PROPPATCH' | 'MKCOL' |' COPY' | 'MOVE' |' LOCK' | 'UNLOCK'> - mode: LiteralUnion<'edit' | 'inline' | 'live' | 'preview' | 'admin'> + method: LiteralUnion + mode: LiteralUnion params: RequestParams path: string port: number rawPath: string remoteAddress: string - scheme: LiteralUnion<'http' | 'https'> - url: string - webSocket: boolean - // Optional - - // Required when contentType is present, but that's not easy to enforce here. - // This can only be string, binary data is only available via portal.getMultipartForm - body?: string - - branch?: LiteralUnion<'draft' | 'master'> - contextPath?: string - contentType?: string repositoryId?: string + scheme: LiteralUnion + url: string validTicket?: boolean + webSocket: boolean } -export type RequestConstructorParams = Omit; +export interface RequestInterface extends RequestConstructorParams { + getHeader: RequestGetHeaderFunction +} -// This is the one sent from JavaScript into Java via PortalRequestSerializer. -export interface RequestToBeSerializedToJava { - branch?: LiteralUnion<'draft' | 'master'> +export interface DefaultRequest extends RequestInterface { + cookies: DefaultRequestCookies + headers: DefaultRequestHeaders +} - // TODO: Maybe it's possible to pass a stream in a HttpFilter. - body?: unknown[] | Record | boolean | number | string | null // | ByteSource +export type Request< + T extends Partial = Record +> = StrictMergeInterfaces; - contentType?: string - cookies?: DefaultRequestCookies // TODO Maybe ComplexCookie is supported here? - headers?: DefaultRequestHeaders - host?: string - method?: LiteralUnion<'GET' | 'POST' |' HEAD' | 'OPTIONS' |' PUT' | 'DELETE' |' TRACE' | 'CONNECT' |' PATCH' | 'PROPFIND' |' PROPPATCH' | 'MKCOL' |' COPY' | 'MOVE' |' LOCK' | 'UNLOCK'> - mode?: LiteralUnion<'edit' | 'inline' | 'live' | 'preview' | 'admin'> - params?: RequestParams - path?: string - port?: number - remoteAddress?: string - scheme?: LiteralUnion<'http' | 'https'> - url?: string - validTicket?: boolean -} +export type RequestToBeSerializedToJava = Omit< + Partial, + 'body' | 'contextPath' | 'rawPath' | 'repositoryId' | 'webSocket' +> & { + body?: unknown[] | Record | boolean | number | string | null; +}; -export interface DefaultResponseHeaders extends Record { +export interface DefaultResponseHeaders extends ResponseHeaders { 'Cache-Control'?: string 'Content-Encoding'?: string 'Content-Type'?: string @@ -346,7 +312,6 @@ export interface DefaultResponseHeaders extends Record; -// export type ArrayWithoutFunctions = Array; - -// Unfortunately, collapses to any -// export type ResponseBody = ArrayWithoutFunctions | RecordWithoutFunctions | NotFunction | ByteSource; export type ResponseBody = unknown[] | Record | boolean | number | string | null | ByteSource; -// This is the one sent from JavaScript into Java via PortalResponseSerializer. -export interface Response { - applyFilters?: boolean - body?: ResponseBody - contentType?: LiteralUnion<'text/html' | 'application/json'> - cookies?: Record - headers?: DefaultResponseHeaders - pageContributions?: PageContributions - postProcess?: boolean - redirect?: string - status?: number -} - -// This is the one sent from JavaScript into Java via PortalResponseSerializer. -// export type Response< -// T extends Record | undefined = undefined -// > = StrictMergeInterfaces; - -// This is the one returned from Java to JavaScript via PortalResponseMapper. export interface MappedResponse { applyFilters: boolean body?: ResponseBody - contentType: LiteralUnion<'text/html' | 'application/json'> - cookies: Record + contentType: string + cookies: ResponseCookies headers: ResponseHeaders - pageContributions: MappedPageContributions + pageContributions: PageContributions postProcess: boolean status: number } +export interface ResponseInterface extends Partial { + redirect?: string +} + +export interface DefaultResponse extends ResponseInterface { + contentType?: LiteralUnion<'text/html' | 'application/json'> + headers?: DefaultResponseHeaders +} + +export type Response< + T extends Partial = Record +> = StrictMergeInterfaces; + export type RequestHandler< - CustomRequest = Request, - CustomResponse = Response -> = (request: CustomRequest) => CustomResponse; + RequestFromJava extends RequestInterface = DefaultRequest, + ResponseToJava extends ResponseInterface = DefaultResponse +> = (request: RequestFromJava) => ResponseToJava; + +export type HttpFilterNext< + RequestToJava extends RequestToBeSerializedToJava = RequestToBeSerializedToJava, + ResponseToJava extends ResponseInterface = DefaultResponse +> = (request: RequestToJava) => ResponseToJava; export interface ControllerModule { all?: RequestHandler @@ -421,20 +370,25 @@ export interface ControllerModule { } export interface HttpFilterControllerModule< - RequestFromJava = Request, // From java - RequestToNext extends RequestToBeSerializedToJava = Partial, // To java - ResponseFromNext extends Response = Response, // To java - ReturnedResponse extends Response = ResponseFromNext // To java + RequestFromJava extends RequestInterface = DefaultRequest, + ResponseFromNext extends ResponseInterface = Response, + ResponseToJava extends ResponseInterface = ResponseFromNext > { - filter: (request: RequestFromJava, next: RequestHandler) => ReturnedResponse + filter: ( + request: RequestFromJava, + next: HttpFilterNext< + RequestToBeSerializedToJava, + ResponseFromNext + > + ) => ResponseToJava; } export interface ResponseProcessorControllerModule< - RequestFromJava = Request, // From java - ResponseFromJava extends MappedResponse = MappedResponse, // From java - ReturnedResponse extends Response = Partial // To java + RequestFromJava extends RequestInterface = DefaultRequest, + ResponseFromJava extends MappedResponse = MappedResponse, + ResponseToJava extends ResponseInterface = Partial > { - responseProcessor: (request: RequestFromJava, response: ResponseFromJava) => ReturnedResponse + responseProcessor: (request: RequestFromJava, response: ResponseFromJava) => ResponseToJava } export type UserKey = `user:${string}:${string}`; diff --git a/modules/lib/test/Controller.test-d.ts b/modules/lib/test/Controller.test-d.ts index b93cfd70aa2..cc41f72dbfe 100644 --- a/modules/lib/test/Controller.test-d.ts +++ b/modules/lib/test/Controller.test-d.ts @@ -1,10 +1,11 @@ import type { ControllerModule, - DefaultRequestCookies, - DefaultRequestHeaders, Request, + RequestBranch, + RequestMethod, + RequestMode, + RequestScheme, Response, - StrictMergeInterfaces, } from '../core/index'; import { expectAssignable, @@ -52,12 +53,12 @@ const myControllerModule = { expectAssignable(myControllerModule); -type PageRequest = Omit Response; interface PageControllerModule { all?: PageRequestHandler // connect?: PageRequestHandler - // delete?: PageRequestHandler + delete?: PageRequestHandler get?: PageRequestHandler - // head:? PageRequestHandler + head?: PageRequestHandler options?: PageRequestHandler // patch?: PageRequestHandler post?: PageRequestHandler - // put?: PageRequestHandler + put?: PageRequestHandler // trace?: PageRequestHandler + // TODO what about propfind, proppatch, mkcol, copy, move, lock and unlock? } const pageRequest /* : PageRequest */ = { diff --git a/modules/lib/test/HttpFilter.test-d.ts b/modules/lib/test/HttpFilter.test-d.ts index 1f721765012..36d77651cfa 100644 --- a/modules/lib/test/HttpFilter.test-d.ts +++ b/modules/lib/test/HttpFilter.test-d.ts @@ -1,9 +1,10 @@ import type { HttpFilterControllerModule, + HttpFilterNext, Request, RequestHandler, + RequestToBeSerializedToJava, Response, - StrictMergeInterfaces, } from '../core/index'; import { @@ -45,30 +46,26 @@ expectAssignable(httpFilterControllerModule1); // ──────────────────────────────────────────────────────────────────────────── // Filter manipulating the request and the response // ──────────────────────────────────────────────────────────────────────────── -type RequestFromJava = StrictMergeInterfaces; - -type RequestToNext = RequestFromJava; - -type ResponseFromNext = StrictMergeInterfaces; - -type ReturnedResponse = ResponseFromNext; - const httpFilterControllerModule2 = { - filter: function (req: RequestFromJava, next: RequestHandler) { + filter: function (req: Request, next: HttpFilterNext, Response>) { // ERROR: I don't think one can add custom properties to the request, only headers... - req.requestLogging = true; // Manipulate request + // req.requestLogging = true; // Manipulate request + req.cookies = { + ...req.cookies, + 'X-Auth-Token': 'letMeIn', + }; log.info('Request:%s', JSON.stringify(req, null, 2)); const response = next(req); // Continue request pipeline - response.responseLogging = true; // Manipulate response + // response.responseLogging = true; // Manipulate response + if (!response.headers) { + response.headers = {}; + } + response.headers['X-Response-Logging'] = 'true'; log.info('Response:%s', JSON.stringify(response, null, 2)); - return response as unknown as ReturnedResponse; + return response as unknown as Response; }, }; -expectAssignable>(httpFilterControllerModule2); +expectAssignable>(httpFilterControllerModule2); // ──────────────────────────────────────────────────────────────────────────── // Filter intercepting the request diff --git a/modules/lib/test/Request.test-d.ts b/modules/lib/test/Request.test-d.ts index 911a35fa4a1..b2f5a7f6488 100644 --- a/modules/lib/test/Request.test-d.ts +++ b/modules/lib/test/Request.test-d.ts @@ -1,6 +1,5 @@ import type { Request, - StrictMergeInterfaces, } from '../core/index'; import {RequestImplementation} from './RequestImplementation'; @@ -17,9 +16,6 @@ import { printType, } from 'tsd'; - - - // Scenario: When implementing a idprovider login function the Request may have a validTicket property // Scenario: When implementing a idprovider logout function the Request may have a validTicket property const idproviderLogoutRequest: Request = new RequestImplementation({ @@ -768,8 +764,9 @@ const branchNotString = { expectNotAssignable(branchNotString); -// Allowed to change type of branch to number -expectAssignable>(branchNotString); +// Allowed to change branch from optional to required, +// but not allowed to change type of branch from string to number +expectNotAssignable>(branchNotString); expectNotAssignable({ ...requiredProperties, @@ -811,9 +808,10 @@ expectNotAssignable({ custom: 'whatever', // Untyped custom property NOT allowed }); -expectAssignable>({ - ...requiredProperties, - custom: 'whatever', // Typed custom property allowed -}); +// Not allowed to add custom properties +// expectAssignable>({ +// ...requiredProperties, +// custom: 'whatever', // Typed custom property allowed +// }); diff --git a/modules/lib/test/Response.test-d.ts b/modules/lib/test/Response.test-d.ts index 63bba6cc5a7..1d0e4593e1d 100644 --- a/modules/lib/test/Response.test-d.ts +++ b/modules/lib/test/Response.test-d.ts @@ -1,7 +1,7 @@ import type { LiteralUnion, Response, - StrictMergeInterfaces, + ResponseBody, } from '../core/index'; import { @@ -33,7 +33,7 @@ expectAssignable(notFoundResponse); // ──────────────────────────────────────────────────────────────────────────── // Scenario: Strictify Response down to a specific Response // ──────────────────────────────────────────────────────────────────────────── -type NotFoundResponse = StrictMergeInterfaces; @@ -48,14 +48,14 @@ expectNotAssignable({ // ──────────────────────────────────────────────────────────────────────────── // Scenario: JSON body // ──────────────────────────────────────────────────────────────────────────── -interface MyObject { +type MyObject = { key: string -} +}; const object = { key: 'value', }; -type JsonResponse = StrictMergeInterfaces = Response<{ body: Body, contentType: LiteralUnion<'application/json'> }>; @@ -68,10 +68,10 @@ const jsonResponse = { // printType(jsonResponse); -// Untyped body object not allowed -// expectNotAssignable(jsonResponse); // TODO +// Untyped body object allowed +expectAssignable(jsonResponse); -// Typed body object allowed +// Typed body object also allowed expectAssignable>(jsonResponse); diff --git a/modules/lib/test/ResponseProcessor.test-d.ts b/modules/lib/test/ResponseProcessor.test-d.ts index 34aaf6fee56..cff95e9b3e8 100644 --- a/modules/lib/test/ResponseProcessor.test-d.ts +++ b/modules/lib/test/ResponseProcessor.test-d.ts @@ -3,7 +3,6 @@ import type { MappedResponse, Response, ResponseProcessorControllerModule, - // StrictMergeInterfaces, } from '../core/index'; import { From 349080b19fe80a386279020e051f1f6511219bba Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Fri, 11 Oct 2024 12:59:45 +0200 Subject: [PATCH 06/19] RequestToBeSerializedToJava -> SerializableRequest --- modules/lib/core/index.d.ts | 6 +++--- modules/lib/test/Controller.test-d.ts | 2 +- modules/lib/test/HttpFilter.test-d.ts | 4 ++-- modules/lib/test/Request.test-d.ts | 4 ++-- modules/lib/test/Response.test-d.ts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/lib/core/index.d.ts b/modules/lib/core/index.d.ts index 0beeefd1e9d..32735e5d373 100644 --- a/modules/lib/core/index.d.ts +++ b/modules/lib/core/index.d.ts @@ -291,7 +291,7 @@ export type Request< T extends Partial = Record > = StrictMergeInterfaces; -export type RequestToBeSerializedToJava = Omit< +export type SerializableRequest = Omit< Partial, 'body' | 'contextPath' | 'rawPath' | 'repositoryId' | 'webSocket' > & { @@ -351,7 +351,7 @@ export type RequestHandler< > = (request: RequestFromJava) => ResponseToJava; export type HttpFilterNext< - RequestToJava extends RequestToBeSerializedToJava = RequestToBeSerializedToJava, + RequestToJava extends SerializableRequest = SerializableRequest, ResponseToJava extends ResponseInterface = DefaultResponse > = (request: RequestToJava) => ResponseToJava; @@ -377,7 +377,7 @@ export interface HttpFilterControllerModule< filter: ( request: RequestFromJava, next: HttpFilterNext< - RequestToBeSerializedToJava, + SerializableRequest, ResponseFromNext > ) => ResponseToJava; diff --git a/modules/lib/test/Controller.test-d.ts b/modules/lib/test/Controller.test-d.ts index cc41f72dbfe..8f3c66ddca2 100644 --- a/modules/lib/test/Controller.test-d.ts +++ b/modules/lib/test/Controller.test-d.ts @@ -11,7 +11,7 @@ import { expectAssignable, // expectDeprecated, // expectDocCommentIncludes, - expectError, + // expectError, // expectNever, expectNotAssignable, // expectNotDeprecated, diff --git a/modules/lib/test/HttpFilter.test-d.ts b/modules/lib/test/HttpFilter.test-d.ts index 36d77651cfa..9a6bcb23148 100644 --- a/modules/lib/test/HttpFilter.test-d.ts +++ b/modules/lib/test/HttpFilter.test-d.ts @@ -3,8 +3,8 @@ import type { HttpFilterNext, Request, RequestHandler, - RequestToBeSerializedToJava, Response, + SerializableRequest, } from '../core/index'; import { @@ -47,7 +47,7 @@ expectAssignable(httpFilterControllerModule1); // Filter manipulating the request and the response // ──────────────────────────────────────────────────────────────────────────── const httpFilterControllerModule2 = { - filter: function (req: Request, next: HttpFilterNext, Response>) { + filter: function (req: Request, next: HttpFilterNext, Response>) { // ERROR: I don't think one can add custom properties to the request, only headers... // req.requestLogging = true; // Manipulate request req.cookies = { diff --git a/modules/lib/test/Request.test-d.ts b/modules/lib/test/Request.test-d.ts index b2f5a7f6488..e159762e4af 100644 --- a/modules/lib/test/Request.test-d.ts +++ b/modules/lib/test/Request.test-d.ts @@ -7,13 +7,13 @@ import { expectAssignable, // expectDeprecated, // expectDocCommentIncludes, - expectError, + // expectError, // expectNever, expectNotAssignable, // expectNotDeprecated, // expectNotType, // expectType, - printType, + // printType, } from 'tsd'; // Scenario: When implementing a idprovider login function the Request may have a validTicket property diff --git a/modules/lib/test/Response.test-d.ts b/modules/lib/test/Response.test-d.ts index 1d0e4593e1d..1d76dc7c525 100644 --- a/modules/lib/test/Response.test-d.ts +++ b/modules/lib/test/Response.test-d.ts @@ -11,7 +11,7 @@ import { // expectError, // expectNever, expectNotAssignable, - printType, + // printType, // expectNotDeprecated, // expectNotType, // expectType, From 56249b17c6c2613af8d41bbb63747796a7fbfea7 Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Fri, 11 Oct 2024 13:25:54 +0200 Subject: [PATCH 07/19] ErrorController and IdProviderController --- modules/lib/core/index.d.ts | 69 +++++++++++++++++++- modules/lib/test/Controller.test-d.ts | 8 +-- modules/lib/test/HttpFilter.test-d.ts | 18 ++--- modules/lib/test/ResponseProcessor.test-d.ts | 6 +- 4 files changed, 82 insertions(+), 19 deletions(-) diff --git a/modules/lib/core/index.d.ts b/modules/lib/core/index.d.ts index 32735e5d373..d517d96842f 100644 --- a/modules/lib/core/index.d.ts +++ b/modules/lib/core/index.d.ts @@ -355,7 +355,7 @@ export type HttpFilterNext< ResponseToJava extends ResponseInterface = DefaultResponse > = (request: RequestToJava) => ResponseToJava; -export interface ControllerModule { +export interface Controller { all?: RequestHandler // connect?: RequestHandler delete?: RequestHandler @@ -369,7 +369,70 @@ export interface ControllerModule { // TODO what about propfind, proppatch, mkcol, copy, move, lock and unlock? } -export interface HttpFilterControllerModule< +export interface ErrorRequest { + exception?: unknown + message: string + request: T + status: number +} + +export type ErrorRequestHandler< + Err extends ErrorRequest = ErrorRequest, + ResponseToJava extends ResponseInterface = DefaultResponse +> = (err: Err) => ResponseToJava; + +export interface ErrorController { + handle400?: ErrorRequestHandler + handle401?: ErrorRequestHandler + handle402?: ErrorRequestHandler + handle403?: ErrorRequestHandler + handle404?: ErrorRequestHandler + handle405?: ErrorRequestHandler + handle406?: ErrorRequestHandler + handle407?: ErrorRequestHandler + handle408?: ErrorRequestHandler + handle409?: ErrorRequestHandler + handle410?: ErrorRequestHandler + handle411?: ErrorRequestHandler + handle412?: ErrorRequestHandler + handle413?: ErrorRequestHandler + handle414?: ErrorRequestHandler + handle415?: ErrorRequestHandler + handle416?: ErrorRequestHandler + handle417?: ErrorRequestHandler + handle418?: ErrorRequestHandler + handle421?: ErrorRequestHandler + handle422?: ErrorRequestHandler + handle423?: ErrorRequestHandler + handle424?: ErrorRequestHandler + handle425?: ErrorRequestHandler + handle426?: ErrorRequestHandler + handle428?: ErrorRequestHandler + handle429?: ErrorRequestHandler + handle431?: ErrorRequestHandler + handle451?: ErrorRequestHandler + handle500?: ErrorRequestHandler + handle501?: ErrorRequestHandler + handle502?: ErrorRequestHandler + handle503?: ErrorRequestHandler + handle504?: ErrorRequestHandler + handle505?: ErrorRequestHandler + handle506?: ErrorRequestHandler + handle507?: ErrorRequestHandler + handle508?: ErrorRequestHandler + handle510?: ErrorRequestHandler + handle511?: ErrorRequestHandler + handleError?: ErrorRequestHandler +} + +export interface IdProviderController extends Controller { + autoLogin?: RequestHandler + handle401?: RequestHandler + login?: RequestHandler + logout?: RequestHandler +} + +export interface HttpFilterController< RequestFromJava extends RequestInterface = DefaultRequest, ResponseFromNext extends ResponseInterface = Response, ResponseToJava extends ResponseInterface = ResponseFromNext @@ -383,7 +446,7 @@ export interface HttpFilterControllerModule< ) => ResponseToJava; } -export interface ResponseProcessorControllerModule< +export interface ResponseProcessorController< RequestFromJava extends RequestInterface = DefaultRequest, ResponseFromJava extends MappedResponse = MappedResponse, ResponseToJava extends ResponseInterface = Partial diff --git a/modules/lib/test/Controller.test-d.ts b/modules/lib/test/Controller.test-d.ts index 8f3c66ddca2..b6f17e544c5 100644 --- a/modules/lib/test/Controller.test-d.ts +++ b/modules/lib/test/Controller.test-d.ts @@ -1,5 +1,5 @@ import type { - ControllerModule, + Controller, Request, RequestBranch, RequestMethod, @@ -24,7 +24,7 @@ const log = { info: (message?: string, ...optionalParams: string[]) => { /* no-op */ }, }; -const myControllerModule = { +const myController = { all: (request: Request) => { log.info('all request:%s', JSON.stringify(request, null, 4)); return { @@ -51,7 +51,7 @@ const myControllerModule = { }, }; -expectAssignable(myControllerModule); +expectAssignable(myController); type PageRequest = Omit; type PageRequestHandler = (request: PageRequest) => Response; -interface PageControllerModule { +interface PageController { all?: PageRequestHandler // connect?: PageRequestHandler delete?: PageRequestHandler diff --git a/modules/lib/test/HttpFilter.test-d.ts b/modules/lib/test/HttpFilter.test-d.ts index 9a6bcb23148..9f68f49a101 100644 --- a/modules/lib/test/HttpFilter.test-d.ts +++ b/modules/lib/test/HttpFilter.test-d.ts @@ -1,5 +1,5 @@ import type { - HttpFilterControllerModule, + HttpFilterController, HttpFilterNext, Request, RequestHandler, @@ -32,7 +32,7 @@ const log = { // ──────────────────────────────────────────────────────────────────────────── // Minimal filter timing the subsequent request // ──────────────────────────────────────────────────────────────────────────── -const httpFilterControllerModule1 = { +const httpFilterController1 = { filter: function (req: Request, next: RequestHandler) { const before = new Date().getTime(); const response = next(req); // next(req) hands over the request to the engine pipeline and returns the response @@ -41,12 +41,12 @@ const httpFilterControllerModule1 = { return response; }, }; -expectAssignable(httpFilterControllerModule1); +expectAssignable(httpFilterController1); // ──────────────────────────────────────────────────────────────────────────── // Filter manipulating the request and the response // ──────────────────────────────────────────────────────────────────────────── -const httpFilterControllerModule2 = { +const httpFilterController2 = { filter: function (req: Request, next: HttpFilterNext, Response>) { // ERROR: I don't think one can add custom properties to the request, only headers... // req.requestLogging = true; // Manipulate request @@ -65,12 +65,12 @@ const httpFilterControllerModule2 = { return response as unknown as Response; }, }; -expectAssignable>(httpFilterControllerModule2); +expectAssignable>(httpFilterController2); // ──────────────────────────────────────────────────────────────────────────── // Filter intercepting the request // ──────────────────────────────────────────────────────────────────────────── -const httpFilterControllerModule3 = { +const httpFilterController3 = { filter: function (req: Request, next: RequestHandler) { if (req.getHeader('X-Auth-Token') !== 'letMeIn') { // intercept request pipeline @@ -84,12 +84,12 @@ const httpFilterControllerModule3 = { return next(req); }, }; -expectAssignable(httpFilterControllerModule3); +expectAssignable(httpFilterController3); // ──────────────────────────────────────────────────────────────────────────── // Filter changing request params // ──────────────────────────────────────────────────────────────────────────── -const httpFilterControllerModule4 = { +const httpFilterController4 = { filter: function (req: Request, next: RequestHandler) { req.params = { param1: 'val', // if param1 was not in the original request it will be added, otherwise the original value will be replaced @@ -100,4 +100,4 @@ const httpFilterControllerModule4 = { return next(req); }, }; -expectAssignable(httpFilterControllerModule4); \ No newline at end of file +expectAssignable(httpFilterController4); \ No newline at end of file diff --git a/modules/lib/test/ResponseProcessor.test-d.ts b/modules/lib/test/ResponseProcessor.test-d.ts index cff95e9b3e8..5e5e43ddd86 100644 --- a/modules/lib/test/ResponseProcessor.test-d.ts +++ b/modules/lib/test/ResponseProcessor.test-d.ts @@ -2,7 +2,7 @@ import type { Request, MappedResponse, Response, - ResponseProcessorControllerModule, + ResponseProcessorController, } from '../core/index'; import { @@ -29,7 +29,7 @@ import { // ──────────────────────────────────────────────────────────────────────────── // dynamically adds a bodyEnd page contribution to the response // ──────────────────────────────────────────────────────────────────────────── -const responseProcessorControllerModule1 = { +const responseProcessorController1 = { responseProcessor: (_req: Request, res: MappedResponse) => { const trackingScript = ''; @@ -45,4 +45,4 @@ const responseProcessorControllerModule1 = { return res as Response; }, }; -expectAssignable(responseProcessorControllerModule1); +expectAssignable(responseProcessorController1); From 24d35313e32c834c1d2f75b467e7628630b1a432 Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Fri, 11 Oct 2024 13:36:11 +0200 Subject: [PATCH 08/19] tests for ErrorController and IdProviderController --- modules/lib/test/ErrorController.test-d.ts | 39 ++++++++++ .../lib/test/IdProviderController.test-d.ts | 75 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 modules/lib/test/ErrorController.test-d.ts create mode 100644 modules/lib/test/IdProviderController.test-d.ts diff --git a/modules/lib/test/ErrorController.test-d.ts b/modules/lib/test/ErrorController.test-d.ts new file mode 100644 index 00000000000..6c1acff0eb1 --- /dev/null +++ b/modules/lib/test/ErrorController.test-d.ts @@ -0,0 +1,39 @@ +import type { + ErrorController, + ErrorRequest, +} from '../core/index'; + +import { + expectAssignable, + // expectDeprecated, + // expectDocCommentIncludes, + // expectError, + // expectNever, + // expectNotAssignable, + // printType, + // expectNotDeprecated, + // expectNotType, + // expectType, + // printType, +} from 'tsd'; + +const log = { + info: (message?: string, ...optionalParams: unknown[]) => { /* no-op */ }, +}; + +const myErrorController = { + handle404: (errorRequest: ErrorRequest) => { + log.info('404 errorRequest:%s', JSON.stringify(errorRequest, null, 4)); + return { + status: 404 as const, + }; + }, + handleError: (errorRequest: ErrorRequest) => { + log.info('generic errorRequest:%s', JSON.stringify(errorRequest, null, 4)); + return { + status: 500 as const, + }; + }, +}; + +expectAssignable(myErrorController); \ No newline at end of file diff --git a/modules/lib/test/IdProviderController.test-d.ts b/modules/lib/test/IdProviderController.test-d.ts new file mode 100644 index 00000000000..8f2e0adbbba --- /dev/null +++ b/modules/lib/test/IdProviderController.test-d.ts @@ -0,0 +1,75 @@ +import type { + IdProviderController, + Request, +} from '../core/index'; + +import { + expectAssignable, + // expectDeprecated, + // expectDocCommentIncludes, + // expectError, + // expectNever, + // expectNotAssignable, + // printType, + // expectNotDeprecated, + // expectNotType, + // expectType, + // printType, +} from 'tsd'; + +const log = { + info: (message?: string, ...optionalParams: unknown[]) => { /* no-op */ }, +}; + +const myIdProviderController = { + autoLogin: (request: Request) => { + log.info('autoLogin request:%s', JSON.stringify(request, null, 4)); + return { + status: 200, + }; + }, + handle401: (request: Request) => { + log.info('401 request:%s', JSON.stringify(request, null, 4)); + return { + contentType: 'text/html', + status: 401 as const, + body: ` + + + + 401 Unauthorized + + +

401 Unauthorized

+

Unauthorized access to this resource, please login.

+ +`, + }; + }, + get: (request: Request) => { + log.info('get request:%s', JSON.stringify(request, null, 4)); + return { + status: 200, + }; + }, + login: (request: Request) => { + log.info('login request:%s', JSON.stringify(request, null, 4)); + return { + status: 200, + }; + }, + logout: (request: Request) => { + log.info('logout request:%s', JSON.stringify(request, null, 4)); + return { + status: 200, + }; + }, + post: (request: Request) => { + log.info('post request:%s', JSON.stringify(request, null, 4)); + return { + status: 200, + }; + }, +}; + +expectAssignable(myIdProviderController); \ No newline at end of file From 33d31b8e84a255e6d29bcc1d4eb76af0db20fc73 Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Fri, 11 Oct 2024 13:38:58 +0200 Subject: [PATCH 09/19] Remove experiment --- modules/lib/core/index.d.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/modules/lib/core/index.d.ts b/modules/lib/core/index.d.ts index d517d96842f..10ca8e6ffb4 100644 --- a/modules/lib/core/index.d.ts +++ b/modules/lib/core/index.d.ts @@ -607,23 +607,8 @@ export interface LayoutRegion< components: Components; } -// type HasLayout = T extends [infer F, ...infer R] -// ? F extends LayoutComponent | Layout -// ? true -// : HasLayout -// : false; - export type Region< Components extends - // ( - // HasLayout extends true - // ? (FragmentComponent | LayoutComponent | PartComponent | TextComponent)[] - // : (FragmentComponent | PartComponent | TextComponent)[] - // ) = ( - // HasLayout extends true - // ? (FragmentComponent | Layout | Part | TextComponent)[] - // : (FragmentComponent | Part | TextComponent)[] - // ) (FragmentComponent | LayoutComponent | PartComponent | TextComponent)[] = (FragmentComponent | Layout | Part | TextComponent)[] // @ts-expect-error TODO LayoutRegion can't eat LayoutComponent nor Layout!!! From 341f5e35ecd00c5783b487d1c8909defd01c1c95 Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Mon, 14 Oct 2024 08:28:11 +0200 Subject: [PATCH 10/19] Less http methods --- modules/lib/core/index.d.ts | 7 +------ modules/lib/test/Controller.test-d.ts | 4 ---- modules/lib/test/Request.test-d.ts | 4 ++-- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/modules/lib/core/index.d.ts b/modules/lib/core/index.d.ts index 10ca8e6ffb4..b12e7ec5ba9 100644 --- a/modules/lib/core/index.d.ts +++ b/modules/lib/core/index.d.ts @@ -210,7 +210,7 @@ export interface ComplexCookie { export type RequestBranch = 'draft' | 'master'; export type RequestGetHeaderFunction = (headerName: string) => string | null; -export type RequestMethod = 'GET' | 'POST' | ' HEAD' | 'OPTIONS' | ' PUT' | 'DELETE' | ' TRACE' | 'CONNECT' | ' PATCH' | 'PROPFIND' | ' PROPPATCH' | 'MKCOL' | ' COPY' | 'MOVE' | ' LOCK' | 'UNLOCK'; +export type RequestMethod = 'GET' | 'POST' | ' HEAD' | 'OPTIONS' | ' PUT' | 'DELETE'; export type RequestMode = 'edit' | 'inline' | 'live' | 'preview' | 'admin'; export type RequestParams = Record; export type RequestScheme = 'http' | 'https'; @@ -222,7 +222,6 @@ export type RequestHeaders = Record; export type ResponseHeaders = Record; export interface DefaultRequestCookies extends RequestCookies { - enonic_xp_tour?: string JSESSIONID?: string } @@ -357,16 +356,12 @@ export type HttpFilterNext< export interface Controller { all?: RequestHandler - // connect?: RequestHandler delete?: RequestHandler get?: RequestHandler head?: RequestHandler options?: RequestHandler - // patch?: RequestHandler post?: RequestHandler put?: RequestHandler - // trace?: RequestHandler - // TODO what about propfind, proppatch, mkcol, copy, move, lock and unlock? } export interface ErrorRequest { diff --git a/modules/lib/test/Controller.test-d.ts b/modules/lib/test/Controller.test-d.ts index b6f17e544c5..914be428669 100644 --- a/modules/lib/test/Controller.test-d.ts +++ b/modules/lib/test/Controller.test-d.ts @@ -71,16 +71,12 @@ type PageRequestHandler = (request: PageRequest) => Response; interface PageController { all?: PageRequestHandler - // connect?: PageRequestHandler delete?: PageRequestHandler get?: PageRequestHandler head?: PageRequestHandler options?: PageRequestHandler - // patch?: PageRequestHandler post?: PageRequestHandler put?: PageRequestHandler - // trace?: PageRequestHandler - // TODO what about propfind, proppatch, mkcol, copy, move, lock and unlock? } const pageRequest /* : PageRequest */ = { diff --git a/modules/lib/test/Request.test-d.ts b/modules/lib/test/Request.test-d.ts index e159762e4af..d96a14d99f2 100644 --- a/modules/lib/test/Request.test-d.ts +++ b/modules/lib/test/Request.test-d.ts @@ -738,10 +738,10 @@ expectAssignable({ branch: 'string but not draft|master', }); -// LiteralUnion suggests 'GET'|'POST'|'PUT'|'DELETE'|'HEAD'|'OPTIONS'|'PATCH'|'TRACE'|'CONNECT', but allows string +// LiteralUnion suggests 'GET'|'POST'|'PUT'|'DELETE'|'HEAD'|'OPTIONS', but allows string expectAssignable({ ...requiredProperties, - method: 'string but not GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|TRACE|CONNECT', + method: 'string but not GET|POST|PUT|DELETE|HEAD|OPTIONS', }); // LiteralUnion suggests 'edit'|'inline'|'live'|'preview'|'admin', but allows string From 666b878ee9c9643654c177e7a4f21ff1ef900b65 Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Mon, 14 Oct 2024 08:33:00 +0200 Subject: [PATCH 11/19] lcKeys --- modules/lib/test/RequestImplementation.ts | 54 ++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/modules/lib/test/RequestImplementation.ts b/modules/lib/test/RequestImplementation.ts index 5b385ff727a..fb350bef73f 100644 --- a/modules/lib/test/RequestImplementation.ts +++ b/modules/lib/test/RequestImplementation.ts @@ -5,6 +5,58 @@ import type { RequestHeaders, } from '../core/index'; +type ReplacerFn = (this: unknown, key: string, value: unknown) => unknown; +type Replacer = ReplacerFn | undefined; + +export function toStr( + value: unknown, + replacer? :Replacer, + space: string | number | undefined = 4, +): string { + return JSON.stringify(value, replacer, space); +} + +const isObject = (value: object | unknown): value is object => + Object.prototype.toString.call(value).slice(8,-1) === 'Object'; + +function mapKeys( + obj: object, + fn: ({ + key, + result, + value, + }: { + key: PropertyKey + result: object + value: unknown + }) => void, +): object { + if (!isObject(obj)) { + throw new TypeError(`mapKeys: First param must be an object! got:${toStr(obj)}`); + } + const result = {}; + const keys = Object.keys(obj); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + fn({ + key, + result, + value: obj[key], + }); + } + return result; +} + +function lcKeys>(obj: T): T { + return mapKeys(obj,({ + key, + result, + value, + }) => { + result[String(key).toLowerCase()] = value; + }) as T; +} + export class RequestImplementation implements Request { cookies: RequestCookies; headers: RequestHeaders; @@ -52,7 +104,7 @@ export class RequestImplementation implements Request { body, }: RequestConstructorParams) { this.cookies = cookies; - this.headers = headers; + this.headers = lcKeys(headers); this.host = host; this.method = method; this.mode = mode; From d08726971b27078e3b0fdbfdc0747101655bcfb2 Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Mon, 14 Oct 2024 08:33:15 +0200 Subject: [PATCH 12/19] codacy --- modules/lib/test/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/lib/test/tsconfig.json b/modules/lib/test/tsconfig.json index e07cce9ad1e..3d3d1aba7ad 100644 --- a/modules/lib/test/tsconfig.json +++ b/modules/lib/test/tsconfig.json @@ -1,5 +1,5 @@ { "compilerOptions": { - "lib": ["DOM"], + "lib": ["DOM"] } } \ No newline at end of file From d7185f014e0421a61b346128cce466b991b374eb Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Mon, 14 Oct 2024 09:21:19 +0200 Subject: [PATCH 13/19] webSocketEvent --- modules/lib/core/index.d.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/modules/lib/core/index.d.ts b/modules/lib/core/index.d.ts index b12e7ec5ba9..dc3d6212a9b 100644 --- a/modules/lib/core/index.d.ts +++ b/modules/lib/core/index.d.ts @@ -354,6 +354,38 @@ export type HttpFilterNext< ResponseToJava extends ResponseInterface = DefaultResponse > = (request: RequestToJava) => ResponseToJava; +export interface WebSocketSession { + id: string, + path: string, + params: Record, + user: Pick, + // TODO: What about 'type'|'modifiedTime'|'email'? +} + +export interface WebSocketOpenEvent { + data: T + session: WebSocketSession + type: 'open' +} + +export interface WebSocketMessageEvent { + data: T, + message: string + session: WebSocketSession, + type: 'message', +} + +export interface WebSocketCloseEvent { + closeReason: number + data: T, + session: WebSocketSession, + type: 'close'; +} + +type WebSocketEvent = WebSocketOpenEvent | WebSocketMessageEvent | WebSocketCloseEvent; + +type WebSocketEventHandler = (event: WebSocketEvent) => void; + export interface Controller { all?: RequestHandler delete?: RequestHandler @@ -362,6 +394,7 @@ export interface Controller { options?: RequestHandler post?: RequestHandler put?: RequestHandler + webSocketEvent?: WebSocketEventHandler } export interface ErrorRequest { From 80b0be33a70377cb01245c5a49b58cfcb476744a Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Mon, 14 Oct 2024 09:42:04 +0200 Subject: [PATCH 14/19] Make WebSocketEvent more like WebSocketEventMapper --- modules/lib/core/index.d.ts | 39 +++++++++++++++---------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/modules/lib/core/index.d.ts b/modules/lib/core/index.d.ts index dc3d6212a9b..0ff9f919968 100644 --- a/modules/lib/core/index.d.ts +++ b/modules/lib/core/index.d.ts @@ -355,34 +355,27 @@ export type HttpFilterNext< > = (request: RequestToJava) => ResponseToJava; export interface WebSocketSession { - id: string, - path: string, - params: Record, - user: Pick, - // TODO: What about 'type'|'modifiedTime'|'email'? -} + id: string + path: string -export interface WebSocketOpenEvent { - data: T - session: WebSocketSession - type: 'open' -} + // TODO Maybe Record + // See com.enonic.xp.portal.impl.mapper.WebSocketEventMapper + // And com.enonic.xp.portal.impl.mapper.MapperHelper + params: Record -export interface WebSocketMessageEvent { - data: T, - message: string - session: WebSocketSession, - type: 'message', + user: Omit } -export interface WebSocketCloseEvent { - closeReason: number - data: T, - session: WebSocketSession, - type: 'close'; -} +export type WebSocketEventType = 'open' | 'message' | 'error' | 'close'; -type WebSocketEvent = WebSocketOpenEvent | WebSocketMessageEvent | WebSocketCloseEvent; +export interface WebSocketEvent { + data: T + closeReason?: number + error?: string + message?: string + session: WebSocketSession + type: WebSocketEventType +} type WebSocketEventHandler = (event: WebSocketEvent) => void; From 9603d687831a4a2f0369a48354beac44c0685c65 Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Fri, 18 Oct 2024 08:19:03 +0200 Subject: [PATCH 15/19] Fix most PR comments --- modules/lib/core/index.d.ts | 309 +++++++++--------- modules/lib/test/Controller.test-d.ts | 31 +- modules/lib/test/ErrorController.test-d.ts | 14 +- modules/lib/test/HttpFilter.test-d.ts | 14 +- .../lib/test/IdProviderController.test-d.ts | 14 +- modules/lib/test/Request.test-d.ts | 12 +- modules/lib/test/RequestImplementation.ts | 14 +- modules/lib/test/Response.test-d.ts | 9 - modules/lib/test/ResponseProcessor.test-d.ts | 10 - 9 files changed, 187 insertions(+), 240 deletions(-) diff --git a/modules/lib/core/index.d.ts b/modules/lib/core/index.d.ts index 0ff9f919968..4a7c0b6d950 100644 --- a/modules/lib/core/index.d.ts +++ b/modules/lib/core/index.d.ts @@ -144,7 +144,7 @@ export type XpRequire = (path: export type LiteralUnion = T | (U & Record); -export type StrictMergeInterfaces< +export type Merge< A, B = Record > = B extends Record @@ -157,13 +157,13 @@ export interface ComplexCookie { * * @type string */ - value: string + value: string; /** * A comment (rfc2109) to document the cookie (optional). * * @type string */ - comment?: string + comment?: string; /** * The expiration date and time for the cookie (optional). * @@ -175,25 +175,25 @@ export interface ComplexCookie { * * @type string */ - domain?: string + domain?: string; /** * Indicates whether the cookie should not be accessible via JavaScript (optional). * * @type string */ - httpOnly?: boolean + httpOnly?: boolean; /** * The maximum age of the cookie in seconds (optional). * * @type string */ - maxAge?: number + maxAge?: number; /** * The path on the server where the cookie should be available. * * @type string */ - path?: string + path?: string; /** * Specifies the SameSite attribute (draft RFC) for the cookie (optional). * @@ -205,12 +205,12 @@ export interface ComplexCookie { * * @type string */ - secure?: boolean + secure?: boolean; } export type RequestBranch = 'draft' | 'master'; export type RequestGetHeaderFunction = (headerName: string) => string | null; -export type RequestMethod = 'GET' | 'POST' | ' HEAD' | 'OPTIONS' | ' PUT' | 'DELETE'; +export type RequestMethod = 'GET' | 'POST' | ' HEAD' | 'OPTIONS' | ' PUT' | 'DELETE' | 'PATCH' | 'TRACE' | 'CONNECT'; export type RequestMode = 'edit' | 'inline' | 'live' | 'preview' | 'admin'; export type RequestParams = Record; export type RequestScheme = 'http' | 'https'; @@ -222,73 +222,70 @@ export type RequestHeaders = Record; export type ResponseHeaders = Record; export interface DefaultRequestCookies extends RequestCookies { - JSESSIONID?: string + JSESSIONID?: string; } export interface DefaultRequestHeaders extends RequestHeaders { - Accept?: string - 'Accept-Charset'?: string - 'Accept-Encoding'?: string - 'Accept-Language'?: string - Authorization?: string - 'Cache-Control'?: string - Connection?: string - 'Content-Length'?: string - 'Content-Type'?: string - Cookie?: string - Language?: string - Host?: string - 'If-None-Match'?: string - Referer?: string - 'sec-ch-ua'?: string - 'sec-ch-ua-mobile'?: string - 'sec-ch-ua-platform'?: string - 'Sec-Fetch-Dest'?: string - 'Sec-Fetch-Mode'?: string - 'Sec-Fetch-Site'?: string - 'Sec-Fetch-User'?: string - 'Upgrade-Insecure-Requests'?: string - 'User-Agent'?: string - 'X-Forwarded-For'?: string - 'X-Forwarded-Host'?: string - 'X-Forwarded-Proto'?: string - 'X-Forwarded-Server'?: string + Accept?: string; + 'Accept-Charset'?: string; + 'Accept-Encoding'?: string; + 'Accept-Language'?: string; + Authorization?: string; + 'Cache-Control'?: string; + Connection?: string; + 'Content-Length'?: string; + 'Content-Type'?: string; + Cookie?: string; + Language?: string; // Not mentioned here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers + Host?: string; + 'If-None-Match'?: string; + Referer?: string; + 'Sec-Fetch-Dest'?: string; + 'Sec-Fetch-Mode'?: string; + 'Sec-Fetch-Site'?: string; + 'Sec-Fetch-User'?: string; + 'Upgrade-Insecure-Requests'?: string; + 'User-Agent'?: string; + 'X-Forwarded-For'?: string; + 'X-Forwarded-Host'?: string; + 'X-Forwarded-Proto'?: string; + 'X-Forwarded-Server'?: string; } export interface RequestConstructorParams { - body?: string - branch?: LiteralUnion - contentType?: string - contextPath?: string - cookies: RequestCookies - headers: RequestHeaders - host: string - method: LiteralUnion - mode: LiteralUnion - params: RequestParams - path: string - port: number - rawPath: string - remoteAddress: string - repositoryId?: string - scheme: LiteralUnion - url: string - validTicket?: boolean - webSocket: boolean + body?: string; + branch?: LiteralUnion; + contentType?: string; + contextPath?: string; + cookies: RequestCookies; + headers: RequestHeaders; + host: string; + method: LiteralUnion; + mode: LiteralUnion; + params: RequestParams; + path: string; + port: number; + rawPath: string; + remoteAddress: string; + repositoryId?: string; + scheme: LiteralUnion; + url: string; + validTicket?: boolean; + webSocket: boolean; } export interface RequestInterface extends RequestConstructorParams { - getHeader: RequestGetHeaderFunction + getHeader: RequestGetHeaderFunction; } export interface DefaultRequest extends RequestInterface { - cookies: DefaultRequestCookies - headers: DefaultRequestHeaders + cookies: DefaultRequestCookies; + headers: DefaultRequestHeaders; } export type Request< T extends Partial = Record -> = StrictMergeInterfaces; +> = Merge; export type SerializableRequest = Omit< Partial, @@ -298,13 +295,13 @@ export type SerializableRequest = O }; export interface DefaultResponseHeaders extends ResponseHeaders { - 'Cache-Control'?: string - 'Content-Encoding'?: string - 'Content-Type'?: string - 'Content-Security-Policy'?: string - 'Date'?: string - Etag?: string | number - Location?: string + 'Cache-Control'?: string; + 'Content-Encoding'?: string; + 'Content-Type'?: string; + 'Content-Security-Policy'?: string; + 'Date'?: string; + Etag?: string | number; + Location?: string; } // NOTE Even though PortalResponseSerializer allows non-array values, @@ -312,37 +309,37 @@ export interface DefaultResponseHeaders extends ResponseHeaders { // So perhaps it's better to enforce that, so it's consistent both ways. // It also causes problems in ResponseProcessors, since they work with both from and to Java. export interface PageContributions { - headBegin?: string[] - headEnd?: string[] - bodyBegin?: string[] - bodyEnd?: string[] + headBegin?: string[]; + headEnd?: string[]; + bodyBegin?: string[]; + bodyEnd?: string[]; } export type ResponseBody = unknown[] | Record | boolean | number | string | null | ByteSource; export interface MappedResponse { - applyFilters: boolean - body?: ResponseBody - contentType: string - cookies: ResponseCookies - headers: ResponseHeaders - pageContributions: PageContributions - postProcess: boolean - status: number + applyFilters: boolean; + body?: ResponseBody; + contentType: string; + cookies: ResponseCookies; + headers: ResponseHeaders; + pageContributions: PageContributions; + postProcess: boolean; + status: number; } export interface ResponseInterface extends Partial { - redirect?: string + redirect?: string; } export interface DefaultResponse extends ResponseInterface { - contentType?: LiteralUnion<'text/html' | 'application/json'> - headers?: DefaultResponseHeaders + contentType?: LiteralUnion<'text/html' | 'application/json'>; + headers?: DefaultResponseHeaders; } export type Response< T extends Partial = Record -> = StrictMergeInterfaces; +> = Merge; export type RequestHandler< RequestFromJava extends RequestInterface = DefaultRequest, @@ -355,46 +352,44 @@ export type HttpFilterNext< > = (request: RequestToJava) => ResponseToJava; export interface WebSocketSession { - id: string - path: string - - // TODO Maybe Record - // See com.enonic.xp.portal.impl.mapper.WebSocketEventMapper - // And com.enonic.xp.portal.impl.mapper.MapperHelper - params: Record - - user: Omit + id: string; + params: Record; + path: string; + user: Omit; } export type WebSocketEventType = 'open' | 'message' | 'error' | 'close'; export interface WebSocketEvent { - data: T - closeReason?: number - error?: string - message?: string - session: WebSocketSession - type: WebSocketEventType + data: T; + closeReason?: number; + error?: string; + message?: string; + session: WebSocketSession; + type: WebSocketEventType; } -type WebSocketEventHandler = (event: WebSocketEvent) => void; +type WebSocketEventHandler> = (event: WebSocketEvent) => void; export interface Controller { - all?: RequestHandler - delete?: RequestHandler - get?: RequestHandler - head?: RequestHandler - options?: RequestHandler - post?: RequestHandler - put?: RequestHandler - webSocketEvent?: WebSocketEventHandler + all?: RequestHandler; + connect?: RequestHandler; + delete?: RequestHandler; + get?: RequestHandler; + head?: RequestHandler; + options?: RequestHandler; + patch?: RequestHandler; + post?: RequestHandler; + put?: RequestHandler; + trace?: RequestHandler; + webSocketEvent?: WebSocketEventHandler; } export interface ErrorRequest { - exception?: unknown - message: string - request: T - status: number + exception?: unknown; + message: string; + request: T; + status: number; } export type ErrorRequestHandler< @@ -403,54 +398,54 @@ export type ErrorRequestHandler< > = (err: Err) => ResponseToJava; export interface ErrorController { - handle400?: ErrorRequestHandler - handle401?: ErrorRequestHandler - handle402?: ErrorRequestHandler - handle403?: ErrorRequestHandler - handle404?: ErrorRequestHandler - handle405?: ErrorRequestHandler - handle406?: ErrorRequestHandler - handle407?: ErrorRequestHandler - handle408?: ErrorRequestHandler - handle409?: ErrorRequestHandler - handle410?: ErrorRequestHandler - handle411?: ErrorRequestHandler - handle412?: ErrorRequestHandler - handle413?: ErrorRequestHandler - handle414?: ErrorRequestHandler - handle415?: ErrorRequestHandler - handle416?: ErrorRequestHandler - handle417?: ErrorRequestHandler - handle418?: ErrorRequestHandler - handle421?: ErrorRequestHandler - handle422?: ErrorRequestHandler - handle423?: ErrorRequestHandler - handle424?: ErrorRequestHandler - handle425?: ErrorRequestHandler - handle426?: ErrorRequestHandler - handle428?: ErrorRequestHandler - handle429?: ErrorRequestHandler - handle431?: ErrorRequestHandler - handle451?: ErrorRequestHandler - handle500?: ErrorRequestHandler - handle501?: ErrorRequestHandler - handle502?: ErrorRequestHandler - handle503?: ErrorRequestHandler - handle504?: ErrorRequestHandler - handle505?: ErrorRequestHandler - handle506?: ErrorRequestHandler - handle507?: ErrorRequestHandler - handle508?: ErrorRequestHandler - handle510?: ErrorRequestHandler - handle511?: ErrorRequestHandler - handleError?: ErrorRequestHandler + handle400?: ErrorRequestHandler; + handle401?: ErrorRequestHandler; + handle402?: ErrorRequestHandler; + handle403?: ErrorRequestHandler; + handle404?: ErrorRequestHandler; + handle405?: ErrorRequestHandler; + handle406?: ErrorRequestHandler; + handle407?: ErrorRequestHandler; + handle408?: ErrorRequestHandler; + handle409?: ErrorRequestHandler; + handle410?: ErrorRequestHandler; + handle411?: ErrorRequestHandler; + handle412?: ErrorRequestHandler; + handle413?: ErrorRequestHandler; + handle414?: ErrorRequestHandler; + handle415?: ErrorRequestHandler; + handle416?: ErrorRequestHandler; + handle417?: ErrorRequestHandler; + handle418?: ErrorRequestHandler; + handle421?: ErrorRequestHandler; + handle422?: ErrorRequestHandler; + handle423?: ErrorRequestHandler; + handle424?: ErrorRequestHandler; + handle425?: ErrorRequestHandler; + handle426?: ErrorRequestHandler; + handle428?: ErrorRequestHandler; + handle429?: ErrorRequestHandler; + handle431?: ErrorRequestHandler; + handle451?: ErrorRequestHandler; + handle500?: ErrorRequestHandler; + handle501?: ErrorRequestHandler; + handle502?: ErrorRequestHandler; + handle503?: ErrorRequestHandler; + handle504?: ErrorRequestHandler; + handle505?: ErrorRequestHandler; + handle506?: ErrorRequestHandler; + handle507?: ErrorRequestHandler; + handle508?: ErrorRequestHandler; + handle510?: ErrorRequestHandler; + handle511?: ErrorRequestHandler; + handleError?: ErrorRequestHandler; } export interface IdProviderController extends Controller { - autoLogin?: RequestHandler - handle401?: RequestHandler - login?: RequestHandler - logout?: RequestHandler + autoLogin?: RequestHandler; + handle401?: RequestHandler; + login?: RequestHandler; + logout?: RequestHandler; } export interface HttpFilterController< @@ -472,7 +467,7 @@ export interface ResponseProcessorController< ResponseFromJava extends MappedResponse = MappedResponse, ResponseToJava extends ResponseInterface = Partial > { - responseProcessor: (request: RequestFromJava, response: ResponseFromJava) => ResponseToJava + responseProcessor: (request: RequestFromJava, response: ResponseFromJava) => ResponseToJava; } export type UserKey = `user:${string}:${string}`; diff --git a/modules/lib/test/Controller.test-d.ts b/modules/lib/test/Controller.test-d.ts index 914be428669..65983f88aa7 100644 --- a/modules/lib/test/Controller.test-d.ts +++ b/modules/lib/test/Controller.test-d.ts @@ -6,18 +6,11 @@ import type { RequestMode, RequestScheme, Response, + WebSocketEventHandler, } from '../core/index'; import { expectAssignable, - // expectDeprecated, - // expectDocCommentIncludes, - // expectError, - // expectNever, expectNotAssignable, - // expectNotDeprecated, - // expectNotType, - // expectType, - // printType, } from 'tsd'; const log = { @@ -31,6 +24,12 @@ const myController = { status: 200, }; }, + connect: (request: Request) => { + log.info('connect request:%s', JSON.stringify(request, null, 4)); + return { + status: 200, + }; + }, get: (request: Request) => { log.info('get request:%s', JSON.stringify(request, null, 4)); return { @@ -43,12 +42,24 @@ const myController = { status: 200, }; }, + patch: (request: Request) => { + log.info('patch request:%s', JSON.stringify(request, null, 4)); + return { + status: 200, + }; + }, post: (request: Request) => { log.info('post request:%s', JSON.stringify(request, null, 4)); return { status: 200, }; }, + trace: (request: Request) => { + log.info('trace request:%s', JSON.stringify(request, null, 4)); + return { + status: 200, + }; + }, }; expectAssignable(myController); @@ -71,12 +82,16 @@ type PageRequestHandler = (request: PageRequest) => Response; interface PageController { all?: PageRequestHandler + connect?: PageRequestHandler delete?: PageRequestHandler get?: PageRequestHandler head?: PageRequestHandler options?: PageRequestHandler + patch?: PageRequestHandler post?: PageRequestHandler put?: PageRequestHandler + trace?: PageRequestHandler + webSocketEvent?: WebSocketEventHandler } const pageRequest /* : PageRequest */ = { diff --git a/modules/lib/test/ErrorController.test-d.ts b/modules/lib/test/ErrorController.test-d.ts index 6c1acff0eb1..c37560f7abe 100644 --- a/modules/lib/test/ErrorController.test-d.ts +++ b/modules/lib/test/ErrorController.test-d.ts @@ -3,19 +3,7 @@ import type { ErrorRequest, } from '../core/index'; -import { - expectAssignable, - // expectDeprecated, - // expectDocCommentIncludes, - // expectError, - // expectNever, - // expectNotAssignable, - // printType, - // expectNotDeprecated, - // expectNotType, - // expectType, - // printType, -} from 'tsd'; +import {expectAssignable} from 'tsd'; const log = { info: (message?: string, ...optionalParams: unknown[]) => { /* no-op */ }, diff --git a/modules/lib/test/HttpFilter.test-d.ts b/modules/lib/test/HttpFilter.test-d.ts index 9f68f49a101..12dda8b8909 100644 --- a/modules/lib/test/HttpFilter.test-d.ts +++ b/modules/lib/test/HttpFilter.test-d.ts @@ -7,19 +7,7 @@ import type { SerializableRequest, } from '../core/index'; -import { - expectAssignable, - // expectDeprecated, - // expectDocCommentIncludes, - // expectError, - // expectNever, - // expectNotAssignable, - // printType, - // expectNotDeprecated, - // expectNotType, - // expectType, - // printType, -} from 'tsd'; +import {expectAssignable} from 'tsd'; const log = { info: (message?: string, ...optionalParams: unknown[]) => { /* no-op */ }, diff --git a/modules/lib/test/IdProviderController.test-d.ts b/modules/lib/test/IdProviderController.test-d.ts index 8f2e0adbbba..8f10d26d80c 100644 --- a/modules/lib/test/IdProviderController.test-d.ts +++ b/modules/lib/test/IdProviderController.test-d.ts @@ -3,19 +3,7 @@ import type { Request, } from '../core/index'; -import { - expectAssignable, - // expectDeprecated, - // expectDocCommentIncludes, - // expectError, - // expectNever, - // expectNotAssignable, - // printType, - // expectNotDeprecated, - // expectNotType, - // expectType, - // printType, -} from 'tsd'; +import {expectAssignable} from 'tsd'; const log = { info: (message?: string, ...optionalParams: unknown[]) => { /* no-op */ }, diff --git a/modules/lib/test/Request.test-d.ts b/modules/lib/test/Request.test-d.ts index d96a14d99f2..21679eb417c 100644 --- a/modules/lib/test/Request.test-d.ts +++ b/modules/lib/test/Request.test-d.ts @@ -5,15 +5,7 @@ import {RequestImplementation} from './RequestImplementation'; import { expectAssignable, - // expectDeprecated, - // expectDocCommentIncludes, - // expectError, - // expectNever, expectNotAssignable, - // expectNotDeprecated, - // expectNotType, - // expectType, - // printType, } from 'tsd'; // Scenario: When implementing a idprovider login function the Request may have a validTicket property @@ -738,10 +730,10 @@ expectAssignable({ branch: 'string but not draft|master', }); -// LiteralUnion suggests 'GET'|'POST'|'PUT'|'DELETE'|'HEAD'|'OPTIONS', but allows string +// LiteralUnion suggests 'GET'|'POST'|'PUT'|'DELETE'|'HEAD'|'OPTIONS'|'PATCH'|'TRACE'|'CONNECT', but allows string expectAssignable({ ...requiredProperties, - method: 'string but not GET|POST|PUT|DELETE|HEAD|OPTIONS', + method: 'string but not GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|TRACE|CONNECT', }); // LiteralUnion suggests 'edit'|'inline'|'live'|'preview'|'admin', but allows string diff --git a/modules/lib/test/RequestImplementation.ts b/modules/lib/test/RequestImplementation.ts index fb350bef73f..2a2fbe65b58 100644 --- a/modules/lib/test/RequestImplementation.ts +++ b/modules/lib/test/RequestImplementation.ts @@ -19,22 +19,22 @@ export function toStr( const isObject = (value: object | unknown): value is object => Object.prototype.toString.call(value).slice(8,-1) === 'Object'; -function mapKeys( - obj: object, +function mapKeys>( + obj: T, fn: ({ key, result, value, }: { key: PropertyKey - result: object + result: T value: unknown }) => void, -): object { +): T { if (!isObject(obj)) { throw new TypeError(`mapKeys: First param must be an object! got:${toStr(obj)}`); } - const result = {}; + const result = {} as T; const keys = Object.keys(obj); for (let i = 0; i < keys.length; i++) { const key = keys[i]; @@ -53,8 +53,8 @@ function lcKeys>(obj: T): T { result, value, }) => { - result[String(key).toLowerCase()] = value; - }) as T; + (result as Record)[String(key).toLowerCase()] = value; + }); } export class RequestImplementation implements Request { diff --git a/modules/lib/test/Response.test-d.ts b/modules/lib/test/Response.test-d.ts index 1d76dc7c525..ece418332ac 100644 --- a/modules/lib/test/Response.test-d.ts +++ b/modules/lib/test/Response.test-d.ts @@ -6,16 +6,7 @@ import type { import { expectAssignable, - // expectDeprecated, - // expectDocCommentIncludes, - // expectError, - // expectNever, expectNotAssignable, - // printType, - // expectNotDeprecated, - // expectNotType, - // expectType, - // printType, } from 'tsd'; diff --git a/modules/lib/test/ResponseProcessor.test-d.ts b/modules/lib/test/ResponseProcessor.test-d.ts index 5e5e43ddd86..f7b8c5ad113 100644 --- a/modules/lib/test/ResponseProcessor.test-d.ts +++ b/modules/lib/test/ResponseProcessor.test-d.ts @@ -7,16 +7,6 @@ import type { import { expectAssignable, - // expectDeprecated, - // expectDocCommentIncludes, - // expectError, - // expectNever, - // expectNotAssignable, - // printType, - // expectNotDeprecated, - // expectNotType, - // expectType, - // printType, } from 'tsd'; // const log = { From 7c2bf7fb50181cafe46b97dd906dec932da44ee7 Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Fri, 18 Oct 2024 08:25:39 +0200 Subject: [PATCH 16/19] Cleanup --- modules/lib/test/Response.test-d.ts | 4 ---- modules/lib/test/tsconfig.json | 5 ----- 2 files changed, 9 deletions(-) delete mode 100644 modules/lib/test/tsconfig.json diff --git a/modules/lib/test/Response.test-d.ts b/modules/lib/test/Response.test-d.ts index ece418332ac..ebe48301e2e 100644 --- a/modules/lib/test/Response.test-d.ts +++ b/modules/lib/test/Response.test-d.ts @@ -142,7 +142,3 @@ expectNotAssignable({ expectNotAssignable({ status: '200', // Not number! }); - -// ──────────────────────────────────────────────────────────────────────────── -// Scenario: Response processor -// ──────────────────────────────────────────────────────────────────────────── \ No newline at end of file diff --git a/modules/lib/test/tsconfig.json b/modules/lib/test/tsconfig.json deleted file mode 100644 index 3d3d1aba7ad..00000000000 --- a/modules/lib/test/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "compilerOptions": { - "lib": ["DOM"] - } -} \ No newline at end of file From e24726dee8be9ce0b53291df8750819be385aa17 Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Fri, 18 Oct 2024 09:19:17 +0200 Subject: [PATCH 17/19] Add generics to Controller --- modules/lib/core/index.d.ts | 25 ++-- modules/lib/test/Controller.test-d.ts | 148 +------------------ modules/lib/test/PageController.test-d.ts | 168 ++++++++++++++++++++++ 3 files changed, 183 insertions(+), 158 deletions(-) create mode 100644 modules/lib/test/PageController.test-d.ts diff --git a/modules/lib/core/index.d.ts b/modules/lib/core/index.d.ts index 4a7c0b6d950..db72a0627be 100644 --- a/modules/lib/core/index.d.ts +++ b/modules/lib/core/index.d.ts @@ -371,17 +371,20 @@ export interface WebSocketEvent { type WebSocketEventHandler> = (event: WebSocketEvent) => void; -export interface Controller { - all?: RequestHandler; - connect?: RequestHandler; - delete?: RequestHandler; - get?: RequestHandler; - head?: RequestHandler; - options?: RequestHandler; - patch?: RequestHandler; - post?: RequestHandler; - put?: RequestHandler; - trace?: RequestHandler; +export interface Controller< + Request extends RequestInterface = DefaultRequest, + Response extends ResponseInterface = DefaultResponse, +> { + all?: RequestHandler; + connect?: RequestHandler; + delete?: RequestHandler; + get?: RequestHandler; + head?: RequestHandler; + options?: RequestHandler; + patch?: RequestHandler; + post?: RequestHandler; + put?: RequestHandler; + trace?: RequestHandler; webSocketEvent?: WebSocketEventHandler; } diff --git a/modules/lib/test/Controller.test-d.ts b/modules/lib/test/Controller.test-d.ts index 65983f88aa7..c6a5774f275 100644 --- a/modules/lib/test/Controller.test-d.ts +++ b/modules/lib/test/Controller.test-d.ts @@ -1,17 +1,8 @@ import type { Controller, Request, - RequestBranch, - RequestMethod, - RequestMode, - RequestScheme, - Response, - WebSocketEventHandler, } from '../core/index'; -import { - expectAssignable, - expectNotAssignable, -} from 'tsd'; +import {expectAssignable} from 'tsd'; const log = { info: (message?: string, ...optionalParams: string[]) => { /* no-op */ }, @@ -63,140 +54,3 @@ const myController = { }; expectAssignable(myController); - -type PageRequest = Omit, - // Omit/Disallow some optional properties - 'contextPath' | 'validTicket' ->; -type PageRequestHandler = (request: PageRequest) => Response; - -interface PageController { - all?: PageRequestHandler - connect?: PageRequestHandler - delete?: PageRequestHandler - get?: PageRequestHandler - head?: PageRequestHandler - options?: PageRequestHandler - patch?: PageRequestHandler - post?: PageRequestHandler - put?: PageRequestHandler - trace?: PageRequestHandler - webSocketEvent?: WebSocketEventHandler -} - -const pageRequest /* : PageRequest */ = { - method: 'GET' as PageRequest['method'], // Avoid flattening down to string - scheme: 'http' as PageRequest['scheme'], // Avoid flattening down to string - mode: 'preview' as PageRequest['mode'], // Avoid flattening down to string - branch: 'draft' as PageRequest['branch'], // Avoid flattening down to string - host: 'localhost', - port: 8080, - path: '/admin/site/preview/my-project/draft/my-site', - rawPath: '/admin/site/preview/my-project/draft/my-site', - url: 'http://localhost:8080/admin/site/preview/my-project/draft/my-site', - remoteAddress: '127.0.0.1', - webSocket: false, - repositoryId: 'com.enonic.cms.my-project', - params: {}, - headers: { - Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - 'Accept-Encoding': 'gzip, deflate, br, zstd', - 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', - Connection: 'keep-alive', - Cookie: 'app.browse.RecentItemsList=portal%3Asite; JSESSIONID=19g9dxfzwnyqo1i6ufom5ksuhg0', - Host: 'localhost:8080', - Referer: 'http://localhost:8080/admin/tool/com.enonic.app.contentstudio/main', - 'sec-ch-ua': 'Chromium;v=128, Not;A=Brand;v=24, Google Chrome;v=128', - 'sec-ch-ua-mobile': '?0', - 'sec-ch-ua-platform': 'macOS', - 'Sec-Fetch-Dest': 'document', - 'Sec-Fetch-Mode': 'navigate', - 'Sec-Fetch-Site': 'same-origin', - 'Upgrade-Insecure-Requests': '1', - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', - } as Record, - getHeader: (header: string): string => { - return pageRequest.headers[header]; - }, - cookies: { - 'app.browse.RecentItemsList': 'portal%3Asite', - JSESSIONID: '19g9dxfzwnyqo1i6ufom5ksuhg0', - }, -}; - -expectAssignable(pageRequest); - -expectNotAssignable({ - ...pageRequest, - contextPath: '/contextPath', -}); - -expectNotAssignable({ - ...pageRequest, - validTicket: true, -}); - -expectNotAssignable({ - ...pageRequest, - branch: 'stringButNotMatchingLiterals', -}); - -expectNotAssignable({ - ...pageRequest, - method: 'stringButNotMatchingLiterals', -}); - -expectNotAssignable({ - ...pageRequest, - mode: 'stringButNotMatchingLiterals', -}); - -expectNotAssignable({ - ...pageRequest, - scheme: 'stringButNotMatchingLiterals', -}); - -expectNotAssignable({ - ...pageRequest, - cookies: undefined, -}); - -expectNotAssignable({ - ...pageRequest, - headers: undefined, -}); - -expectNotAssignable({ - ...pageRequest, - params: undefined, -}); - -expectNotAssignable({ - ...pageRequest, - rawPath: undefined, -}); - -expectNotAssignable({ - ...pageRequest, - remoteAddress: undefined, -}); - -expectNotAssignable({ - ...pageRequest, - repositoryId: undefined, -}); - -expectNotAssignable({ - ...pageRequest, - webSocket: undefined, -}); \ No newline at end of file diff --git a/modules/lib/test/PageController.test-d.ts b/modules/lib/test/PageController.test-d.ts new file mode 100644 index 00000000000..0a4b79b77fb --- /dev/null +++ b/modules/lib/test/PageController.test-d.ts @@ -0,0 +1,168 @@ +import type { + Controller, + Request, + RequestBranch, + RequestHeaders, + RequestMethod, + RequestMode, + RequestScheme, +} from '../core/index'; + +import { + expectAssignable, + expectNotAssignable, +} from 'tsd'; + + +type PageRequest = Omit, + // Omit/Disallow some optional properties + 'contextPath' | 'validTicket' +>; + +const pageRequest = { + method: 'GET', + scheme: 'http', + mode: 'preview', + branch: 'draft', + + host: 'localhost', + port: 8080, + path: '/admin/site/preview/my-project/draft/my-site', + rawPath: '/admin/site/preview/my-project/draft/my-site', + url: 'http://localhost:8080/admin/site/preview/my-project/draft/my-site', + remoteAddress: '127.0.0.1', + webSocket: false, + repositoryId: 'com.enonic.cms.my-project', + params: {}, + headers: { + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,no;q=0.7', + Connection: 'keep-alive', + Cookie: 'app.browse.RecentItemsList=portal%3Asite; JSESSIONID=19g9dxfzwnyqo1i6ufom5ksuhg0', + Host: 'localhost:8080', + Referer: 'http://localhost:8080/admin/tool/com.enonic.app.contentstudio/main', + 'sec-ch-ua': 'Chromium;v=128, Not;A=Brand;v=24, Google Chrome;v=128', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': 'macOS', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'same-origin', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + getHeader: (header: string): string | null => { + // When you just create an object, without declaring it's type, the type of the object is "autogenerated". + // The autogenerated type has named properties rather than indexed properties. + // If you later try to access a property using an index, it causes type errors, since "named" properties are not "indexed". + // You can fix this either by casting to a type with an index signature, either upon creation, or on each usage. + // Here the latter is done: + const value = (pageRequest.headers as RequestHeaders)[header]; + if (value === undefined) { + return null; + } + return value; + }, + cookies: { + 'app.browse.RecentItemsList': 'portal%3Asite', + JSESSIONID: '19g9dxfzwnyqo1i6ufom5ksuhg0', + }, +} as +// When you just create an object, without declaring it's type, the type of the object is "autogenerated". +// For properties that have literal string values, they are simply flattened down to string. +// Since the value of some of the properties of PageRequest are limitied to literal string, +// that causes a mismatch when comparing the autogenerated type with the PageRequest type. +// Just adding as const to the whole object avoids the flattening down to string. +const; + +expectAssignable(pageRequest); + +expectNotAssignable({ + ...pageRequest, + contextPath: '/contextPath', +}); + +expectNotAssignable({ + ...pageRequest, + validTicket: true, +}); + +expectNotAssignable({ + ...pageRequest, + branch: 'stringButNotMatchingLiterals', +}); + +expectNotAssignable({ + ...pageRequest, + method: 'stringButNotMatchingLiterals', +}); + +expectNotAssignable({ + ...pageRequest, + mode: 'stringButNotMatchingLiterals', +}); + +expectNotAssignable({ + ...pageRequest, + scheme: 'stringButNotMatchingLiterals', +}); + +expectNotAssignable({ + ...pageRequest, + cookies: undefined, +}); + +expectNotAssignable({ + ...pageRequest, + headers: undefined, +}); + +expectNotAssignable({ + ...pageRequest, + params: undefined, +}); + +expectNotAssignable({ + ...pageRequest, + rawPath: undefined, +}); + +expectNotAssignable({ + ...pageRequest, + remoteAddress: undefined, +}); + +expectNotAssignable({ + ...pageRequest, + repositoryId: undefined, +}); + +expectNotAssignable({ + ...pageRequest, + webSocket: undefined, +}); + +type PageController = Controller; + +const log = { + info: (message?: string, ...optionalParams: string[]) => { /* no-op */ }, +}; + +expectAssignable({ + get: (request: PageRequest) => { + log.info('get request:%s', JSON.stringify(request, null, 4)); + return { + status: 200, + }; + }, +}); \ No newline at end of file From dc5011cdba0fdbef39cb97d7cdb612645bb99dab Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Fri, 18 Oct 2024 10:25:45 +0200 Subject: [PATCH 18/19] Add some autocompletion to Sec-Fetch-* headers --- modules/lib/core/index.d.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/modules/lib/core/index.d.ts b/modules/lib/core/index.d.ts index db72a0627be..229b2f99c2d 100644 --- a/modules/lib/core/index.d.ts +++ b/modules/lib/core/index.d.ts @@ -225,6 +225,10 @@ export interface DefaultRequestCookies extends RequestCookies { JSESSIONID?: string; } +export type SecFetchDest = 'audio' | 'audioworklet' | 'document' | 'embed' | 'empty' | 'fencedframe' | 'font' | 'frame' | 'iframe' | 'image' | 'manifest' | 'object' | 'paintworklet' | 'report' | 'script' | 'serviceworker' | 'sharedworker' | 'style' | 'track' | 'video' | 'webidentity' | 'worker' | 'xslt'; +export type SecFetchMode = 'cors'| 'navigate' | 'no-cors' | 'same-origin' | 'websocket'; +export type SecFetchSite = 'cross-site' | 'same-origin' | 'same-site' | 'none'; + export interface DefaultRequestHeaders extends RequestHeaders { Accept?: string; 'Accept-Charset'?: string; @@ -236,19 +240,18 @@ export interface DefaultRequestHeaders extends RequestHeaders { 'Content-Length'?: string; 'Content-Type'?: string; Cookie?: string; - Language?: string; // Not mentioned here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers Host?: string; 'If-None-Match'?: string; Referer?: string; - 'Sec-Fetch-Dest'?: string; - 'Sec-Fetch-Mode'?: string; - 'Sec-Fetch-Site'?: string; - 'Sec-Fetch-User'?: string; - 'Upgrade-Insecure-Requests'?: string; + 'Sec-Fetch-Dest'?: LiteralUnion; + 'Sec-Fetch-Mode'?: LiteralUnion; + 'Sec-Fetch-Site'?: LiteralUnion + 'Sec-Fetch-User'?: '?1'; + 'Upgrade-Insecure-Requests'?: '1'; 'User-Agent'?: string; 'X-Forwarded-For'?: string; 'X-Forwarded-Host'?: string; - 'X-Forwarded-Proto'?: string; + 'X-Forwarded-Proto'?: LiteralUnion; 'X-Forwarded-Server'?: string; } From 7b1f13b3dc72fe7f9283c9de915563a7fe50b72a Mon Sep 17 00:00:00 2001 From: Christian Westgaard Date: Fri, 18 Oct 2024 10:32:14 +0200 Subject: [PATCH 19/19] Both Chrome and Firefox sends the Priority Request Header --- modules/lib/core/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/lib/core/index.d.ts b/modules/lib/core/index.d.ts index 229b2f99c2d..bde4ace4f6e 100644 --- a/modules/lib/core/index.d.ts +++ b/modules/lib/core/index.d.ts @@ -242,6 +242,7 @@ export interface DefaultRequestHeaders extends RequestHeaders { Cookie?: string; Host?: string; 'If-None-Match'?: string; + Priority?: string; Referer?: string; 'Sec-Fetch-Dest'?: LiteralUnion; 'Sec-Fetch-Mode'?: LiteralUnion;