feat: establish TypeScript-aware frontend tooling

This commit is contained in:
donghyeon-ka
2026-07-26 13:41:23 +09:00
parent 1a1747c737
commit 0fed35586a
41 changed files with 1086 additions and 125 deletions
+96 -7
View File
@@ -2,12 +2,53 @@ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
/** @param {string} name @param {string} fallback */
function argumentValue(name, fallback) {
const index = process.argv.indexOf(name);
return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback;
}
const governancePath = argumentValue(
"--governance",
"config/contracts/registry-governance.json",
);
const artifactPath = argumentValue(
"--artifact",
"artifacts/quality/registries.json",
);
const governance = JSON.parse(
await readFile("config/contracts/registry-governance.json", "utf8"),
await readFile(governancePath, "utf8"),
);
const failures = [];
const owners = new Map();
const snapshots = [];
const rowsByRegistry = new Map();
const registryExtensions = [".js", ".jsx", ".mjs", ".ts", ".tsx", ".mts"];
/** @param {string} declaredPath */
async function resolveRegistrySource(declaredPath) {
const extension = path.extname(declaredPath);
const basePath = extension
? declaredPath.slice(0, -extension.length)
: declaredPath;
const candidates = [];
for (const candidateExtension of registryExtensions) {
const candidate = `${basePath}${candidateExtension}`;
try {
await access(candidate);
candidates.push(candidate);
} catch {
// A migration may legitimately replace the declared extension.
}
}
if (candidates.length > 1) {
failures.push(
`ambiguous registry source ${declaredPath}: ${candidates.join(", ")}`,
);
return null;
}
return candidates[0] ?? null;
}
for (const specification of governance.registries) {
if (owners.has(specification.registryId)) {
@@ -16,10 +57,11 @@ for (const specification of governance.registries) {
owners.set(specification.registryId, specification.owner);
let rows = specification.declaredRows;
const sourcePath = await resolveRegistrySource(specification.path);
try {
await access(specification.path);
if (!sourcePath) throw new Error("missing registry source");
const module = await import(
`${pathToFileURL(path.resolve(specification.path)).href}?registry-check=${Date.now()}`
`${pathToFileURL(path.resolve(sourcePath)).href}?registry-check=${Date.now()}`
);
rows = module[specification.exportName];
} catch {
@@ -31,6 +73,8 @@ for (const specification of governance.registries) {
continue;
}
rowsByRegistry.set(specification.registryId, rows);
for (const [rowName, row] of Object.entries(rows)) {
if (!row || typeof row !== "object" || Array.isArray(row)) {
failures.push(`${specification.registryId}.${rowName} is not an object`);
@@ -43,16 +87,61 @@ for (const specification of governance.registries) {
}
}
for (const field of specification.uniqueFields ?? []) {
const values = new Map();
for (const [rowName, row] of Object.entries(rows)) {
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
const value = row[field];
if (value === undefined) continue;
if (values.has(value)) {
failures.push(
`${specification.registryId}.${rowName} duplicates ${field}=${String(value)} from ${values.get(value)}`,
);
} else {
values.set(value, rowName);
}
}
}
snapshots.push({
registryId: specification.registryId,
owner: specification.owner,
source: specification.path,
source: sourcePath ?? specification.path,
rowCount: Object.keys(rows).length,
rows,
});
}
const sourceFiles = [
for (const specification of governance.registries) {
const rows = rowsByRegistry.get(specification.registryId);
if (!rows) continue;
for (const reference of specification.references ?? []) {
const targetRows = rowsByRegistry.get(reference.registryId);
if (!targetRows) {
failures.push(
`${specification.registryId} references unknown registry ${reference.registryId}`,
);
continue;
}
const targetValues = new Set(
Object.values(targetRows)
.filter((row) => row && typeof row === "object" && !Array.isArray(row))
.map((row) => row[reference.targetField])
.filter((value) => value !== undefined),
);
for (const [rowName, row] of Object.entries(rows)) {
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
const value = row[reference.field];
if (value !== undefined && !targetValues.has(value)) {
failures.push(
`${specification.registryId}.${rowName}.${reference.field} references unknown ${reference.registryId}.${reference.targetField}=${String(value)}`,
);
}
}
}
}
const sourceFiles = governance.sourceDirectories ?? [
"src/application",
"src/presentation",
"src/domain",
@@ -75,7 +164,7 @@ async function scanDirectory(directory) {
await scanDirectory(target);
continue;
}
if (!/\.(js|jsx|mjs)$/.test(entry.name)) continue;
if (!/\.(js|jsx|mjs|ts|tsx|mts)$/.test(entry.name)) continue;
const content = await readFile(target, "utf8");
for (const pattern of adHocPatterns) {
if (pattern.expression.test(content)) {
@@ -91,7 +180,7 @@ for (const sourceDirectory of sourceFiles) {
await mkdir("artifacts/quality", { recursive: true });
await writeFile(
"artifacts/quality/registries.json",
artifactPath,
`${JSON.stringify(
{
schemaVersion: 1,