Extract independent MIT Mortar kit and contribution workflow
Mortar contribution checks / Mortar baseline and browser checks (push) Successful in 3m15s
Mortar contribution checks / Mortar baseline and browser checks (push) Successful in 3m15s
This commit is contained in:
commit
43fe76158c
108 files changed
+10081
No files matched your search
@@ -0,0 +1,172 @@
|
||||
import {
|
||||
readFile,
|
||||
readdir,
|
||||
realpath,
|
||||
lstat,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { createHash } from "node:crypto";
|
||||
import ts from "typescript";
|
||||
export const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
export async function checkKit(root = ROOT) {
|
||||
const findings = [];
|
||||
const add = (rule, file, message, line = 1) =>
|
||||
findings.push({ rule, file, line, severity: "error", message });
|
||||
const policy = JSON.parse(
|
||||
await readFile(resolve(root, "baseline-policy.json"), "utf8"),
|
||||
);
|
||||
const pkg = JSON.parse(await readFile(resolve(root, "package.json"), "utf8"));
|
||||
const manifests = [];
|
||||
for (const file of (await readdir(resolve(root, "manifests"))).sort()) {
|
||||
if (!file.endsWith(".json")) continue;
|
||||
try {
|
||||
manifests.push(
|
||||
JSON.parse(await readFile(resolve(root, "manifests", file), "utf8")),
|
||||
);
|
||||
} catch {
|
||||
add("M001", `manifests/${file}`, "Manifest must be valid JSON.");
|
||||
}
|
||||
}
|
||||
const actualExports = new Set();
|
||||
const ast = ts.createSourceFile(
|
||||
"index.ts",
|
||||
await readFile(resolve(root, "src/index.ts"), "utf8"),
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
);
|
||||
for (const node of ast.statements)
|
||||
if (
|
||||
ts.isExportDeclaration(node) &&
|
||||
!node.isTypeOnly &&
|
||||
node.exportClause &&
|
||||
ts.isNamedExports(node.exportClause)
|
||||
)
|
||||
for (const e of node.exportClause.elements)
|
||||
if (!e.isTypeOnly) actualExports.add(e.name.text);
|
||||
const ids = new Set();
|
||||
const names = new Set();
|
||||
const base = await realpath(root);
|
||||
for (const c of manifests) {
|
||||
if (
|
||||
!c.id ||
|
||||
ids.has(c.id) ||
|
||||
names.has(c.export) ||
|
||||
!actualExports.has(c.export)
|
||||
)
|
||||
add(
|
||||
"M001",
|
||||
`manifests/${c.id}.json`,
|
||||
"Use a unique component ID and an existing named export.",
|
||||
);
|
||||
ids.add(c.id);
|
||||
names.add(c.export);
|
||||
for (const [file, prefix] of [
|
||||
[c.source, "src/"],
|
||||
[c.preview?.source, "previews/"],
|
||||
]) {
|
||||
try {
|
||||
if (
|
||||
typeof file !== "string" ||
|
||||
!file.startsWith(prefix) ||
|
||||
file.includes("..") ||
|
||||
!(await lstat(resolve(root, file))).isFile() ||
|
||||
!(await realpath(resolve(root, file))).startsWith(base + "/")
|
||||
)
|
||||
throw Error();
|
||||
} catch {
|
||||
add(
|
||||
"M002",
|
||||
String(file),
|
||||
"Source and explicit preview must be regular files within their canonical repository directories.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const name of actualExports)
|
||||
if (!names.has(name))
|
||||
add("M001", "src/index.ts", `Add a manifest and preview for ${name}.`);
|
||||
for (const c of manifests)
|
||||
for (const id of c.contract?.dependencies || [])
|
||||
if (!ids.has(id))
|
||||
add(
|
||||
"M003",
|
||||
`manifests/${c.id}.json`,
|
||||
`Unknown component dependency ${id}.`,
|
||||
);
|
||||
async function inspect(directory) {
|
||||
for (const entry of await readdir(resolve(root, directory), {
|
||||
withFileTypes: true,
|
||||
})) {
|
||||
const file = `${directory}/${entry.name}`;
|
||||
if (entry.isSymbolicLink()) {
|
||||
add("M002", file, "Symlinks cannot hide shared source.");
|
||||
continue;
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
await inspect(file);
|
||||
continue;
|
||||
}
|
||||
if (!/\.tsx?$/.test(file)) continue;
|
||||
const source = await readFile(resolve(root, file), "utf8");
|
||||
const lines = source.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (
|
||||
/\b(?:fetch|XMLHttpRequest)\b|UI_TOKEN|@tony-ui\/control|ui\.tony\.codes|packages\/control/.test(
|
||||
lines[i],
|
||||
)
|
||||
)
|
||||
add(
|
||||
"M004",
|
||||
file,
|
||||
"Shared UI must not depend on hosted governance, credentials or network requests.",
|
||||
i + 1,
|
||||
);
|
||||
if (
|
||||
/(?:bg|text|border|ring)-(?:indigo|blue|green|red|amber|violet|slate|gray)-\d{2,3}\b/.test(
|
||||
lines[i],
|
||||
)
|
||||
)
|
||||
add(
|
||||
"M005",
|
||||
file,
|
||||
"Use semantic component tokens instead of a fixed Tailwind palette.",
|
||||
i + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
await inspect("src");
|
||||
const catalog = {
|
||||
schemaVersion: 1,
|
||||
package: pkg.name,
|
||||
version: pkg.version,
|
||||
policyVersion: policy.version,
|
||||
components: manifests,
|
||||
};
|
||||
return {
|
||||
passed: findings.length === 0,
|
||||
policyVersion: policy.version,
|
||||
findings,
|
||||
catalog,
|
||||
};
|
||||
}
|
||||
if (
|
||||
process.argv[1] &&
|
||||
import.meta.url === pathToFileURL(resolve(process.argv[1])).href
|
||||
) {
|
||||
const report = await checkKit();
|
||||
if (process.argv.includes("--write-catalog") && report.passed)
|
||||
await writeFile(
|
||||
resolve(ROOT, "catalog.json"),
|
||||
JSON.stringify(report.catalog, null, 2) + "\n",
|
||||
);
|
||||
const { catalog, ...result } = report;
|
||||
console.log(
|
||||
process.argv.includes("--format")
|
||||
? JSON.stringify(result, null, 2)
|
||||
: `${report.passed ? "PASS" : "FAIL"}: ${catalog.components.length} components; baseline ${report.policyVersion}\n${report.findings.map((f) => `${f.file}:${f.line} ${f.rule} ${f.message}`).join("\n")}`,
|
||||
);
|
||||
process.exitCode = report.passed ? 0 : 1;
|
||||
}
|
||||
Reference in new issue
Block a user