diff --git a/apps/back-end/src/routes.ts b/apps/back-end/src/routes.ts index afed75c1..40eaa87d 100644 --- a/apps/back-end/src/routes.ts +++ b/apps/back-end/src/routes.ts @@ -137,6 +137,18 @@ export function MykomapRouter( return reply; }, + async getConfig({ params: { datasetId }, request, reply }) { + // Validate the parameters some more + + if (!sendJson(request, reply, filePath("datasets", datasetId, "config"))) + throw new TsRestResponseError(contract.getDataset, { + status: 404, + body: { message: `unknown datasetId '${datasetId}'` }, + }); + + return reply; + }, + async getVersion(req) { return { body: __BUILD_INFO__, diff --git a/apps/back-end/test/data/config/dataset-A/config.json b/apps/back-end/test/data/config/dataset-A/config.json deleted file mode 100644 index d12eeadc..00000000 --- a/apps/back-end/test/data/config/dataset-A/config.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "prefixes": { - "https://dev.lod.coop/essglobal/2.1/standard/activities-ica/": "aci", - "https://dev.lod.coop/essglobal/2.1/standard/base-membership-type/": "bmt", - "https://dev.lod.coop/essglobal/2.1/standard/organisational-structure/": "os" - }, - "languages": ["en", "fr"], - "fields": { - "uid": "value", - "name": "value", - "description": "value", - "website": { - "type": "multi", - "of": { - "type": "value" - } - }, - "dc_domains": { - "type": "multi", - "of": { "type": "value" } - }, - "country_id": { - "type": "vocab", - "uri": "coun" - }, - "primary_activity": { - "type": "vocab", - "uri": "aci", - "titleUri": "ui:primary_activity" - }, - "organisational_structure": { - "type": "vocab", - "uri": "os", - "titleUri": "ui:organisational_structure" - }, - "typology": { - "type": "vocab", - "uri": "bmt", - "titleUri": "ui:typology" - }, - "latitude": "value", - "longitude": "value", - "geocontainer_lat": "value", - "geocontainer_lon": "value", - "geocoded_addr": "value", - "data_sources": { - "type": "multi", - "of": { "type": "vocab", "uri": "dso" } - } - }, - "vocabs": { - "aci": { - "en": { - "title": "Economic Activity", - "terms": { - "ICA210": "Housing", - "ICA220": "Transport", - "ICA230": "Utilities" - } - }, - "fr": { - "title": "Secteur économique", - "terms": { - "ICA210": "Logement", - "ICA220": "Transports", - "ICA230": "Services publics" - } - } - }, - "bmt": { - "en": { - "title": "Typology", - "terms": { - "BMT10": "Consumer/Users", - "BMT20": "Producers", - "BMT30": "Workers" - } - }, - "fr": { - "title": "Typologie", - "terms": { - "BMT10": "Consommateurs/usagers", - "BMT20": "Producteurs", - "BMT30": "Travailleurs" - } - } - }, - "os": { - "en": { - "title": "Structure Type", - "terms": { - "OS60": "Workers cooperative", - "OS80": "Consumer/User coops", - "OS90": "Producer cooperative" - } - }, - "fr": { - "title": "Type de structure", - "terms": { - "OS50": "Entreprise (autre)", - "OS60": "Coopératives de travail associé", - "OS80": "Coopératives de consommateurs", - "OS90": "Coopératives de producteurs" - } - } - }, - "coun": { - "en": { - "title": "Country", - "terms": { - "GB": "United Kingdom", - "FR": "France" - } - }, - "fr": { - "title": "Pays", - "terms": { - "GB": "Royaume-Uni", - "FR": "France" - } - } - }, - "dso": { - "en": { - "title": "Data Source", - "terms": { - "CUK": "Co-operatives UK", - "DC": "DotCoop", - "ICA": "International Cooperative Alliance", - "NCBA": "National Cooperative Business Association (USA)" - } - }, - "fr": { - "title": "Source de données", - "terms": { - "CUK": "Co-operatives UK", - "DC": "DotCoop", - "ICA": "Alliance coopérative internationale", - "NCBA": "National Cooperative Business Association (USA)" - } - } - }, - "ui": { - "en": { - "primary_activity": "Primary Activity", - "organisational_structure": "Organisational Structure", - "typology": "Typology" - }, - "fr": { - "primary_activity": "Activité principale", - "organisational_structure": "Structure organisationnelle", - "typology": "Typologie" - } - } - } -} diff --git a/apps/back-end/test/data/datasets/dataset-A/initiatives/0.json b/apps/back-end/test/data/datasets/dataset-A/initiatives/0.json deleted file mode 100644 index 37052eeb..00000000 --- a/apps/back-end/test/data/datasets/dataset-A/initiatives/0.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "uid": "test/cuk/R000001", - "name": "Apples Co-op", - "description": "We sell apples", - "website": "https://apples.coop", - "dc_domains": ["apples.coop", "orchards.coop"], - "country_id": "coun:GB", - "primary_activity": "aci:ICA210", - "organisational_structure": "os:OS60", - "typology": "bmt:BMT20", - "latitude": 51.507476, - "longitude": -0.127825, - "geocontainer_lat": 51.50747643, - "geocontainer_lon": -0.12782464, - "geocoded_addr": "1 West Street, Sheffield, S1 2AB, UK", - "data_sources": ["dso:DC", "dso:CUK"] -} diff --git a/apps/back-end/test/data/datasets/dataset-A/initiatives/1.json b/apps/back-end/test/data/datasets/dataset-A/initiatives/1.json deleted file mode 100644 index bccecf94..00000000 --- a/apps/back-end/test/data/datasets/dataset-A/initiatives/1.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "uid": "test/cuk/R000002", - "name": "Pears United", - "description": "We sell pears", - "website": "https://pears.coop", - "dc_domains": ["pears.coop", "pearsunited.coop"], - "country_id": "coun:FR", - "primary_activity": "aci:ICA230", - "organisational_structure": "os:OS90", - "typology": "bmt:BMT30", - "latitude": 50.850452, - "longitude": 0.924728, - "geocontainer_lat": 50.85045216, - "geocontainer_lon": 0.92472819, - "geocoded_addr": "79 rue de la Mare aux Carats, 34090 Montpellier, France", - "data_sources": ["dso:DC"] -} diff --git a/apps/back-end/test/data/datasets/dataset-A/locations.json b/apps/back-end/test/data/datasets/dataset-A/locations.json deleted file mode 100644 index e8996e0f..00000000 --- a/apps/back-end/test/data/datasets/dataset-A/locations.json +++ /dev/null @@ -1,4 +0,0 @@ -[ - [-0.12783, 51.50748], - [0.92473, 50.85045] -] diff --git a/apps/back-end/test/data/datasets/dataset-A/searchable.json b/apps/back-end/test/data/datasets/dataset-A/searchable.json deleted file mode 100644 index 20ca7235..00000000 --- a/apps/back-end/test/data/datasets/dataset-A/searchable.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "country_id": "coun:GB", - "primary_activity": "aci:ICA210", - "organisational_structure": "os:OS60", - "typology": "bmt:BMT20", - "searchString": "test co-op 1 west street sheffield s1 2ab uk test.coop" - }, - { - "country_id": "coun:FR", - "primary_activity": "aci:ICA230", - "organisational_structure": "os:OS90", - "typology": "bmt:BMT30", - "searchString": "pears united 79 rue de la mare aux carats 34090 montpellier france pears.coop" - } -] diff --git a/apps/back-end/test/data/datasets/test-A.json b/apps/back-end/test/data/datasets/test-A.json new file mode 100644 index 00000000..70aaa390 --- /dev/null +++ b/apps/back-end/test/data/datasets/test-A.json @@ -0,0 +1 @@ +[[1,2],[3,2],[5,2],["a",3]] diff --git a/apps/back-end/test/data/datasets/test-A/config.json b/apps/back-end/test/data/datasets/test-A/config.json new file mode 100644 index 00000000..e116519f --- /dev/null +++ b/apps/back-end/test/data/datasets/test-A/config.json @@ -0,0 +1,30 @@ +{ + "prefixes": { + "https://dev.lod.coop/essglobal/2.1/standard/activities-modified/": "am" + }, + "vocabs": { + "am": { + "en": { + "title": "Activities (Modified)", + "terms": { + "AM10": "Arts, Media, Culture & Leisure", + "AM20": "Campaigning, Activism & Advocacy", + "AM30": "Community & Collective Spaces", + "AM40": "Education", + "AM50": "Energy", + "AM60": "Food", + "AM70": "Goods & Services", + "AM80": "Health, Social Care & Wellbeing", + "AM90": "Housing", + "AM100": "Money & Finance", + "AM110": "Nature, Conservation & Environment", + "AM120": "Reduce, Reuse, Repair & Recycle", + "AM130": "Agriculture", + "AM140": "Industry", + "AM150": "Utilities", + "AM160": "Transport" + } + } + } + } +} diff --git a/apps/back-end/test/data/datasets/test-A/items/0.json b/apps/back-end/test/data/datasets/test-A/items/0.json new file mode 100644 index 00000000..d1f40b48 --- /dev/null +++ b/apps/back-end/test/data/datasets/test-A/items/0.json @@ -0,0 +1 @@ +{"id":0, "a":"a:foo", "b":"b:bar", "desc": "humbug"} diff --git a/apps/back-end/test/data/datasets/test-A/search/a:foo/b:bar/text.json b/apps/back-end/test/data/datasets/test-A/search/a:foo/b:bar/text.json new file mode 100644 index 00000000..22fd8752 --- /dev/null +++ b/apps/back-end/test/data/datasets/test-A/search/a:foo/b:bar/text.json @@ -0,0 +1 @@ +[2] diff --git a/apps/back-end/test/data/datasets/test-A/search/a:foo/text.json b/apps/back-end/test/data/datasets/test-A/search/a:foo/text.json new file mode 100644 index 00000000..7660873d --- /dev/null +++ b/apps/back-end/test/data/datasets/test-A/search/a:foo/text.json @@ -0,0 +1 @@ +[1] diff --git a/apps/back-end/test/validation.test.ts b/apps/back-end/test/validation.test.ts deleted file mode 100644 index 3fba5dd0..00000000 --- a/apps/back-end/test/validation.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -/// - -import { expect, test } from "vitest"; -import { schemas } from "@mykomap/common"; - -const { DatasetId, QName } = schemas; - -test("testing DatasetId validation", async (t) => { - const expectTrue = ["0", "A", "z", "_", "-", "01234", "Quick-Brown-Fox_42"]; - const expectFalse = ["", " ", "/", "?", "&", ":", ".", "="]; - expectTrue.forEach((it) => - expect(DatasetId.safeParse(it).success, `parsing '${it}'`).toBeTruthy(), - ); - expectFalse.forEach((it) => - expect(DatasetId.safeParse(it).success, `parsing '${it}'`).toBeFalsy(), - ); -}); - -test("testing QName validation", async (t) => { - const expectTrue = ["a:b", "a1:b1", "_:_", "_1-.:_1-."]; - const expectFalse = [ - "", - ":", - "a:", - ":a", - "_", - "-", - ".", - "&", - ";", - "/", - "1:", - ":1", - "a:1", - "1:a", - "-:-", - ]; - expectTrue.forEach((it) => - expect(QName.safeParse(it).success, `parsing '${it}'`).toBeTruthy(), - ); - expectFalse.forEach((it) => - expect(QName.safeParse(it).success, `parsing '${it}'`).toBeFalsy(), - ); -}); diff --git a/libs/common/package.json b/libs/common/package.json index e46efd64..24a3b04d 100644 --- a/libs/common/package.json +++ b/libs/common/package.json @@ -13,11 +13,12 @@ "prebuild": "npm run generate-openapi", "build": "vite build", "generate-openapi": "npx vite-node src/api/generate-openapi.ts", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "vitest run" }, "dependencies": { "@ts-rest/core": "^3.51.0", "@ts-rest/open-api": "^3.51.0", + "glob": "^11.0.0", "zod": "^3.23.8" }, "devDependencies": { @@ -26,6 +27,7 @@ "typescript": "^5.5.4", "vite": "^5.4.3", "vite-node": "^2.0.5", - "vite-plugin-dts": "^4.1.0" + "vite-plugin-dts": "^4.1.0", + "vitest": "^2.1.4" } } diff --git a/libs/common/src/api/contract.ts b/libs/common/src/api/contract.ts index 7ef8b772..3240ebb2 100644 --- a/libs/common/src/api/contract.ts +++ b/libs/common/src/api/contract.ts @@ -1,73 +1,55 @@ import { initContract } from "@ts-rest/core"; import { z } from "zod"; import { extendZodWithOpenApi } from "@anatine/zod-openapi"; +import * as Rx from "../rxdefs.js"; +import RxUtils from "../rxutils.js"; +import { Iso639Set1Codes } from "../iso639-1.js"; extendZodWithOpenApi(z); const c = initContract(); -/** A regex testing for an *URL-safe* base64 string (RFC4648 sect 5) */ -const UrlSafeBase64Rx = /^[A-Z0-9_-]+$/i; - -/** A string regular expression matching an XML NCName - * - * - An NCName is an XML Name, but with no colons allowed. - * - An XML Name is a complicated beast, but loosely a Unicode version of \w, - * or in other words, a unicode alphanumeric symbolic identifier. - * - It can contain digits, letters, hyphens, periods and underscores and - * certain unicode equivalents. - * - But it must not start with digits, a hyphen or a period, nor certain - * unicode equivalents. - * - * The regex for an XML Name is adapted from O'Reilly Regex Cookbook section 8.4, - * "XML 1.0 names (approximate)" - but the colon is removed. - * - * Paraphrasing that book's explanation: - * - the name start character can be a [:_] or - * - any of the following Unicode categories: - * - Lowercase letter (Ll) - * - Uppercase letter (Lu) - * - Titlecase letter (Lt) - * - Letter without case (Lo) - * - Letter number (Nl) - * - subsequent characters can also include [.-] or - * - Mark (M) - * - Modifier letter (Lm) - * - Decimal digit (Nd) +/** Helper function to generate Zod refinements from a RegExp * - * This definition is not compiled, as it is intended for composition below. - * Therefore it is wrapped in a non-capturing group to isolate it without affecting - * the captures which may be defined around it. + * It promotes the RegExp to be a unicode, entire-string match, if it is + * not already. * - * This regex requires node 10+ to be able to use /u and \p. + * It also sets the validation error message attribute from the message parameter. * + * @returns a Zod validator generated by the Zod.string().refine() method */ -const NCName = - "(?:[_\\p{Ll}\\p{Lu}\\p{Lt}\\p{Lo}\\p{Nl}][_.\\p{L}\\p{M}\\p{Nd}\\p{Nl}-]*)"; - -/** Match a QName - * - * Paraphrasing https://en.wikipedia.org/wiki/QName - * - A QName is an NCName (see above) - * - Or two of them delimited by a colon. - * - * Note: for our purposes, we *require* a colon delimiter - it can't just be a NCName. - * This is because we need an abbreviation with which to look up the URL prefix. - * - * FIXME Perhaps we should also disallow common URI scheme prefixes. - * FIXME Maybe we don't care about unicode? - * - * This regex requires node 10+ to be able to use /u and \p. - * - */ -const QNameRx = new RegExp(`^${NCName}[:]${NCName}$`, "gsu"); +function ZodRegex(rx: RegExp, message: string) { + return z.string().refine((v: any) => RxUtils.uaon(rx).test(String(v)), { + message, + }); +} const Location = z.array(z.number()).min(2).max(2); -const DatasetId = z.string().regex(UrlSafeBase64Rx); +const DatasetId = z.string().regex(Rx.UrlSafeBase64); const DatasetItemId = z.coerce.number().int().nonnegative(); const DatasetItem = z.object({}).passthrough(); const Dataset = z.array(Location); -const QName = z.string().regex(QNameRx); +const NCName = ZodRegex(Rx.NCName, "Invalid NCName format"); +const QName = ZodRegex(Rx.QName, "Invalid QName format"); +// Developer note: PrefixUri is regex based, as it attempts to avoid the .url() deficiencies in +// https://github.com/colinhacks/zod/issues/2236. But also our concept of a URI is narrowed, see +// documentation for Rx.PrefixUri. +const PrefixUri = ZodRegex(Rx.PrefixUri, "Invalid prefix URI format"); +const PrefixIndex = z.record(PrefixUri, NCName); +// Zod.enum needs some hand-holding to be happy with using object keys, as it wants a +// guaranteed non-zero length list +const [lang0, ...langs] = Object.keys(Iso639Set1Codes); +const Iso639Set1Code = z.enum([lang0, ...langs]); +const VocabDef = z.object({ + title: z.string(), + terms: z.record(NCName, z.string()), +}); +const I18nVocabDefs = z.record(Iso639Set1Code, VocabDef); +const VocabIndex = z.record(NCName, I18nVocabDefs); +const ConfigData = z.object({ + prefixes: PrefixIndex, + vocabs: VocabIndex, +}); const VersionInfo = z.object({ name: z.string(), buildTime: z.string().datetime({ offset: false }), @@ -79,12 +61,19 @@ const ErrorInfo = z.object({ message: z.string() }).passthrough(); export const schemas = { Location, + ConfigData, DatasetId, DatasetItemId, DatasetItem, Dataset, + I18nVocabDefs, + Iso639Set1Code, + NCName, + PrefixUri, + PrefixIndex, QName, VersionInfo, + VocabIndex, ErrorInfo, }; @@ -176,6 +165,32 @@ export const contract = c.router({ }), }, }, + getConfig: { + method: "GET", + path: "/dataset/:datasetId/config", + summary: "obtain various configured parameters for a map", + description: + "Obtains configured parameters for a map, which amongst other things, " + + "include default values for various options, and definitions of " + + "vocabulary terms with their localised labels, that are used to " + + "interpret identifers in the data and/or elsewhere.", + pathParams: z.object({ + datasetId: DatasetId.openapi({ + // description: "uniquely specifies the dataset wanted", + }), + }), + responses: { + 200: ConfigData.openapi({ + // description: "variuos configured parameters for a map", + }), + 400: ErrorInfo.openapi({ + // description: "bad input parameter", + }), + 404: ErrorInfo.openapi({ + // description: "no such map", + }), + }, + }, getVersion: { method: "GET", path: "/version", diff --git a/libs/common/src/api/mykomap-openapi.json b/libs/common/src/api/mykomap-openapi.json index b5fba42c..7bff9d9b 100644 --- a/libs/common/src/api/mykomap-openapi.json +++ b/libs/common/src/api/mykomap-openapi.json @@ -13,7 +13,7 @@ "required": true, "schema": { "type": "string", - "pattern": "^[A-Z0-9_-]+$" + "pattern": "^[A-Za-z0-9_-]+$" } } ], @@ -90,7 +90,7 @@ "required": true, "schema": { "type": "string", - "pattern": "^[A-Z0-9_-]+$" + "pattern": "^[A-Za-z0-9_-]+$" } }, { @@ -108,13 +108,11 @@ { "type": "array", "items": { - "type": "string", - "pattern": "^(?:[_\\p{Ll}\\p{Lu}\\p{Lt}\\p{Lo}\\p{Nl}][_.\\p{L}\\p{M}\\p{Nd}\\p{Nl}-]*)[:](?:[_\\p{Ll}\\p{Lu}\\p{Lt}\\p{Lo}\\p{Nl}][_.\\p{L}\\p{M}\\p{Nd}\\p{Nl}-]*)$" + "type": "string" } }, { - "type": "string", - "pattern": "^(?:[_\\p{Ll}\\p{Lu}\\p{Lt}\\p{Lo}\\p{Nl}][_.\\p{L}\\p{M}\\p{Nd}\\p{Nl}-]*)[:](?:[_\\p{Ll}\\p{Lu}\\p{Lt}\\p{Lo}\\p{Nl}][_.\\p{L}\\p{M}\\p{Nd}\\p{Nl}-]*)$" + "type": "string" } ] } @@ -190,7 +188,7 @@ "required": true, "schema": { "type": "string", - "pattern": "^[A-Z0-9_-]+$" + "pattern": "^[A-Za-z0-9_-]+$" } }, { @@ -259,6 +257,111 @@ } } }, + "/dataset/{datasetId}/config": { + "get": { + "description": "Obtains configured parameters for a map, which amongst other things, include default values for various options, and definitions of vocabulary terms with their localised labels, that are used to interpret identifers in the data and/or elsewhere.", + "summary": "obtain various configured parameters for a map", + "tags": [], + "parameters": [ + { + "name": "datasetId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]+$" + } + } + ], + "operationId": "getConfig", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "prefixes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "vocabs": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "terms": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "title", + "terms" + ] + } + } + } + }, + "required": [ + "prefixes", + "vocabs" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "additionalProperties": true + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "additionalProperties": true + } + } + } + } + } + } + }, "/version": { "get": { "description": "Obtains version information about the backend Mykomap server, in the form of a JSON object", diff --git a/libs/common/src/iso639-1.ts b/libs/common/src/iso639-1.ts new file mode 100644 index 00000000..87103bcb --- /dev/null +++ b/libs/common/src/iso639-1.ts @@ -0,0 +1,229 @@ +/** Defines the ISO639 set-1 language codes, mapped to the English name of the language. + * + * Derived on 14/10/2024 from https://www.loc.gov/standards/iso639-2/php/English_list.php + * + * Note: sorted alphabetically, and any duplicate codes from the table have been + * necessarily commented out, somewhat arbitrarily (I've aimed to leave the most + * obvious names, which are here mainly for convenience at the moment.) + */ +export const Iso639Set1Codes = { + // ISO 639-1 code: "English Name of Language" + aa: "Afar", + ab: "Abkhazian", + ae: "Avestan", + af: "Afrikaans", + ak: "Akan", + am: "Amharic", + an: "Aragonese", + ar: "Arabic", + as: "Assamese", + av: "Avaric", + ay: "Aymara", + az: "Azerbaijani", + ba: "Bashkir", + be: "Belarusian", + bg: "Bulgarian", + bi: "Bislama", + bm: "Bambara", + bn: "Bengali", + bo: "Tibetan", + br: "Breton", + bs: "Bosnian", + ca: "Catalan", + //ca: "Valencian", + ce: "Chechen", + ch: "Chamorro", + co: "Corsican", + cr: "Cree", + cs: "Czech", + cu: "Church Slavic", + //cu: "Church Slavonic", + //cu: "Old Bulgarian", + //cu: "Old Church Slavonic", + //cu: "Old Slavonic", + cv: "Chuvash", + cy: "Welsh", + da: "Danish", + de: "German", + //dv: "Dhivehi", + //dv: "Divehi", + dv: "Maldivian", + dz: "Dzongkha", + ee: "Ewe", + el: "Greek, Modern (1453-)", + en: "English", + eo: "Esperanto", + //es: "Castilian", + es: "Spanish", + et: "Estonian", + eu: "Basque", + fa: "Persian", + ff: "Fulah", + fi: "Finnish", + fj: "Fijian", + fo: "Faroese", + fr: "French", + fy: "Western Frisian", + ga: "Irish", + //gd: "Gaelic", + gd: "Scottish Gaelic", + gl: "Galician", + gn: "Guarani", + gu: "Gujarati", + gv: "Manx", + ha: "Hausa", + he: "Hebrew", + hi: "Hindi", + ho: "Hiri Motu", + hr: "Croatian", + //ht: "Haitian Creole", + ht: "Haitian", + hu: "Hungarian", + hy: "Armenian", + hz: "Herero", + ia: "Interlingua (International Auxiliary Language Association)", + id: "Indonesian", + ie: "Interlingue", + //ie: "Occidental", + ig: "Igbo", + //ii: "Nuosu", + ii: "Sichuan Yi", + ik: "Inupiaq", + io: "Ido", + is: "Icelandic", + it: "Italian", + iu: "Inuktitut", + ja: "Japanese", + jv: "Javanese", + ka: "Georgian", + kg: "Kongo", + //ki: "Gikuyu", + ki: "Kikuyu", + kj: "Kuanyama", + //kj: "Kwanyama", + kk: "Kazakh", + kl: "Greenlandic", + //kl: "Kalaallisut", + km: "Central Khmer", + kn: "Kannada", + ko: "Korean", + kr: "Kanuri", + ks: "Kashmiri", + ku: "Kurdish", + kv: "Komi", + kw: "Cornish", + //ky: "Kirghiz", + ky: "Kyrgyz", + la: "Latin", + //lb: "Letzeburgesch", + lb: "Luxembourgish", + lg: "Ganda", + li: "Limburgan", + //li: "Limburger", + //li: "Limburgish", + ln: "Lingala", + lo: "Lao", + lt: "Lithuanian", + lu: "Luba-Katanga", + lv: "Latvian", + mg: "Malagasy", + mh: "Marshallese", + mi: "Maori", + mk: "Macedonian", + ml: "Malayalam", + mn: "Mongolian", + mr: "Marathi", + ms: "Malay", + mt: "Maltese", + my: "Burmese", + na: "Nauru", + //nb: "Bokmål, Norwegian", + nb: "Norwegian Bokmål", + //nd: "Ndebele, North", + nd: "North Ndebele", + ne: "Nepali", + ng: "Ndonga", + nl: "Dutch", + //nl: "Flemish", + nn: "Norwegian Nynorsk", + //nn: "Nynorsk, Norwegian", + no: "Norwegian", + //nr: "Ndebele, South", + nr: "South Ndebele", + //nv: "Navaho", + nv: "Navajo", + ny: "Chewa", + //ny: "Chichewa", + //ny: "Nyanja", + oc: "Occitan (post 1500)", + oj: "Ojibwa", + om: "Oromo", + or: "Oriya", + os: "Ossetian", + //os: "Ossetic", + //pa: "Panjabi", + pa: "Punjabi", + pi: "Pali", + pl: "Polish", + ps: "Pashto", + //ps: "Pushto", + pt: "Portuguese", + qu: "Quechua", + rm: "Romansh", + rn: "Rundi", + //ro: "Moldavian", + //ro: "Moldovan", + ro: "Romanian", + ru: "Russian", + rw: "Kinyarwanda", + sa: "Sanskrit", + sc: "Sardinian", + sd: "Sindhi", + se: "Northern Sami", + sg: "Sango", + //si: "Sinhala", + si: "Sinhalese", + sk: "Slovak", + sl: "Slovenian", + sm: "Samoan", + sn: "Shona", + so: "Somali", + sq: "Albanian", + sr: "Serbian", + ss: "Swati", + st: "Sotho, Southern", + su: "Sundanese", + sv: "Swedish", + sw: "Swahili", + ta: "Tamil", + te: "Telugu", + tg: "Tajik", + th: "Thai", + ti: "Tigrinya", + tk: "Turkmen", + tl: "Tagalog", + tn: "Tswana", + to: "Tonga (Tonga Islands)", + tr: "Turkish", + ts: "Tsonga", + tt: "Tatar", + tw: "Twi", + ty: "Tahitian", + ug: "Uighur", + //ug: "Uyghur", + uk: "Ukrainian", + ur: "Urdu", + uz: "Uzbek", + ve: "Venda", + vi: "Vietnamese", + vo: "Volapük", + wa: "Walloon", + wo: "Wolof", + xh: "Xhosa", + yi: "Yiddish", + yo: "Yoruba", + //za: "Chuang", + za: "Zhuang", + zh: "Chinese", + zu: "Zulu", +} as const; diff --git a/libs/common/src/rxdefs.ts b/libs/common/src/rxdefs.ts new file mode 100644 index 00000000..e9eb0582 --- /dev/null +++ b/libs/common/src/rxdefs.ts @@ -0,0 +1,133 @@ +/** Regular expression definitions */ +import RxUtils from "./rxutils.js"; +const { min, seq, conc, maybe } = RxUtils; + +/** A regex testing for an *URL-safe* base64 string (RFC4648 sect 5) */ +export const UrlSafeBase64 = /^[A-Za-z0-9_-]+$/imsu; + +/** A regular expression matching an XML NCName + * + * - An NCName is an XML Name, but with no colons allowed. + * - An XML Name is a complicated beast, but loosely a Unicode version of \w, + * or in other words, a unicode alphanumeric symbolic identifier. + * - It can contain digits, letters, hyphens, periods and underscores and + * certain unicode equivalents. + * - But it must not start with digits, a hyphen or a period, nor certain + * unicode equivalents. + * + * The regex for an XML Name is adapted from O'Reilly Regex Cookbook section 8.4, + * "XML 1.0 names (approximate)" - but the colon is removed. + * + * Paraphrasing that book's explanation: + * - the name start character can be a [:_] or + * - any of the following Unicode categories: + * - Lowercase letter (Ll) + * - Uppercase letter (Lu) + * - Titlecase letter (Lt) + * - Letter without case (Lo) + * - Letter number (Nl) + * - subsequent characters can also include [.-] or + * - Mark (M) + * - Modifier letter (Lm) + * - Decimal digit (Nd) + * + * This definition is is wrapped in a non-capturing group to isolate it without affecting + * captures which may be defined around it in derived regular expressions. + * + * This regex requires node 10+ to be able to use /u and \p. + * + */ +export const NCName = + /(?:[_\p{Ll}\p{Lu}\p{Lt}\p{Lo}\p{Nl}][_.\p{L}\p{M}\p{Nd}\p{Nl}-]*)/imsu; + +/** Match a QName + * + * Paraphrasing https://en.wikipedia.org/wiki/QName + * - A QName is an NCName (see above) + * - Or two of them delimited by a colon. + * + * Note: for our purposes, we *require* a colon delimiter - it can't just be a NCName. + * This is because we need an abbreviation with which to look up the URL prefix. + * + * This definition is is wrapped in a non-capturing group to isolate it without affecting + * captures which may be defined around it in derived regular expressions. + * + * FIXME Perhaps we should also disallow common URI scheme prefixes. + * FIXME Maybe we don't care about unicode? + * + * This regex requires node 10+ to be able to use /u and \p. + * + */ +export const QName = new RegExp(`${NCName.source}[:]${NCName.source}`, "imsu"); + +/** A Prefix-URI scheme. + * + * Note, limited to http and https. + */ +const Scheme = /https?:\/\//; + +/** A domain name component. + * + * A letter, optionally followed by letters, digits or hyphens, but ending with a letter or digit. + * + * Based on https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.1 + */ +const DomainLabel = /[A-Za-z](?:[A-Za-z0-9-]*[A-Za-z0-9])?/; + +/** A domain name. + * + * A domain label optionally followed by more domain labels, each prefixed with a period + * + * Based on https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.1 + */ +const Domain = seq(DomainLabel, min(0, seq("[.]", DomainLabel))); + +/** A percent-encoded URI character. */ +const PctEnc = /%[0-9A-Fa-f]{2}/; + +// URI reserved characters +// reserved /[:/?#\[\]@!$&'()*+,;=]/; + +/** A valid (non-percent-encoded) URI path character. + * + * Path characters can be any of: + * - unreserved chars /[A-Za-z0-9._~-]/ + * - percent-encoded chars i./%[A-Za-z0-9]{2}/ + * - sub-delims chars /[!$&'()*+,;=]/ + * - colon or at chars /[:@]/ + * + * Based on https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + */ +const PathChar = /[A-Za-z0-9._~!$&'()*+,;=:@-]/; + +/** A segment of a URI path, without '/' delimiters. */ +const PathSegment = min(1, PctEnc, PathChar); + +// This is roughly based on the RFC's "path-abempty" syntax - the other kinds +// seem not to fit a http URL, or be special cases of path-abempty +// (path-absolute, path-noscheme, path-rootless, path-empty) +const Path = seq(min(0, seq("/", PathSegment)), maybe("/")); + +/** Matches a Prefix URI. + * + * A prefix URI is one which is a common prefix to a linked-data SKOS vocab + * namespace, i.e. one to which the term IDs can be suffixed to, in order to get + * a full URI for the term. It also tends to be a URI for the SKOS vocab as a + * whole. + * + * Constructed based on the RFC3986 spec, narrowed for our purposes. + * https://datatracker.ietf.org/doc/html/rfc3986 + * + * This does not match the full specification of an URI as outloned in RFC3986, + * as we have more restrictive ideas of a valid URI. Specifically: + * - no local URIs, like http://localhost + * - no IPv6 ot IPv6 hostnames + * - no port address, like http://example.com:1234 + * - no user or password, like http://joe:secret@example.com + * - only http or https schemes (not ftp, gopher, ldap, etc; also we insist on lower case) + * - no querystring, like http://example.com/somewhere?querystring + * - a trailing anchor character ("#") is allowed, but nothing more (the URI needs to be + * ready for anchor IDs to be suffixed) + * + */ +export const PrefixUri = conc(Scheme, Domain, Path, maybe("#")); diff --git a/libs/common/src/rxutils.ts b/libs/common/src/rxutils.ts new file mode 100644 index 00000000..190cdd3a --- /dev/null +++ b/libs/common/src/rxutils.ts @@ -0,0 +1,91 @@ +/** A collection of Regular expression building utilities. */ +const RxUtils = (() => { + /** Map a string or RegExp to a string */ + const _str = (component: string | RegExp) => + component instanceof RegExp ? component.source : component; + + /** Wrap patterns in a non-capturing group, separated by a delimiter + * + * If there are more than one pattern, each one is also wrapped in a non-capturing group. + * + * Note, unlike the other functions here, this one is for internal use and + * returns a string, not a RegExp. + */ + const _wrap = ( + delim: "" | "|", + ...components: Array + ): string => { + switch (components.length) { + case 0: + return ""; + case 1: + return `(?:${_str(components[0])})`; + default: + return ( + "(?:" + components.map((c) => `(?:${_str(c)})`).join(delim) + ")" + ); + } + }; + + /** Wrap patterns in a non-capturing group, separated by a delimiter. + * + * If there are more than one pattern, each one is also wrapped in a non-capturing group. + */ + const wrap = ( + delim: "" | "|", + ...components: Array + ): RegExp => new RegExp(_wrap(delim, ...components)); + + /** Simply concatenate the patterns passed, no grouping or delimiter */ + const conc = (...components: Array): RegExp => + new RegExp(components.map((c) => _str(c)).join("")); + + /** Match the patterns passed as a sequence */ + const seq = (...components: Array): RegExp => + new RegExp(_wrap("", ...components)); + + /** Match just one of the patterns passed */ + const oneOf = (...components: Array): RegExp => + new RegExp(_wrap("|", ...components)); + + /** Match at least n of one the patterns passed */ + const min = (n: 0 | 1, ...components: Array): RegExp => { + const expr = _wrap("|", ...components); + switch (n) { + case 0: + return new RegExp(`${expr}*`); + case 1: + return new RegExp(`${expr}+`); + } + }; + + /** Matches zero or one of the patterns passed */ + const maybe = (...components: Array): RegExp => { + const expr = _wrap("|", ...components); + return new RegExp(`${expr}?`); + }; + + /** Match unicode, all or nothing. + * + * Ensures the regular expression has msu flags and is anchored to match all + * of a string or nothing. + */ + const uaon = (component: string | RegExp): RegExp => { + let [expr, flags] = + component instanceof RegExp + ? [component.source, component.flags] + : [component, ""]; + + expr = expr.replace(/^\^?/ms, "^"); // ensure anchor at start + expr = expr.replace(/\$?$/ms, "$"); // ensure anchor at end + + // Extend and deduplicate flags + flags = [...new Set((flags + "msu").split(""))].join(""); + + return new RegExp(expr, flags); + }; + + return { conc, maybe, min, seq, oneOf, uaon, wrap }; +})(); + +export default RxUtils; diff --git a/libs/common/test/data/validation/ConfigData/case-1.good.json b/libs/common/test/data/validation/ConfigData/case-1.good.json new file mode 100644 index 00000000..e116519f --- /dev/null +++ b/libs/common/test/data/validation/ConfigData/case-1.good.json @@ -0,0 +1,30 @@ +{ + "prefixes": { + "https://dev.lod.coop/essglobal/2.1/standard/activities-modified/": "am" + }, + "vocabs": { + "am": { + "en": { + "title": "Activities (Modified)", + "terms": { + "AM10": "Arts, Media, Culture & Leisure", + "AM20": "Campaigning, Activism & Advocacy", + "AM30": "Community & Collective Spaces", + "AM40": "Education", + "AM50": "Energy", + "AM60": "Food", + "AM70": "Goods & Services", + "AM80": "Health, Social Care & Wellbeing", + "AM90": "Housing", + "AM100": "Money & Finance", + "AM110": "Nature, Conservation & Environment", + "AM120": "Reduce, Reuse, Repair & Recycle", + "AM130": "Agriculture", + "AM140": "Industry", + "AM150": "Utilities", + "AM160": "Transport" + } + } + } + } +} diff --git a/libs/common/test/file-utils.ts b/libs/common/test/file-utils.ts new file mode 100644 index 00000000..63b1d48d --- /dev/null +++ b/libs/common/test/file-utils.ts @@ -0,0 +1,42 @@ +import { readFileSync } from "node:fs"; +import { readFile } from "node:fs/promises"; + +/** Read a file asynchronously, given its path + */ +async function slurp( + path: string, + opts: { + enc?: BufferEncoding; + } = {}, +): Promise { + return readFile(path, opts.enc ?? "utf8").then((it) => it.toString()); +} + +/** Read a file synchronously, given its path + */ +function slurpSync( + path: string, + opts: { + enc?: BufferEncoding; + } = {}, +): string { + return readFileSync(path, opts.enc ?? "utf8").toString(); +} + +/** Read in a JSON file asynchronously, given its path. */ +async function slurpJson( + path: string, + reviver?: (this: any, key: any, value: any) => any, +): Promise { + return slurp(path).then((x) => JSON.parse(x, reviver)); +} + +/** Read in a JSON file synchronously, given its path. */ +function slurpJsonSync( + path: string, + reviver?: (this: any, key: any, value: any) => any, +): Promise { + return JSON.parse(slurpSync(path), reviver); +} + +export { slurp, slurpSync, slurpJson, slurpJsonSync }; diff --git a/libs/common/test/validation.test.ts b/libs/common/test/validation.test.ts new file mode 100644 index 00000000..f5feb9d1 --- /dev/null +++ b/libs/common/test/validation.test.ts @@ -0,0 +1,183 @@ +/// + +import { expect, test, Assertion } from "vitest"; +import { schemas } from "../src/index.js"; +import { ZodType } from "zod"; +import { slurpJsonSync } from "./file-utils.js"; +import { globSync } from "glob"; +import { join } from "node:path"; + +const { DatasetId, QName, PrefixUri, Iso639Set1Code, ConfigData } = schemas; + +/** Creates expectations on validating each of an array of cases + * + * Mainly here to provide the common part of expectValid and expectInvalid. + */ +function expectSafeParse( + validation: Z, + cases: unknown[], + expectTest: (assert: Assertion) => void, +) { + cases.forEach((it) => { + const result = validation.safeParse(it); + expectTest(expect(result.success, `parsing '${it}'...\n${result.error}`)); + }); +} + +/** Expect Zod validations of each of the cases given to be truthy */ +function expectValid(validation: Z, cases: unknown[]) { + expectSafeParse(validation, cases, (a) => a.toBeTruthy()); +} + +/** Expect Zod validations of each of the cases given to be falsy */ +function expectInvalid(validation: Z, cases: unknown[]) { + expectSafeParse(validation, cases, (a) => a.toBeFalsy()); +} + +/** Find files given by a glob and parse as JSON */ +function glorpJson(pathGlob: string) { + const paths = globSync(pathGlob).map((path) => + join(import.meta.dirname, path), + ); + return paths.map((path) => slurpJsonSync(path)); +} + +test("testing DatasetId validation", async (t) => { + expectValid(DatasetId, [ + "0", + "A", + "z", + "_", + "-", + "01234", + "Quick-Brown-Fox_42", + ]); + expectInvalid(DatasetId, ["", " ", "/", "?", "&", ":", ".", "="]); +}); + +test("testing QName validation", async (t) => { + expectValid(QName, ["a:b", "a1:b1", "_:_", "_1-.:_1-."]); + expectInvalid(QName, [ + "", + ":", + "a:", + ":a", + "_", + "-", + ".", + "&", + ";", + "/", + "1:", + ":1", + "a:1", + "1:a", + "-:-", + ]); +}); + +test("testing PrefixUri validation", async (t) => { + expectValid(PrefixUri, [ + "http://a", + "http://a/", + "http://e.a", + "http://e.a/", + "http://example.com", + "http://EXAMPLE.COM", + "http://example.Com", + "https://example.com", + "http://www.example.com", + "http://www.example.com/", + "https://w3-example/", + "https://w3-example1.com/", + "http://example.com#", + "http://example.com/#", + "http://example.com/foo", + "http://example.com/foo/", + "http://example.com/foo/bar", + "http://example.com/foo/bar/", + "http://example.com/foo#", + "http://example.com/foo/#", + "http://example.com/foo/bar#", + "http://example.com/foo/bar/#", + "http://example.com/%2e%4F", + "http://example.com/A-Za-z0-9._~!$&'()*+,;=:@-%20/", + ]); + expectInvalid(PrefixUri, [ + "http://", + "http://-", + "http://.", + "http://.a", + "http://-a", + "http://a-", + "http://a.", + "http://3a.com", + "http://a.3com", + "http://-a.com", + "http://a-.com", + "http://a.-com", + "http://a.-com", + "http://a.com-", + "http://a_b", + "http://a_b.c", + "http://a@b", + "http://a@b.c", + "http://a:b.c", + "http://b.c:8000", + "HTTP://example.com", + "Http://example.com", + "htt://example.com", + "httpss://example.com", + "http//example.com", + "http:/example.com", + "http:///example.com", + "http//:/example.com", + "http/example.com", + "http/:example.com", + "http//:example.com", + "http://example.com?", + "http://example.com/?", + "http://example.com?q", + "http://example.com/?q", + "http://example.com/foo?q", + "http://example.com/foo/?q", + "http://example.com/#?", + "http://example.com#?", + "http://example.com#q", + "http://example.com/#q", + 'http://example.com/foo"bar', + "http://example.com//foobar", + "http://example.com/foo//bar", + "http://example.com/foobar//", + ]); +}); + +test("testing Iso639Set1Code validation", async (t) => { + expectValid(Iso639Set1Code, ["en", "fr", "ko", "es"]); + expectInvalid(Iso639Set1Code, [ + "xe", + "En", + "eN", + "EN", + "enn", + "en ", + " en", + "e ", + "e n", + " ", + "e_", + "_e", + "e-", + "-e", + "__", + "--", + "'en'", + "en:", + "e:", + ]); +}); + +test("testing ConfigData validation", async (t) => { + expectValid(ConfigData, glorpJson("data/validation/vocabIndex/*.good.json")); + expectInvalid(ConfigData, glorpJson("data/validation/vocabIndex/*.bad.json")); +}); diff --git a/package-lock.json b/package-lock.json index dd961ecf..53bf883a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "mykomap-monolith", "workspaces": [ "apps/*", "libs/*" @@ -115,6 +116,7 @@ "dependencies": { "@ts-rest/core": "^3.51.0", "@ts-rest/open-api": "^3.51.0", + "glob": "^11.0.0", "zod": "^3.23.8" }, "devDependencies": { @@ -123,7 +125,142 @@ "typescript": "^5.5.4", "vite": "^5.4.3", "vite-node": "^2.0.5", - "vite-plugin-dts": "^4.1.0" + "vite-plugin-dts": "^4.1.0", + "vitest": "^2.1.4" + } + }, + "libs/common/node_modules/@vitest/expect": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.4.tgz", + "integrity": "sha512-DOETT0Oh1avie/D/o2sgMHGrzYUFFo3zqESB2Hn70z6QB1HrS2IQ9z5DfyTqU8sg4Bpu13zZe9V4+UTNQlUeQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.4", + "@vitest/utils": "2.1.4", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "libs/common/node_modules/@vitest/runner": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.4.tgz", + "integrity": "sha512-sKRautINI9XICAMl2bjxQM8VfCMTB0EbsBc/EDFA57V6UQevEKY/TOPOF5nzcvCALltiLfXWbq4MaAwWx/YxIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.4", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "libs/common/node_modules/@vitest/snapshot": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.4.tgz", + "integrity": "sha512-3Kab14fn/5QZRog5BPj6Rs8dc4B+mim27XaKWFWHWA87R56AKjHTGcBFKpvZKDzC4u5Wd0w/qKsUIio3KzWW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.4", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "libs/common/node_modules/@vitest/spy": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.4.tgz", + "integrity": "sha512-4JOxa+UAizJgpZfaCPKK2smq9d8mmjZVPMt2kOsg/R8QkoRzydHH1qHxIYNvr1zlEaFj4SXiaaJWxq/LPLKaLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "libs/common/node_modules/tinypool": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", + "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "libs/common/node_modules/vitest": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.4.tgz", + "integrity": "sha512-eDjxbVAJw1UJJCHr5xr/xM86Zx+YxIEXGAR+bmnEID7z9qWfoxpHw0zdobz+TQAFOLT+nEXz3+gx6nUJ7RgmlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.4", + "@vitest/mocker": "2.1.4", + "@vitest/pretty-format": "^2.1.4", + "@vitest/runner": "2.1.4", + "@vitest/snapshot": "2.1.4", + "@vitest/spy": "2.1.4", + "@vitest/utils": "2.1.4", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.4", + "@vitest/ui": "2.1.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } } }, "node_modules/@adobe/css-tools": { @@ -3626,7 +3763,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -6849,10 +6985,67 @@ "@types/estree": "^1.0.0" } }, + "node_modules/@vitest/mocker": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.4.tgz", + "integrity": "sha512-Ky/O1Lc0QBbutJdW0rqLeFNbuLEyS+mIPiNdlVlp2/yhJ0SbyYqObS5IHdhferJud8MbbwMnexg4jordE5cCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/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/@vitest/mocker/node_modules/@vitest/spy": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.4.tgz", + "integrity": "sha512-4JOxa+UAizJgpZfaCPKK2smq9d8mmjZVPMt2kOsg/R8QkoRzydHH1qHxIYNvr1zlEaFj4SXiaaJWxq/LPLKaLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/@vitest/pretty-format": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.2.tgz", - "integrity": "sha512-FIoglbHrSUlOJPDGIrh2bjX1sNars5HbxlcsFKCtKzu4+5lpsRhOCVcuzp0fEhAGHkPZRIXVNzPcpSlkoZ3LuA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.4.tgz", + "integrity": "sha512-L95zIAkEuTDbUX1IsjRl+vyBSLh3PwLLgKpghl37aCK9Jvw0iP+wKwIFhfjdUtA2myLgjrG6VU6JCFLv8q/3Ww==", "dev": true, "license": "MIT", "dependencies": { @@ -7034,14 +7227,14 @@ } }, "node_modules/@vitest/utils": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.2.tgz", - "integrity": "sha512-zMO2KdYy6mx56btx9JvAqAZ6EyS3g49krMPPrgOp1yxGZiA93HumGk+bZ5jIZtOg5/VBYl5eBmGRQHqq4FG6uQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.4.tgz", + "integrity": "sha512-MXDnZn0Awl2S86PSNIim5PWXgIAx8CIkzu35mBdSApUip6RFOGXBCf3YFyeEu8n1IHk4bWD46DeYFu9mQlFIRg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.2", - "loupe": "^3.1.1", + "@vitest/pretty-format": "2.1.4", + "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" }, "funding": { @@ -7290,7 +7483,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7303,7 +7495,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7782,7 +7973,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -7887,7 +8077,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -8123,9 +8312,9 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", - "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", + "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", "dev": true, "license": "MIT", "dependencies": { @@ -8520,9 +8709,9 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "license": "MIT", "engines": { @@ -8607,7 +8796,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -9021,7 +9209,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/editorconfig": { @@ -9061,7 +9248,6 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -10323,10 +10509,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10335,7 +10531,7 @@ "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -10914,7 +11110,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -11191,7 +11386,6 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -11228,7 +11422,6 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -12042,7 +12235,6 @@ "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" @@ -12343,7 +12535,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/isobject": { @@ -12454,7 +12645,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -12846,7 +13036,6 @@ "version": "11.0.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", - "dev": true, "license": "ISC", "engines": { "node": "20 || >=22" @@ -12863,9 +13052,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.11", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", - "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", "dev": true, "license": "MIT", "dependencies": { @@ -13140,7 +13329,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -13674,7 +13862,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -13774,7 +13961,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13790,7 +13976,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -15627,7 +15812,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -15640,7 +15824,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -15676,7 +15859,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -15943,7 +16125,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -15962,7 +16143,6 @@ "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", @@ -15977,7 +16157,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -15987,14 +16166,12 @@ "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/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -16108,7 +16285,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -16125,7 +16301,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -16138,7 +16313,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -16600,6 +16774,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyexec": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", + "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/tinypool": { "version": "0.8.4", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", @@ -17389,14 +17570,14 @@ } }, "node_modules/vite-node": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.2.tgz", - "integrity": "sha512-HPcGNN5g/7I2OtPjLqgOtCRu/qhVvBxTUD3qzitmL0SrG1cWFzxzhMDWussxSbrRYWqnKf8P2jiNhPMSN+ymsQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.4.tgz", + "integrity": "sha512-kqa9v+oi4HwkG6g8ufRnb5AeplcRw8jUF6/7/Qz1qRQOXHImG8YnLbB+LLszENwFnoBl9xIf9nVdCFzNd7GQEg==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.6", + "debug": "^4.3.7", "pathe": "^1.1.2", "vite": "^5.0.0" }, @@ -18271,7 +18452,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -18397,7 +18577,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -18416,7 +18595,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -18434,7 +18612,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -18444,7 +18621,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -18460,14 +18636,12 @@ "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/wrap-ansi-cjs/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", @@ -18482,7 +18656,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1"