ci: convert json-validation to typescript (@fehmer) (#6899)

- convert json-validation to typescript
- integrate tests for assets back into the json-validation script
This commit is contained in:
Christian Fehmer 2025-08-21 00:42:21 +02:00 committed by GitHub
parent a1af28bb5d
commit d2c627fcc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 358 additions and 284 deletions

View file

@ -1,46 +0,0 @@
import { describe, it, expect } from "vitest";
import { Fonts } from "../../src/ts/constants/fonts";
import { readdirSync } from "fs";
const ignoredFonts = new Set([
"GallaudetRegular.woff2", //used for asl
"Vazirmatn-Regular.woff2", //default font
]);
describe("fonts", () => {
it("should have all related font files", () => {
const fontFiles = listFontFiles();
const expectedFontFiles = Object.entries(Fonts)
.filter(([_name, config]) => !config.systemFont)
.map(([_name, config]) => config.fileName as string);
const missingFontFiles = expectedFontFiles
.filter((fileName) => !fontFiles.includes(fileName))
.map((name) => `fontend/static/webfonts/${name}`);
expect(missingFontFiles, "missing font files").toEqual([]);
});
it("should not have additional font files", () => {
const fontFiles = listFontFiles();
const expectedFontFiles = new Set(
Object.entries(Fonts)
.filter(([_name, config]) => !config.systemFont)
.map(([_name, config]) => config.fileName as string)
);
const additionalFontFiles = fontFiles
.filter((name) => !expectedFontFiles.has(name))
.map((name) => `fontend/static/webfonts/${name}`);
expect(
additionalFontFiles,
"additional font files not declared in frontend/src/ts/constants/fonts.ts"
).toEqual([]);
});
});
function listFontFiles() {
return readdirSync(import.meta.dirname + "/../../static/webfonts").filter(
(it) => !ignoredFonts.has(it)
);
}

View file

@ -1,68 +0,0 @@
import { describe, it, expect } from "vitest";
import { readdirSync } from "fs";
import { LanguageGroups, LanguageList } from "../../src/ts/constants/languages";
import { Language } from "@monkeytype/schemas/languages";
describe("languages", () => {
describe("LanguageList", () => {
it("should not have duplicates", () => {
const duplicates = LanguageList.filter(
(item, index) => LanguageList.indexOf(item) !== index
);
expect(duplicates).toEqual([]);
});
it("should have all related json files", () => {
const languageFiles = listLanguageFiles();
const missingLanguageFiles = LanguageList.filter(
(it) => !languageFiles.includes(it)
).map((it) => `fontend/static/languages/${it}.json`);
expect(missingLanguageFiles, "missing language json files").toEqual([]);
});
it("should not have additional related json files", () => {
const LanguageFiles = listLanguageFiles();
const additionalLanguageFiles = LanguageFiles.filter(
(it) => !LanguageList.some((language) => language === it)
).map((it) => `fontend/static/languages/${it}.json`);
expect(
additionalLanguageFiles,
"additional language json files not declared in frontend/src/ts/constants/languages.ts"
).toEqual([]);
});
});
describe("LanguageGroups", () => {
it("should contain each language once", () => {
const languagesWithMultipleGroups = [];
const groupByLanguage = new Map<Language, string>();
for (const group of Object.keys(LanguageGroups)) {
for (const language of LanguageGroups[group] as Language[]) {
if (groupByLanguage.has(language)) {
languagesWithMultipleGroups.push(language);
}
groupByLanguage.set(language, group);
}
}
expect(
languagesWithMultipleGroups,
"languages with multiple groups"
).toEqual([]);
expect(
Array.from(groupByLanguage.keys()).sort(),
"every language has a group"
).toEqual(LanguageList.sort());
});
});
});
function listLanguageFiles() {
return readdirSync(import.meta.dirname + "/../../static/languages").map(
(it) => it.substring(0, it.length - 5) as Language
);
}

View file

@ -1,40 +0,0 @@
import { describe, it, expect } from "vitest";
import { readdirSync } from "fs";
import { LayoutsList } from "../../src/ts/constants/layouts";
import { LayoutName } from "@monkeytype/schemas/layouts";
describe("layouts", () => {
it("should not have duplicates", () => {
const duplicates = LayoutsList.filter(
(item, index) => LayoutsList.indexOf(item) !== index
);
expect(duplicates).toEqual([]);
});
it("should have all related json files", () => {
const layoutFiles = listLayoutFiles();
const missingLayoutFiles = LayoutsList.filter(
(it) => !layoutFiles.includes(it)
).map((it) => `fontend/static/layouts/${it}.json`);
expect(missingLayoutFiles, "missing layout json files").toEqual([]);
});
it("should not have additional related json files", () => {
const layoutFiles = listLayoutFiles();
const additionalLayoutFiles = layoutFiles
.filter((it) => !LayoutsList.includes(it))
.map((it) => `fontend/static/layouts/${it}.json`);
expect(
additionalLayoutFiles,
"additional layout json files not declared in frontend/src/ts/constants/layouts.ts"
).toEqual([]);
});
});
function listLayoutFiles() {
return readdirSync(import.meta.dirname + "/../../static/layouts").map(
(it) => it.substring(0, it.length - 5) as LayoutName
);
}

View file

@ -7,7 +7,7 @@
"eslint": "eslint \"./**/*.ts\"",
"oxlint": "oxlint .",
"lint": "npm run oxlint && npm run eslint",
"validate-json": "node ./scripts/json-validation.cjs",
"validate-json": "tsx ./scripts/json-validation.ts",
"audit": "vite-bundle-visualizer",
"dep-graph": "madge -c -i \"dep-graph.png\" ./src/ts",
"ts-check": "tsc --noEmit",
@ -63,6 +63,7 @@
"postcss": "8.4.31",
"sass": "1.70.0",
"subset-font": "2.3.0",
"tsx": "4.16.2",
"typescript": "5.5.4",
"unplugin-inject-preload": "3.0.0",
"vite": "6.3.4",

View file

@ -5,18 +5,24 @@
* pnpm validate-json challenges fonts -p (npm run validate-json challenges fonts -- -p)
*/
// eslint-disable no-require-imports
const fs = require("fs");
const Ajv = require("ajv");
import * as fs from "fs";
import Ajv from "ajv";
import { LanguageGroups, LanguageList } from "../src/ts/constants/languages";
import { Language } from "@monkeytype/schemas/languages";
import { Layout, ThemeName } from "@monkeytype/schemas/configs";
import { LayoutsList } from "../src/ts/constants/layouts";
import { KnownFontName } from "@monkeytype/schemas/fonts";
import { Fonts } from "../src/ts/constants/fonts";
import { ThemesList } from "../src/ts/constants/themes";
const ajv = new Ajv();
function findDuplicates(words) {
const wordFrequencies = {};
const duplicates = [];
function findDuplicates(words: string[]): string[] {
const wordFrequencies: Record<string, number> = {};
const duplicates: string[] = [];
words.forEach((word) => {
wordFrequencies[word] =
word in wordFrequencies ? wordFrequencies[word] + 1 : 1;
wordFrequencies[word] = (wordFrequencies[word] ?? 0) + 1;
if (wordFrequencies[word] === 2) {
duplicates.push(word);
@ -25,7 +31,7 @@ function findDuplicates(words) {
return duplicates;
}
function validateChallenges() {
async function validateChallenges(): Promise<void> {
return new Promise((resolve, reject) => {
const challengesSchema = {
type: "array",
@ -103,20 +109,25 @@ function validateChallenges() {
encoding: "utf8",
flag: "r",
})
);
) as object;
const challengesValidator = ajv.compile(challengesSchema);
if (challengesValidator(challengesData)) {
console.log("Challenges list JSON schema is \u001b[32mvalid\u001b[0m");
} else {
console.log("Challenges list JSON schema is \u001b[31minvalid\u001b[0m");
return reject(new Error(challengesValidator.errors[0].message));
reject(new Error(challengesValidator?.errors?.[0]?.message));
}
resolve();
});
}
function validateLayouts() {
async function validateLayouts(): Promise<void> {
return new Promise((resolve, reject) => {
const problems: Partial<Record<Layout | "_additional", string[]>> = {};
const addProblem = (layout: keyof typeof problems, error: string): void => {
problems[layout] = [...(problems[layout] ?? []), error];
};
const charDefinitionSchema = {
type: "array",
minItems: 1,
@ -221,49 +232,76 @@ function validateLayouts() {
},
};
let layoutsErrors = [];
const layouts = fs
.readdirSync("./static/layouts")
.map((it) => it.substring(0, it.length - 5));
for (let layoutName of layouts) {
let layoutData = "";
for (let layoutName of LayoutsList) {
let layoutData = undefined;
if (!fs.existsSync(`./static/layouts/${layoutName}.json`)) {
addProblem(
layoutName,
`missing json file frontend/static/layouts/${layoutName}.json`
);
continue;
}
try {
layoutData = JSON.parse(
fs.readFileSync(`./static/layouts/${layoutName}.json`, "utf-8")
);
) as object & { type: "ansi" | "iso" };
} catch (e) {
layoutsErrors.push(`Layout ${layoutName} has error: ${e.message}`);
addProblem(
layoutName,
`Unable to parse ${e instanceof Error ? e.message : e}`
);
continue;
}
if (!layoutsSchema[layoutData.type]) {
const msg = `Layout ${layoutName} has an invalid type: ${layoutData.type}`;
console.log(msg);
layoutsErrors.push(msg);
if (layoutsSchema[layoutData.type] === undefined) {
addProblem(layoutName, `invalid type: ${layoutData.type}`);
} else {
const layoutsValidator = ajv.compile(layoutsSchema[layoutData.type]);
if (!layoutsValidator(layoutData)) {
console.log(
`Layout ${layoutName} JSON schema is \u001b[31minvalid\u001b[0m`
addProblem(
layoutName,
layoutsValidator.errors?.[0]?.message ?? "unknown"
);
layoutsErrors.push(layoutsValidator.errors[0].message);
}
}
}
if (layoutsErrors.length === 0) {
console.log(`Layout JSON schemas are \u001b[32mvalid\u001b[0m`);
} else {
console.log(`Layout JSON schemas are \u001b[31minvalid\u001b[0m`);
return reject(new Error(layoutsErrors.join("\n")));
//no files not defined in LayoutsList
const additionalLayoutFiles = fs
.readdirSync("./static/layouts")
.map((it) => it.substring(0, it.length - 5))
.filter((it) => !LayoutsList.some((layout) => layout === it))
.map((it) => `frontend/static/layouts/${it}.json`);
if (additionalLayoutFiles.length !== 0) {
problems._additional = additionalLayoutFiles;
}
if (Object.keys(problems).length === 0) {
console.log(`Layouts are all \u001b[32mvalid\u001b[0m`);
} else {
console.log(`Layouts are \u001b[31minvalid\u001b[0m`);
console.log(
Object.entries(problems)
.map(([layout, problems]) => {
let label = `${layout}.json`;
if (layout === "_additional")
label =
"Additional layout files not declared in frontend/src/ts/constants/layouts.ts";
return `${label}:\n ${problems
.map((error) => "\t- " + error)
.join("\n")}`;
})
.join("\n")
);
reject(new Error("layouts with errors"));
}
resolve();
});
}
function validateQuotes() {
async function validateQuotes(): Promise<void> {
return new Promise((resolve, reject) => {
//quotes
const quoteSchema = {
@ -309,16 +347,19 @@ function validateQuotes() {
let quoteIdsAllGood = true;
let quoteIdsErrors;
let quoteLengthsAllGood = true;
let quoteLengthErrors = [];
let quoteLengthErrors: string[] = [];
const quotesFiles = fs.readdirSync("./static/quotes/");
quotesFiles.forEach((quotefilename) => {
quotefilename = quotefilename.split(".")[0];
quotesFiles.forEach((quotefilename: string) => {
quotefilename = quotefilename.split(".")[0] as string;
const quoteData = JSON.parse(
fs.readFileSync(`./static/quotes/${quotefilename}.json`, {
encoding: "utf8",
flag: "r",
})
);
) as object & {
language: string;
quotes: { id: number; text: string; length: number }[];
};
if (quoteData.language !== quotefilename) {
quoteFilesAllGood = false;
quoteFilesErrors = "Name is not " + quotefilename;
@ -330,7 +371,7 @@ function validateQuotes() {
);
quoteFilesAllGood = false;
quoteFilesErrors =
quoteValidator.errors[0].message +
quoteValidator.errors?.[0]?.message +
` (at static/quotes/${quotefilename}.json)`;
return;
}
@ -342,7 +383,7 @@ function validateQuotes() {
);
quoteIdsAllGood = false;
quoteIdsErrors =
quoteIdsValidator.errors[0].message +
quoteIdsValidator.errors?.[0]?.message +
` (at static/quotes/${quotefilename}.json)`;
}
const incorrectQuoteLength = quoteData.quotes.filter(
@ -367,29 +408,37 @@ function validateQuotes() {
console.log(`Quote file JSON schemas are \u001b[32mvalid\u001b[0m`);
} else {
console.log(`Quote file JSON schemas are \u001b[31minvalid\u001b[0m`);
return reject(new Error(quoteFilesErrors));
reject(new Error(quoteFilesErrors));
}
if (quoteIdsAllGood) {
console.log(`Quote IDs are \u001b[32munique\u001b[0m`);
} else {
console.log(`Quote IDs are \u001b[31mnot unique\u001b[0m`);
return reject(new Error(quoteIdsErrors));
reject(new Error(quoteIdsErrors));
}
if (quoteLengthsAllGood) {
console.log(`Quote length fields are \u001b[32mcorrect\u001b[0m`);
} else {
console.log(`Quote length fields are \u001b[31mincorrect\u001b[0m`);
return reject(new Error(quoteLengthErrors));
reject(new Error(quoteLengthErrors.join(",")));
}
resolve();
});
}
function validateLanguages() {
async function validateLanguages(): Promise<void> {
return new Promise((resolve, reject) => {
const languages = fs
.readdirSync("./static/languages")
.map((it) => it.substring(0, it.length - 5));
const problems: Partial<
Record<Language | "_additional" | "_groups", string[]>
> = {};
const addProblem = (
language: keyof typeof problems,
error: string
): void => {
problems[language] = [...(problems[language] ?? []), error];
};
//language files
const languageFileSchema = {
type: "object",
@ -414,84 +463,250 @@ function validateLanguages() {
},
required: ["name", "words"],
};
let languageFilesAllGood = true;
let languageWordListsAllGood = true;
let languageFilesErrors;
const duplicatePercentageThreshold = 0.0001;
let langsWithDuplicates = 0;
languages.forEach((language) => {
const languageFileData = JSON.parse(
fs.readFileSync(`./static/languages/${language}.json`, {
encoding: "utf8",
flag: "r",
})
);
for (const language of LanguageList) {
let languageFileData;
try {
languageFileData = JSON.parse(
fs.readFileSync(`./static/languages/${language}.json`, {
encoding: "utf8",
flag: "r",
})
) as object & { name: string; words: string[] };
} catch (e) {
addProblem(
language,
`missing json file frontend/static/languages/${language}.json`
);
continue;
}
const languageFileValidator = ajv.compile(languageFileSchema);
if (!languageFileValidator(languageFileData)) {
console.log(
`Language ${language} JSON schema is \u001b[31minvalid\u001b[0m`
addProblem(
language,
languageFileValidator.errors?.[0]?.message ?? "unknown"
);
languageFilesAllGood = false;
languageFilesErrors =
languageFileValidator.errors[0].message +
` (at static/languages/${language}.json`;
return;
continue;
}
if (languageFileData.name !== language) {
languageFilesAllGood = false;
languageFilesErrors = "Name is not " + language;
problems[language] = [
...(problems[language] ?? []),
"Name is not " + language,
];
}
const duplicates = findDuplicates(languageFileData.words);
const duplicatePercentage =
(duplicates.length / languageFileData.words.length) * 100;
if (duplicatePercentage >= duplicatePercentageThreshold) {
langsWithDuplicates++;
languageWordListsAllGood = false;
languageFilesErrors = `Language '${languageFileData.name}' contains ${
duplicates.length
} (${Math.round(duplicatePercentage)}%) duplicates:`;
console.log(languageFilesErrors);
console.log(duplicates);
addProblem(
language,
`contains ${duplicates.length} (${Math.round(
duplicatePercentage
)}%) duplicates:\n ${duplicates.join(",")}`
);
}
});
if (languageFilesAllGood) {
console.log(
`Language word list JSON schemas are \u001b[32mvalid\u001b[0m`
);
} else {
console.log(
`Language word list JSON schemas are \u001b[31minvalid\u001b[0m`
);
return reject(new Error(languageFilesErrors));
}
if (languageWordListsAllGood) {
console.log(
`Language word lists duplicate check is \u001b[32mvalid\u001b[0m`
//no files not defined in LanguageList
const additionalLanguageFiles = fs
.readdirSync("./static/languages")
.map((it) => it.substring(0, it.length - 5))
.filter((it) => !LanguageList.some((language) => language === it))
.map((it) => `frontend/static/languages/${it}.json`);
if (additionalLanguageFiles.length !== 0) {
problems._additional = additionalLanguageFiles;
}
//check groups
const languagesWithMultipleGroups = [];
const groupByLanguage = new Map<Language, string>();
for (const group of Object.keys(LanguageGroups)) {
for (const language of LanguageGroups[group] as Language[]) {
if (groupByLanguage.has(language)) {
languagesWithMultipleGroups.push(language);
}
groupByLanguage.set(language, group);
}
}
if (languagesWithMultipleGroups.length !== 0) {
addProblem(
"_groups",
`languages with multiple groups: ${languagesWithMultipleGroups.join(
", "
)}`
);
}
const languagesMissingGroup = LanguageList.filter(
(lang) => !groupByLanguage.has(lang)
);
if (languagesMissingGroup.length !== 0) {
problems._groups = [
...(problems._groups ?? []),
`languages missing group: ${languagesMissingGroup.join(", ")}`,
];
}
if (Object.keys(problems).length === 0) {
console.log(`Languages are all \u001b[32mvalid\u001b[0m`);
} else {
console.log(`Languages are \u001b[31minvalid\u001b[0m`);
console.log(
`Language word lists duplicate check is \u001b[31minvalid\u001b[0m (${langsWithDuplicates} languages contain duplicates)`
Object.entries(problems)
.map(([language, problems]) => {
let label = `${language}.json`;
if (language === "_additional")
label =
"Additional language files not declared in frontend/src/ts/constants/languages.ts";
else if (language === "_groups")
label =
"Problems in LanguageGroups on frontend/src/ts/constants/languages.ts";
return `${label}:\n ${problems
.map((error) => "\t- " + error)
.join("\n")}`;
})
.join("\n")
);
return reject(new Error(languageFilesErrors));
reject(new Error("languages with errors"));
}
resolve();
});
}
function main() {
async function validateFonts(): Promise<void> {
const problems: Partial<Record<KnownFontName | "_additional", string[]>> = {};
const addProblem = (fontName: keyof typeof problems, error: string): void => {
problems[fontName] = [...(problems[fontName] ?? []), error];
};
//no missing files
const ignoredFonts = new Set([
"GallaudetRegular.woff2", //used for asl
"Vazirmatn-Regular.woff2", //default font
]);
const fontFiles = fs
.readdirSync("./static/webfonts")
.filter((it) => !ignoredFonts.has(it));
//missing font files
Object.entries(Fonts)
.filter(([_name, config]) => !config.systemFont)
.filter(([_name, config]) => !fontFiles.includes(config.fileName as string))
.forEach(([name, config]) =>
addProblem(
name as KnownFontName,
`missing file frontend/static/webfonts/${config.fileName}`
)
);
//additional font files
const expectedFontFiles = new Set(
Object.entries(Fonts)
.filter(([_name, config]) => !config.systemFont)
.map(([_name, config]) => config.fileName as string)
);
const additionalFontFiles = fontFiles
.filter((name) => !expectedFontFiles.has(name))
.map((name) => `frontend/static/webfonts/${name}`);
if (additionalFontFiles.length !== 0) {
additionalFontFiles.forEach((file) => addProblem("_additional", file));
}
if (Object.keys(problems).length === 0) {
console.log(`Fonts are all \u001b[32mvalid\u001b[0m`);
} else {
console.log(`Fonts are \u001b[31minvalid\u001b[0m`);
console.log(
Object.entries(problems)
.map(([fontName, problems]) => {
let label = `${fontName}`;
if (fontName === "_additional")
label =
"Additional font files not declared in frontend/src/ts/constants/fonts.ts";
return `${label}:\n ${problems
.map((error) => "\t- " + error)
.join("\n")}`;
})
.join("\n")
);
throw new Error("layouts with errors");
}
}
async function validateThemes(): Promise<void> {
const problems: Partial<Record<ThemeName | "_additional", string[]>> = {};
const addProblem = (
themeName: keyof typeof problems,
error: string
): void => {
problems[themeName] = [...(problems[themeName] ?? []), error];
};
//no missing files
const themeFiles = fs
.readdirSync("./static/themes")
.map((it) => it.substring(0, it.length - 4));
//missing theme files
ThemesList.filter((it) => !themeFiles.includes(it.name)).forEach((it) =>
addProblem(it.name, `missing file frontend/static/themes/${it.name}.css`)
);
//additional font files
const additionalThemeFiles = themeFiles
.filter((it) => !ThemesList.some((theme) => theme.name === it))
.map((it) => `frontend/static/layouts/${it}.json`);
if (additionalThemeFiles.length !== 0) {
problems._additional = additionalThemeFiles;
}
if (Object.keys(problems).length === 0) {
console.log(`Themes are all \u001b[32mvalid\u001b[0m`);
} else {
console.log(`Themes are \u001b[31minvalid\u001b[0m`);
console.log(
Object.entries(problems)
.map(([fontName, problems]) => {
let label = `${fontName}`;
if (fontName === "_additional")
label =
"Additional font files not declared in frontend/src/ts/constants/fonts.ts";
return `${label}:\n ${problems
.map((error) => "\t- " + error)
.join("\n")}`;
})
.join("\n")
);
throw new Error("themes with errors");
}
}
async function main(): Promise<void> {
const args = process.argv.slice(2);
// oxlint-disable-next-line prefer-set-has this error doesnt make sense
const flags = args.filter((arg) => arg.startsWith("-"));
const keys = args.filter((arg) => !arg.startsWith("-"));
const mainValidators = {
const mainValidators: Record<string, () => Promise<void>> = {
quotes: validateQuotes,
languages: validateLanguages,
layouts: validateLayouts,
challenges: validateChallenges,
fonts: validateFonts,
themes: validateThemes,
};
const validatorsIndex = {
@ -499,7 +714,12 @@ function main() {
Object.entries(mainValidators).map(([k, v]) => [k, [v]])
),
// add arbitrary keys and validator groupings down here
others: [validateChallenges, validateLayouts],
others: [
validateChallenges,
validateLayouts,
validateFonts,
validateThemes,
],
};
// flags
@ -514,13 +734,15 @@ function main() {
console.error(`There is no validator for key '${key}'.`);
if (!passWithNoValidators) process.exit(1);
} else if (!validateAll) {
//@ts-expect-error magic
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
validatorsIndex[key].forEach((validator) => tasks.add(validator));
}
}
if (tasks.size > 0) {
return Promise.all([...tasks].map((validator) => validator()));
await Promise.all([...tasks].map(async (validator) => validator()));
return;
}
}
main();
void main();

View file

@ -56,8 +56,8 @@
"lint-json-assets": "cd frontend && eslint \"./static/**/*.json\"",
"validate-json": "cd frontend && pnpm validate-json",
"check-quote-assets": "cd frontend && npm run validate-json quotes",
"check-language-assets": "cd frontend && npm run validate-json languages && turbo run test -- constants/languages",
"check-other-assets": "cd frontend && npm run validate-json others && turbo run test -- constants/layouts constants/themes constants/fonts",
"check-language-assets": "cd frontend && npm run validate-json languages",
"check-other-assets": "cd frontend && npm run validate-json others",
"knip": "knip"
},
"engines": {

67
pnpm-lock.yaml generated
View file

@ -173,7 +173,7 @@ importers:
version: link:../packages/typescript-config
'@redocly/cli':
specifier: 2.0.5
version: 2.0.5(@opentelemetry/api@1.8.0)(ajv@8.12.0)(core-js@3.37.1)(encoding@0.1.13)
version: 2.0.5(@opentelemetry/api@1.8.0)(ajv@8.17.1)(core-js@3.37.1)(encoding@0.1.13)
'@types/bcrypt':
specifier: 5.0.2
version: 5.0.2
@ -454,6 +454,9 @@ importers:
subset-font:
specifier: 2.3.0
version: 2.3.0
tsx:
specifier: 4.16.2
version: 4.16.2
typescript:
specifier: 5.5.4
version: 5.5.4
@ -3381,6 +3384,9 @@ packages:
ajv@8.12.0:
resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
ansi-align@3.0.1:
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
@ -3746,11 +3752,6 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
browserslist@4.25.1:
resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
browserslist@4.25.3:
resolution: {integrity: sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@ -5075,6 +5076,9 @@ packages:
fast-safe-stringify@2.1.1:
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
fast-uri@3.0.6:
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
fast-url-parser@1.1.3:
resolution: {integrity: sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==}
@ -8615,7 +8619,7 @@ packages:
superagent@7.1.6:
resolution: {integrity: sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==}
engines: {node: '>=6.4.0 <13 || >=14'}
deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net
deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net
superstatic@9.0.3:
resolution: {integrity: sha512-e/tmW0bsnQ/33ivK6y3CapJT0Ovy4pk/ohNPGhIAGU2oasoNLRQ1cv6enua09NU9w6Y0H/fBu07cjzuiWvLXxw==}
@ -8625,6 +8629,7 @@ packages:
supertest@6.2.3:
resolution: {integrity: sha512-3GSdMYTMItzsSYjnIcljxMVZKPW1J9kYHZY+7yLfD0wpPwww97GeImZC1oOk0S5+wYl2niJwuFusBJqwLqYM3g==}
engines: {node: '>=6.0.0'}
deprecated: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net
supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
@ -9810,7 +9815,7 @@ snapshots:
dependencies:
'@babel/compat-data': 7.28.0
'@babel/helper-validator-option': 7.27.1
browserslist: 4.25.1
browserslist: 4.25.3
lru-cache: 5.1.1
semver: 6.3.1
@ -11924,14 +11929,14 @@ snapshots:
require-from-string: 2.0.2
uri-js-replace: 1.0.1
'@redocly/cli@2.0.5(@opentelemetry/api@1.8.0)(ajv@8.12.0)(core-js@3.37.1)(encoding@0.1.13)':
'@redocly/cli@2.0.5(@opentelemetry/api@1.8.0)(ajv@8.17.1)(core-js@3.37.1)(encoding@0.1.13)':
dependencies:
'@opentelemetry/exporter-trace-otlp-http': 0.202.0(@opentelemetry/api@1.8.0)
'@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.8.0)
'@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.8.0)
'@opentelemetry/semantic-conventions': 1.34.0
'@redocly/openapi-core': 2.0.5(ajv@8.12.0)
'@redocly/respect-core': 2.0.5(ajv@8.12.0)
'@redocly/openapi-core': 2.0.5(ajv@8.17.1)
'@redocly/respect-core': 2.0.5(ajv@8.17.1)
abort-controller: 3.0.0
chokidar: 3.6.0
colorette: 1.4.0
@ -11982,11 +11987,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@redocly/openapi-core@2.0.5(ajv@8.12.0)':
'@redocly/openapi-core@2.0.5(ajv@8.17.1)':
dependencies:
'@redocly/ajv': 8.11.2
'@redocly/config': 0.28.0
ajv-formats: 2.1.1(ajv@8.12.0)
ajv-formats: 2.1.1(ajv@8.17.1)
colorette: 1.4.0
js-levenshtein: 1.1.6
js-yaml: 4.1.0
@ -11996,13 +12001,13 @@ snapshots:
transitivePeerDependencies:
- ajv
'@redocly/respect-core@2.0.5(ajv@8.12.0)':
'@redocly/respect-core@2.0.5(ajv@8.17.1)':
dependencies:
'@faker-js/faker': 7.6.0
'@noble/hashes': 1.8.0
'@redocly/ajv': 8.11.2
'@redocly/openapi-core': 2.0.5(ajv@8.12.0)
better-ajv-errors: 1.2.0(ajv@8.12.0)
'@redocly/openapi-core': 2.0.5(ajv@8.17.1)
better-ajv-errors: 1.2.0(ajv@8.17.1)
colorette: 2.0.20
jest-diff: 29.7.0
jest-matcher-utils: 29.7.0
@ -12812,6 +12817,10 @@ snapshots:
optionalDependencies:
ajv: 8.12.0
ajv-formats@2.1.1(ajv@8.17.1):
optionalDependencies:
ajv: 8.17.1
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@ -12826,6 +12835,13 @@ snapshots:
require-from-string: 2.0.2
uri-js: 4.4.1
ajv@8.17.1:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.0.6
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
ansi-align@3.0.1:
dependencies:
string-width: 4.2.3
@ -13149,11 +13165,11 @@ snapshots:
before-after-hook@3.0.2: {}
better-ajv-errors@1.2.0(ajv@8.12.0):
better-ajv-errors@1.2.0(ajv@8.17.1):
dependencies:
'@babel/code-frame': 7.27.1
'@humanwhocodes/momoa': 2.0.4
ajv: 8.12.0
ajv: 8.17.1
chalk: 4.1.2
jsonpointer: 5.0.1
leven: 3.1.0
@ -13260,13 +13276,6 @@ snapshots:
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.24.4)
browserslist@4.25.1:
dependencies:
caniuse-lite: 1.0.30001735
electron-to-chromium: 1.5.207
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.1)
browserslist@4.25.3:
dependencies:
caniuse-lite: 1.0.30001735
@ -14902,6 +14911,8 @@ snapshots:
fast-safe-stringify@2.1.1: {}
fast-uri@3.0.6: {}
fast-url-parser@1.1.3:
dependencies:
punycode: 1.4.1
@ -19784,12 +19795,6 @@ snapshots:
escalade: 3.2.0
picocolors: 1.1.1
update-browserslist-db@1.1.3(browserslist@4.25.1):
dependencies:
browserslist: 4.25.1
escalade: 3.2.0
picocolors: 1.1.1
update-browserslist-db@1.1.3(browserslist@4.25.3):
dependencies:
browserslist: 4.25.3