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 path from "path";
const { writeFileSync } = process.getBuiltinModule("node:fs");
const path = process.getBuiltinModule("node:path");
const __dirname = import.meta.dirname;
const currentPath = __dirname;
const outDir = path.resolve(currentPath, "../src/common/messages/combinedMessages.prod.ts");
console.log(`Writing to ${outDir}`);
process.stdout.write(`Writing to ${outDir}\n`);
writeFileSync(
outDir,
`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 { parse } from "yaml";
import { objectToDotted } from "./messagelib.ts";
const fsPromises = process.getBuiltinModule("node:fs/promises");
const path = process.getBuiltinModule("node:path");
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();
function flattenMessages(src: Record<string, unknown>) {
function flattenMessages(src: unknown) {
return Object.fromEntries(
Object.entries(objectToDotted(src))
.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>>();
for (const file of files) {
const segments = file.split(/[/\\]/);
const locale = segments[segments.length - 1]!.replace(/\.yaml$/, "");
const content = await readFile(file, "utf-8");
localeData.set(locale, flattenMessages(parse(content) ?? {}));
const localeFilename = segments[segments.length - 1];
if (localeFilename === undefined) {
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";
@@ -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 { 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 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";
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 outDir = path.resolve(thisFileDir, "../src/common/messagesJson");
const out = {} as Record<string, { [key: string]: string | undefined }>;
for (const [key, value] of Object.entries(allMessages)) {
//@ts-ignore
for (const [lang, langValue] of Object.entries(allMessages[key])) {
for (const [lang, langValue] of Object.entries(value)) {
if (!out[lang]) out[lang] = {};
if (lang in value) {
out[lang][key] = langValue as string;
} else {
if (lang === "def") {
out[lang][key] = key;
} else {
out[lang][key] = undefined;
}
}
out[lang][key] = langValue;
}
}
+12 -13
View File
@@ -1,6 +1,6 @@
import { access, readFile } from "node:fs/promises";
import { dirname, relative, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const fsPromises = process.getBuiltinModule("node:fs/promises");
const path = process.getBuiltinModule("node:path");
const url = process.getBuiltinModule("node:url");
type InspectionError = {
check: "current-label" | "local-reference" | "retired-label";
@@ -20,7 +20,7 @@ const messageCataloguePath = "src/common/messagesJson/en.json";
const markdownLinkPattern = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^)]*["'])?\)/gu;
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 {
@@ -48,14 +48,14 @@ async function inspectLocalReferences(
const [pathPart] = target.split("#", 1);
if (!pathPart) continue;
checked++;
const referencedPath = resolve(repositoryRoot, dirname(documentPath), pathPart);
const referencedPath = path.resolve(repositoryRoot, path.dirname(documentPath), pathPart);
try {
await access(referencedPath);
await fsPromises.access(referencedPath);
} catch {
errors.push({
check: "local-reference",
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 documents = new Map<string, string>();
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 catalogue = JSON.parse(await readFile(resolve(repositoryRoot, messageCataloguePath), "utf8")) as Record<
string,
string
>;
const catalogue = JSON.parse(
await fsPromises.readFile(path.resolve(repositoryRoot, messageCataloguePath), "utf8")
) as Record<string, string>;
const requiredMessageKeys = [
"TweakMismatchResolve.Action.UseConfigured",
"TweakMismatchResolve.Action.UseMine",
@@ -132,7 +131,7 @@ async function runCli(): Promise<void> {
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) {
await runCli();
}
+14 -10
View File
@@ -1,27 +1,31 @@
// 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 { glob } from "tinyglobby";
import { dottedToObject } from "./messagelib";
const fsPromises = process.getBuiltinModule("node:fs/promises");
const path = process.getBuiltinModule("node:path");
const __dirname = import.meta.dirname;
const targetDir = resolve(join(__dirname, "../src/common/messagesJson/"));
console.log(`Target directory: ${targetDir}`);
const targetDir = path.resolve(path.join(__dirname, "../src/common/messagesJson/"));
process.stdout.write(`Target directory: ${targetDir}\n`);
const files = await glob(`*.json`, { expandDirectories: false, absolute: true, cwd: targetDir });
for (const file of files) {
const filePath = resolve(file);
console.log(`Processing file: ${filePath}`);
const content = await readFile(filePath, "utf-8");
const jsonDataSrc = JSON.parse(content);
const filePath = path.resolve(file);
process.stdout.write(`Processing file: ${filePath}\n`);
const content = await fsPromises.readFile(filePath, "utf-8");
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(
Object.entries(jsonDataSrc).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
);
const jsonData = dottedToObject(jsonDataD2);
const yamlData = stringify(jsonData, { indent: 2 });
const yamlFilePath = filePath.replace(/\.json$/, ".yaml").replace("Json", "YAML");
await writeFile(yamlFilePath, yamlData, "utf-8");
console.log(`Converted ${filePath} to ${yamlFilePath}`);
await fsPromises.writeFile(yamlFilePath, yamlData, "utf-8");
process.stdout.write(`Converted ${filePath} to ${yamlFilePath}\n`);
}
// console.dir(files, { depth: 0 });
+47 -36
View File
@@ -1,38 +1,49 @@
export function objectToDotted(obj: any, prefix = ""): Record<string, any> {
return Object.entries(obj).reduce(
(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>
);
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function dottedToObject(obj: Record<string, any>): Record<string, any> {
return Object.entries(obj).reduce(
(acc, [key, value]) => {
if (key.includes(" ")) {
// Return as is.
return { ...acc, [key]: value }; // Skip keys with spaces
}
const keys = key.split(".");
keys.reduce((nestedAcc, currKey, index) => {
if (currKey in nestedAcc && typeof nestedAcc[currKey] !== "object") {
nestedAcc[currKey] = { _value: nestedAcc[currKey] }; // Convert to object if not already
}
if (index === keys.length - 1) {
nestedAcc[currKey] = value;
} else {
nestedAcc[currKey] = nestedAcc[currKey] || {};
}
return nestedAcc[currKey];
}, acc);
return acc;
},
{} as Record<string, any>
);
export function objectToDotted(obj: unknown, prefix = ""): Record<string, unknown> {
if (!isRecord(obj)) {
throw new TypeError("Expected a message catalogue object");
}
const flattened: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
const newKey = prefix ? `${prefix}.${key}` : key;
if (isRecord(value)) {
Object.assign(flattened, objectToDotted(value, newKey));
} else {
flattened[newKey] = value;
}
}
return flattened;
}
export function dottedToObject(obj: unknown): Record<string, unknown> {
if (!isRecord(obj)) {
throw new TypeError("Expected a dotted message catalogue object");
}
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)
import { readFile, writeFile } from "fs/promises";
import { join, resolve } from "path";
import { parse } from "yaml";
import { glob } from "tinyglobby";
import { objectToDotted } from "./messagelib";
const fsPromises = process.getBuiltinModule("node:fs/promises");
const path = process.getBuiltinModule("node:path");
const __dirname = import.meta.dirname;
const targetDir = resolve(join(__dirname, "../src/common/messagesYAML/"));
console.log(`Target directory: ${targetDir}`);
const targetDir = path.resolve(path.join(__dirname, "../src/common/messagesYAML/"));
process.stdout.write(`Target directory: ${targetDir}\n`);
const files = await glob(`*.yaml`, { expandDirectories: false, absolute: true, cwd: targetDir });
for (const file of files) {
const filePath = resolve(file);
const content = await readFile(filePath, "utf-8");
const jsonDataSrc = parse(content);
const filePath = path.resolve(file);
const content = await fsPromises.readFile(filePath, "utf-8");
const jsonDataSrc: unknown = parse(content);
const jsonDataD2 = objectToDotted(jsonDataSrc);
const jsonData = Object.fromEntries(
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))
);
const yamlData = JSON.stringify(jsonData, null, 4) + "\n";
const yamlFilePath = filePath.replace(/\.yaml$/, ".json").replace("YAML", "Json");
await writeFile(yamlFilePath, yamlData, "utf-8");
console.log(`Converted ${filePath} to ${yamlFilePath}`);
await fsPromises.writeFile(yamlFilePath, yamlData, "utf-8");
process.stdout.write(`Converted ${filePath} to ${yamlFilePath}\n`);
}
// console.dir(files, { depth: 0 });
+2 -1
View File
@@ -12,6 +12,7 @@
"buildDev": "node esbuild.config.mjs dev",
"lint": "eslint --cache --cache-strategy content --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",
"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",
@@ -22,7 +23,7 @@
"prettyNoWrite": "prettier --config ./.prettierrc.mjs \"**/*.js\" \"**/*.ts\" \"**/*.json\" ",
"precheck:compatibility": "npm run build",
"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:bakejson": "tsx _tools/bakei18n.ts",
"i18n:format": "prettier --config .prettierrc.mjs --write --log-level error 'src/common/messagesJson/*.json' 'src/common/messages/*.ts'",