Files
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

229 lines
7.2 KiB
TypeScript

import { readFile, readdir, stat } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { parseFrontmatter } from "../packages/plugin-runtime/src/parsers/frontmatter.ts";
const repoRoot = path.resolve(import.meta.dirname, "..");
const craftRoot = path.join(repoRoot, "craft");
const futureSectionsPath = path.join(craftRoot, "FUTURE_SECTIONS.md");
const skillManifestRoots = [
"skills",
"design-templates",
"plugins/_official/examples",
"docs/examples",
];
const pluginManifestRoot = "plugins/_official";
const slugPattern = /^[a-z0-9][a-z0-9-]*$/;
export type CraftReference = {
manifestPath: string;
slug: unknown;
};
export type CraftReferenceViolation = CraftReference & {
kind: "invalid" | "unresolved";
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isAbsenceError(error: unknown): boolean {
return isRecord(error) && (error["code"] === "ENOENT" || error["code"] === "ENOTDIR");
}
async function pathExists(filePath: string): Promise<boolean> {
try {
await stat(filePath);
return true;
} catch (error) {
if (isAbsenceError(error)) return false;
throw error;
}
}
function toRepositoryPath(root: string, filePath: string): string {
return path.relative(root, filePath).split(path.sep).join("/");
}
export function extractCraftRequiresSlugs(source: string): unknown[] {
const { data } = parseFrontmatter(source);
const od = data["od"];
if (!isRecord(od)) return [];
const craft = od["craft"];
if (!isRecord(craft)) return [];
const requires = craft["requires"];
return Array.isArray(requires) ? [...requires] : [];
}
export function extractPluginManifestContextCraftSlugs(source: string): unknown[] {
const manifest: unknown = JSON.parse(source);
if (!isRecord(manifest)) return [];
const od = manifest["od"];
if (!isRecord(od)) return [];
const context = od["context"];
if (!isRecord(context)) return [];
const craft = context["craft"];
return Array.isArray(craft) ? [...craft] : [];
}
export function findCraftReferenceViolations(
references: CraftReference[],
existingSlugs: ReadonlySet<string>,
futureSlugs: ReadonlySet<string>,
): CraftReferenceViolation[] {
const violations: CraftReferenceViolation[] = [];
for (const reference of references) {
if (typeof reference.slug !== "string" || !slugPattern.test(reference.slug)) {
violations.push({ ...reference, kind: "invalid" });
continue;
}
if (!existingSlugs.has(reference.slug) && !futureSlugs.has(reference.slug)) {
violations.push({ ...reference, kind: "unresolved" });
}
}
return violations;
}
async function collectNamedManifests(directory: string, fileName: string): Promise<string[]> {
if (!(await pathExists(directory))) return [];
const entries = await readdir(directory, { withFileTypes: true });
const manifests: string[] = [];
for (const entry of entries) {
const fullPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
manifests.push(...(await collectNamedManifests(fullPath, fileName)));
} else if (entry.isFile() && entry.name === fileName) {
manifests.push(fullPath);
}
}
return manifests;
}
export async function collectCraftReferences(root: string = repoRoot): Promise<CraftReference[]> {
const skillManifests = (
await Promise.all(
skillManifestRoots.map((manifestRoot) =>
collectNamedManifests(path.join(root, manifestRoot), "SKILL.md"),
),
)
).flat();
const pluginManifests = await collectNamedManifests(
path.join(root, pluginManifestRoot),
"open-design.json",
);
const references: CraftReference[] = [];
for (const manifestPath of skillManifests) {
const source = await readFile(manifestPath, "utf8");
for (const slug of extractCraftRequiresSlugs(source)) {
references.push({
manifestPath: toRepositoryPath(root, manifestPath),
slug,
});
}
}
for (const manifestPath of pluginManifests) {
const source = await readFile(manifestPath, "utf8");
for (const slug of extractPluginManifestContextCraftSlugs(source)) {
references.push({
manifestPath: toRepositoryPath(root, manifestPath),
slug,
});
}
}
return references.sort((left, right) => left.manifestPath.localeCompare(right.manifestPath));
}
async function collectExistingCraftSlugs(): Promise<Set<string>> {
const entries = await readdir(craftRoot, { withFileTypes: true });
const slugs = new Set<string>();
for (const entry of entries) {
if (!entry.isFile() || path.extname(entry.name) !== ".md") continue;
const slug = path.basename(entry.name, ".md");
if (slug === "README" || slug === "FUTURE_SECTIONS") continue;
if (slugPattern.test(slug)) slugs.add(slug);
}
return slugs;
}
function extractFutureCraftSlugs(source: string): Set<string> {
const slugs = new Set<string>();
for (const line of source.split(/\r?\n/)) {
const match = /^\s*[-*]\s+`?([a-z0-9][a-z0-9-]*)`?\s*$/.exec(line);
if (match?.[1]) slugs.add(match[1]);
}
return slugs;
}
async function collectFutureCraftSlugs(): Promise<Set<string>> {
if (!(await pathExists(futureSectionsPath))) return new Set();
return extractFutureCraftSlugs(await readFile(futureSectionsPath, "utf8"));
}
function formatSlug(slug: unknown): string {
return typeof slug === "string" ? `'${slug}'` : JSON.stringify(slug);
}
function printViolations(violations: CraftReferenceViolation[]): void {
const invalid = violations.filter((violation) => violation.kind === "invalid");
const unresolved = violations.filter((violation) => violation.kind === "unresolved");
if (invalid.length > 0) {
console.error("Invalid craft reference entries:");
for (const violation of invalid) {
console.error(`- ${violation.manifestPath}: ${formatSlug(violation.slug)}`);
}
console.error("Craft slugs must be strings containing lowercase letters, digits, and hyphens only.");
}
if (unresolved.length > 0) {
console.error("Unresolved craft reference slugs:");
for (const violation of unresolved) {
console.error(`- ${violation.manifestPath}: ${formatSlug(violation.slug)}`);
}
console.error("Add craft/<slug>.md, fix the typo, or list an intentional forward reference in craft/FUTURE_SECTIONS.md.");
}
}
export async function checkCraftReferences(): Promise<boolean> {
const references = await collectCraftReferences();
const existingSlugs = await collectExistingCraftSlugs();
const futureSlugs = await collectFutureCraftSlugs();
const violations = findCraftReferenceViolations(references, existingSlugs, futureSlugs);
const manifestCount = new Set(references.map((reference) => reference.manifestPath)).size;
if (violations.length > 0) {
printViolations(violations);
return false;
}
console.log(
`Craft reference check passed: ${references.length} references across ${manifestCount} manifests resolve or are explicitly planned.`,
);
return true;
}
const isMain = process.argv[1] ? import.meta.url === pathToFileURL(process.argv[1]).href : false;
if (isMain && !(await checkCraftReferences())) {
process.exitCode = 1;
}