mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-28 01:39:29 +08:00
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:
parent
a1af28bb5d
commit
d2c627fcc8
7 changed files with 358 additions and 284 deletions
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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
67
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue