Check repository tools with community lint

This commit is contained in:
vorotamoroz
2026-07-27 09:07:20 +00:00
parent 2b95766d4f
commit f1e382c6ed
10 changed files with 151 additions and 98 deletions
+4 -3
View File
@@ -1,11 +1,12 @@
import { writeFileSync } from "fs";
import { allMessages } from "../src/common/messages/combinedMessages.dev.ts"; import { allMessages } from "../src/common/messages/combinedMessages.dev.ts";
import path from "path";
const { writeFileSync } = process.getBuiltinModule("node:fs");
const path = process.getBuiltinModule("node:path");
const __dirname = import.meta.dirname; const __dirname = import.meta.dirname;
const currentPath = __dirname; const currentPath = __dirname;
const outDir = path.resolve(currentPath, "../src/common/messages/combinedMessages.prod.ts"); const outDir = path.resolve(currentPath, "../src/common/messages/combinedMessages.prod.ts");
console.log(`Writing to ${outDir}`); process.stdout.write(`Writing to ${outDir}\n`);
writeFileSync( writeFileSync(
outDir, outDir,
`export const allMessages: Readonly<Record<string, Readonly<Record<string, string>>>> = ${JSON.stringify(allMessages, null, 4)};` `export const allMessages: Readonly<Record<string, Readonly<Record<string, string>>>> = ${JSON.stringify(allMessages, null, 4)};`
+13 -8
View File
@@ -1,14 +1,14 @@
import { readFile } from "fs/promises";
import { join, resolve } from "path";
import { glob } from "tinyglobby"; import { glob } from "tinyglobby";
import { parse } from "yaml"; import { parse } from "yaml";
import { objectToDotted } from "./messagelib.ts"; import { objectToDotted } from "./messagelib.ts";
const fsPromises = process.getBuiltinModule("node:fs/promises");
const path = process.getBuiltinModule("node:path");
const __dirname = import.meta.dirname; const __dirname = import.meta.dirname;
const targetDir = resolve(join(__dirname, "../src/common/messagesYAML/")); const targetDir = path.resolve(path.join(__dirname, "../src/common/messagesYAML/"));
const files = (await glob(`*.yaml`, { expandDirectories: false, absolute: true, cwd: targetDir })).sort(); const files = (await glob(`*.yaml`, { expandDirectories: false, absolute: true, cwd: targetDir })).sort();
function flattenMessages(src: Record<string, unknown>) { function flattenMessages(src: unknown) {
return Object.fromEntries( return Object.fromEntries(
Object.entries(objectToDotted(src)) Object.entries(objectToDotted(src))
.map(([key, value]) => [key.endsWith("._value") ? key.slice(0, -7) : key, value] as const) .map(([key, value]) => [key.endsWith("._value") ? key.slice(0, -7) : key, value] as const)
@@ -20,9 +20,14 @@ function flattenMessages(src: Record<string, unknown>) {
const localeData = new Map<string, Record<string, string>>(); const localeData = new Map<string, Record<string, string>>();
for (const file of files) { for (const file of files) {
const segments = file.split(/[/\\]/); const segments = file.split(/[/\\]/);
const locale = segments[segments.length - 1]!.replace(/\.yaml$/, ""); const localeFilename = segments[segments.length - 1];
const content = await readFile(file, "utf-8"); if (localeFilename === undefined) {
localeData.set(locale, flattenMessages(parse(content) ?? {})); throw new Error(`Could not determine the locale name for ${file}`);
}
const locale = localeFilename.replace(/\.yaml$/, "");
const content = await fsPromises.readFile(file, "utf-8");
const parsed: unknown = parse(content);
localeData.set(locale, flattenMessages(parsed ?? {}));
} }
const baseLocale = "en"; const baseLocale = "en";
@@ -55,4 +60,4 @@ const report = Object.fromEntries(
}) })
); );
console.log(JSON.stringify(report, null, 2)); process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
+2 -3
View File
@@ -1,9 +1,8 @@
import { writeFileSync } from "fs";
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "../src/common/rosetta"; import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "../src/common/rosetta";
import { allMessages } from "../src/common/messages/combinedMessages.dev.ts"; import { allMessages } from "../src/common/messages/combinedMessages.dev.ts";
import path from "path"; const { writeFileSync } = process.getBuiltinModule("node:fs");
const path = process.getBuiltinModule("node:path");
const thisFileDir = __dirname; const thisFileDir = __dirname;
const outDir = path.join(thisFileDir, "i18n"); const outDir = path.join(thisFileDir, "i18n");
+5 -14
View File
@@ -1,26 +1,17 @@
import { writeFileSync } from "fs";
import { allMessages } from "../src/common/messages/combinedMessages.prod.ts"; import { allMessages } from "../src/common/messages/combinedMessages.prod.ts";
const __dirname = import.meta.dirname; const __dirname = import.meta.dirname;
import path from "path";
const { writeFileSync } = process.getBuiltinModule("node:fs");
const path = process.getBuiltinModule("node:path");
const thisFileDir = __dirname; const thisFileDir = __dirname;
const outDir = path.resolve(thisFileDir, "../src/common/messagesJson"); const outDir = path.resolve(thisFileDir, "../src/common/messagesJson");
const out = {} as Record<string, { [key: string]: string | undefined }>; const out = {} as Record<string, { [key: string]: string | undefined }>;
for (const [key, value] of Object.entries(allMessages)) { for (const [key, value] of Object.entries(allMessages)) {
//@ts-ignore for (const [lang, langValue] of Object.entries(value)) {
for (const [lang, langValue] of Object.entries(allMessages[key])) {
if (!out[lang]) out[lang] = {}; if (!out[lang]) out[lang] = {};
if (lang in value) { out[lang][key] = langValue;
out[lang][key] = langValue as string;
} else {
if (lang === "def") {
out[lang][key] = key;
} else {
out[lang][key] = undefined;
}
}
} }
} }
+12 -13
View File
@@ -1,6 +1,6 @@
import { access, readFile } from "node:fs/promises"; const fsPromises = process.getBuiltinModule("node:fs/promises");
import { dirname, relative, resolve } from "node:path"; const path = process.getBuiltinModule("node:path");
import { fileURLToPath, pathToFileURL } from "node:url"; const url = process.getBuiltinModule("node:url");
type InspectionError = { type InspectionError = {
check: "current-label" | "local-reference" | "retired-label"; check: "current-label" | "local-reference" | "retired-label";
@@ -20,7 +20,7 @@ const messageCataloguePath = "src/common/messagesJson/en.json";
const markdownLinkPattern = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^)]*["'])?\)/gu; const markdownLinkPattern = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^)]*["'])?\)/gu;
function repositoryRootFromThisFile(): string { function repositoryRootFromThisFile(): string {
return resolve(dirname(fileURLToPath(import.meta.url)), ".."); return path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "..");
} }
function normaliseReferenceTarget(rawTarget: string): string { function normaliseReferenceTarget(rawTarget: string): string {
@@ -48,14 +48,14 @@ async function inspectLocalReferences(
const [pathPart] = target.split("#", 1); const [pathPart] = target.split("#", 1);
if (!pathPart) continue; if (!pathPart) continue;
checked++; checked++;
const referencedPath = resolve(repositoryRoot, dirname(documentPath), pathPart); const referencedPath = path.resolve(repositoryRoot, path.dirname(documentPath), pathPart);
try { try {
await access(referencedPath); await fsPromises.access(referencedPath);
} catch { } catch {
errors.push({ errors.push({
check: "local-reference", check: "local-reference",
file: documentPath, file: documentPath,
detail: `Missing local reference: ${relative(repositoryRoot, referencedPath)}`, detail: `Missing local reference: ${path.relative(repositoryRoot, referencedPath)}`,
}); });
} }
} }
@@ -68,14 +68,13 @@ export async function inspectTroubleshootingDocs(
const errors: InspectionError[] = []; const errors: InspectionError[] = [];
const documents = new Map<string, string>(); const documents = new Map<string, string>();
for (const guidePath of guidePaths) { for (const guidePath of guidePaths) {
documents.set(guidePath, await readFile(resolve(repositoryRoot, guidePath), "utf8")); documents.set(guidePath, await fsPromises.readFile(path.resolve(repositoryRoot, guidePath), "utf8"));
} }
const troubleshooting = documents.get("docs/troubleshooting.md")!; const troubleshooting = documents.get("docs/troubleshooting.md")!;
const catalogue = JSON.parse(await readFile(resolve(repositoryRoot, messageCataloguePath), "utf8")) as Record< const catalogue = JSON.parse(
string, await fsPromises.readFile(path.resolve(repositoryRoot, messageCataloguePath), "utf8")
string ) as Record<string, string>;
>;
const requiredMessageKeys = [ const requiredMessageKeys = [
"TweakMismatchResolve.Action.UseConfigured", "TweakMismatchResolve.Action.UseConfigured",
"TweakMismatchResolve.Action.UseMine", "TweakMismatchResolve.Action.UseMine",
@@ -132,7 +131,7 @@ async function runCli(): Promise<void> {
if (!result.ok) process.exitCode = 1; if (!result.ok) process.exitCode = 1;
} }
const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : undefined; const invokedPath = process.argv[1] ? url.pathToFileURL(path.resolve(process.argv[1])).href : undefined;
if (invokedPath === import.meta.url) { if (invokedPath === import.meta.url) {
await runCli(); await runCli();
} }
+14 -10
View File
@@ -1,27 +1,31 @@
// Convert Application convenient Message Resources (JSON) to Human-Editable format (YAML) // Convert Application convenient Message Resources (JSON) to Human-Editable format (YAML)
import { readFile, writeFile } from "fs/promises";
import { join, resolve } from "path";
import { stringify } from "yaml"; import { stringify } from "yaml";
import { glob } from "tinyglobby"; import { glob } from "tinyglobby";
import { dottedToObject } from "./messagelib"; import { dottedToObject } from "./messagelib";
const fsPromises = process.getBuiltinModule("node:fs/promises");
const path = process.getBuiltinModule("node:path");
const __dirname = import.meta.dirname; const __dirname = import.meta.dirname;
const targetDir = resolve(join(__dirname, "../src/common/messagesJson/")); const targetDir = path.resolve(path.join(__dirname, "../src/common/messagesJson/"));
console.log(`Target directory: ${targetDir}`); process.stdout.write(`Target directory: ${targetDir}\n`);
const files = await glob(`*.json`, { expandDirectories: false, absolute: true, cwd: targetDir }); const files = await glob(`*.json`, { expandDirectories: false, absolute: true, cwd: targetDir });
for (const file of files) { for (const file of files) {
const filePath = resolve(file); const filePath = path.resolve(file);
console.log(`Processing file: ${filePath}`); process.stdout.write(`Processing file: ${filePath}\n`);
const content = await readFile(filePath, "utf-8"); const content = await fsPromises.readFile(filePath, "utf-8");
const jsonDataSrc = JSON.parse(content); const jsonDataSrc: unknown = JSON.parse(content);
if (typeof jsonDataSrc !== "object" || jsonDataSrc === null || Array.isArray(jsonDataSrc)) {
throw new TypeError(`Expected ${filePath} to contain a JSON object`);
}
const jsonDataD2 = Object.fromEntries( const jsonDataD2 = Object.fromEntries(
Object.entries(jsonDataSrc).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) Object.entries(jsonDataSrc).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
); );
const jsonData = dottedToObject(jsonDataD2); const jsonData = dottedToObject(jsonDataD2);
const yamlData = stringify(jsonData, { indent: 2 }); const yamlData = stringify(jsonData, { indent: 2 });
const yamlFilePath = filePath.replace(/\.json$/, ".yaml").replace("Json", "YAML"); const yamlFilePath = filePath.replace(/\.json$/, ".yaml").replace("Json", "YAML");
await writeFile(yamlFilePath, yamlData, "utf-8"); await fsPromises.writeFile(yamlFilePath, yamlData, "utf-8");
console.log(`Converted ${filePath} to ${yamlFilePath}`); process.stdout.write(`Converted ${filePath} to ${yamlFilePath}\n`);
} }
// console.dir(files, { depth: 0 }); // console.dir(files, { depth: 0 });
+47 -36
View File
@@ -1,38 +1,49 @@
export function objectToDotted(obj: any, prefix = ""): Record<string, any> { function isRecord(value: unknown): value is Record<string, unknown> {
return Object.entries(obj).reduce( return typeof value === "object" && value !== null && !Array.isArray(value);
(acc, [key, value]) => {
const newKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
Object.assign(acc, objectToDotted(value, newKey));
} else {
acc[newKey] = value;
}
return acc;
},
{} as Record<string, any>
);
} }
export function dottedToObject(obj: Record<string, any>): Record<string, any> {
return Object.entries(obj).reduce( export function objectToDotted(obj: unknown, prefix = ""): Record<string, unknown> {
(acc, [key, value]) => { if (!isRecord(obj)) {
if (key.includes(" ")) { throw new TypeError("Expected a message catalogue object");
// Return as is. }
return { ...acc, [key]: value }; // Skip keys with spaces const flattened: Record<string, unknown> = {};
} for (const [key, value] of Object.entries(obj)) {
const keys = key.split("."); const newKey = prefix ? `${prefix}.${key}` : key;
keys.reduce((nestedAcc, currKey, index) => { if (isRecord(value)) {
if (currKey in nestedAcc && typeof nestedAcc[currKey] !== "object") { Object.assign(flattened, objectToDotted(value, newKey));
nestedAcc[currKey] = { _value: nestedAcc[currKey] }; // Convert to object if not already } else {
} flattened[newKey] = value;
if (index === keys.length - 1) { }
nestedAcc[currKey] = value; }
} else { return flattened;
nestedAcc[currKey] = nestedAcc[currKey] || {}; }
}
return nestedAcc[currKey]; export function dottedToObject(obj: unknown): Record<string, unknown> {
}, acc); if (!isRecord(obj)) {
return acc; throw new TypeError("Expected a dotted message catalogue object");
}, }
{} as Record<string, any> const nestedResult: Record<string, unknown> = {};
); for (const [key, value] of Object.entries(obj)) {
if (key.includes(" ")) {
nestedResult[key] = value;
continue;
}
const keys = key.split(".");
let nested = nestedResult;
for (const [index, currentKey] of keys.entries()) {
if (index === keys.length - 1) {
nested[currentKey] = value;
continue;
}
const currentValue = nested[currentKey];
if (isRecord(currentValue)) {
nested = currentValue;
continue;
}
const replacement = currentValue === undefined || currentValue === null ? {} : { _value: currentValue };
nested[currentKey] = replacement;
nested = replacement;
}
}
return nestedResult;
} }
+41
View File
@@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import { dottedToObject, objectToDotted } from "./messagelib";
describe("message catalogue conversion", () => {
it("flattens nested objects while preserving leaf values", () => {
expect(
objectToDotted({
dialogue: {
title: "Title",
options: ["first", "second"],
},
"literal key": "Literal",
})
).toEqual({
"dialogue.title": "Title",
"dialogue.options": ["first", "second"],
"literal key": "Literal",
});
});
it("preserves an existing leaf under _value when a dotted child follows it", () => {
expect(
dottedToObject({
section: "Base value",
"section.child": "Child value",
"literal key": "Literal",
})
).toEqual({
section: {
_value: "Base value",
child: "Child value",
},
"literal key": "Literal",
});
});
it("rejects non-object catalogue roots", () => {
expect(() => objectToDotted("not an object")).toThrow("Expected a message catalogue object");
expect(() => dottedToObject(["not", "an", "object"])).toThrow("Expected a dotted message catalogue object");
});
});
+11 -10
View File
@@ -1,29 +1,30 @@
// Convert Human-Editable format (YAML) to Application convenient Message Resources (JSON) // Convert Human-Editable format (YAML) to Application convenient Message Resources (JSON)
import { readFile, writeFile } from "fs/promises";
import { join, resolve } from "path";
import { parse } from "yaml"; import { parse } from "yaml";
import { glob } from "tinyglobby"; import { glob } from "tinyglobby";
import { objectToDotted } from "./messagelib"; import { objectToDotted } from "./messagelib";
const fsPromises = process.getBuiltinModule("node:fs/promises");
const path = process.getBuiltinModule("node:path");
const __dirname = import.meta.dirname; const __dirname = import.meta.dirname;
const targetDir = resolve(join(__dirname, "../src/common/messagesYAML/")); const targetDir = path.resolve(path.join(__dirname, "../src/common/messagesYAML/"));
console.log(`Target directory: ${targetDir}`); process.stdout.write(`Target directory: ${targetDir}\n`);
const files = await glob(`*.yaml`, { expandDirectories: false, absolute: true, cwd: targetDir }); const files = await glob(`*.yaml`, { expandDirectories: false, absolute: true, cwd: targetDir });
for (const file of files) { for (const file of files) {
const filePath = resolve(file); const filePath = path.resolve(file);
const content = await readFile(filePath, "utf-8"); const content = await fsPromises.readFile(filePath, "utf-8");
const jsonDataSrc = parse(content); const jsonDataSrc: unknown = parse(content);
const jsonDataD2 = objectToDotted(jsonDataSrc); const jsonDataD2 = objectToDotted(jsonDataSrc);
const jsonData = Object.fromEntries( const jsonData = Object.fromEntries(
Object.entries(jsonDataD2) Object.entries(jsonDataD2)
.map(([key, value]) => [key.endsWith("._value") ? key.slice(0, -7) : key, value]) .map(([key, value]): [string, unknown] => [key.endsWith("._value") ? key.slice(0, -7) : key, value])
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) .sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
); );
const yamlData = JSON.stringify(jsonData, null, 4) + "\n"; const yamlData = JSON.stringify(jsonData, null, 4) + "\n";
const yamlFilePath = filePath.replace(/\.yaml$/, ".json").replace("YAML", "Json"); const yamlFilePath = filePath.replace(/\.yaml$/, ".json").replace("YAML", "Json");
await writeFile(yamlFilePath, yamlData, "utf-8"); await fsPromises.writeFile(yamlFilePath, yamlData, "utf-8");
console.log(`Converted ${filePath} to ${yamlFilePath}`); process.stdout.write(`Converted ${filePath} to ${yamlFilePath}\n`);
} }
// console.dir(files, { depth: 0 }); // console.dir(files, { depth: 0 });
+2 -1
View File
@@ -12,6 +12,7 @@
"buildDev": "node esbuild.config.mjs dev", "buildDev": "node esbuild.config.mjs dev",
"lint": "eslint --cache --cache-strategy content --concurrency off src", "lint": "eslint --cache --cache-strategy content --concurrency off src",
"lint:community": "eslint --config eslint.community.config.mjs --concurrency off src", "lint:community": "eslint --config eslint.community.config.mjs --concurrency off src",
"lint:community:tools": "eslint --config eslint.community.config.mjs --concurrency off --max-warnings 0 _tools",
"svelte-check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings", "svelte-check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings",
"tsc-check": "tsc --noEmit", "tsc-check": "tsc --noEmit",
"tsc-check:apps": "tsc --noEmit -p src/apps/browser/tsconfig.json && tsc --noEmit -p src/apps/cli/tsconfig.json && tsc --noEmit -p src/apps/webapp/tsconfig.json && tsc --noEmit -p src/apps/webpeer/tsconfig.app.json && tsc --noEmit -p src/apps/webpeer/tsconfig.node.json", "tsc-check:apps": "tsc --noEmit -p src/apps/browser/tsconfig.json && tsc --noEmit -p src/apps/cli/tsconfig.json && tsc --noEmit -p src/apps/webapp/tsconfig.json && tsc --noEmit -p src/apps/webpeer/tsconfig.app.json && tsc --noEmit -p src/apps/webpeer/tsconfig.node.json",
@@ -22,7 +23,7 @@
"prettyNoWrite": "prettier --config ./.prettierrc.mjs \"**/*.js\" \"**/*.ts\" \"**/*.json\" ", "prettyNoWrite": "prettier --config ./.prettierrc.mjs \"**/*.js\" \"**/*.ts\" \"**/*.json\" ",
"precheck:compatibility": "npm run build", "precheck:compatibility": "npm run build",
"check:compatibility": "node utils/check-compatibility.js --file main.js --ios 15", "check:compatibility": "node utils/check-compatibility.js --file main.js --ios 15",
"check": "npm run tsc-check && npm run tsc-check:apps && npm run lint && npm run lint:community -- --quiet && npm run svelte-check && npm run check:compatibility", "check": "npm run tsc-check && npm run tsc-check:apps && npm run lint && npm run lint:community -- --quiet && npm run lint:community:tools && npm run svelte-check && npm run check:compatibility",
"i18n:bake": "npm run i18n:yaml2json && npm run i18n:bakejson && npm run i18n:format", "i18n:bake": "npm run i18n:yaml2json && npm run i18n:bakejson && npm run i18n:format",
"i18n:bakejson": "tsx _tools/bakei18n.ts", "i18n:bakejson": "tsx _tools/bakei18n.ts",
"i18n:format": "prettier --config .prettierrc.mjs --write --log-level error 'src/common/messagesJson/*.json' 'src/common/messages/*.ts'", "i18n:format": "prettier --config .prettierrc.mjs --write --log-level error 'src/common/messagesJson/*.json' 'src/common/messages/*.ts'",