177 lines
5.1 KiB
TypeScript
177 lines
5.1 KiB
TypeScript
import path from "node:path";
|
|
import { type AnyPlaybillComponent, Playbill } from "@proscenium/playbill";
|
|
import { Glob } from "bun";
|
|
import z from "zod";
|
|
import { loadYAMLFileOrFail } from "#util";
|
|
import { ComponentFile } from "./component-file";
|
|
import { FileNotFoundError } from "./errors";
|
|
|
|
// binding.yaml file schema
|
|
const BindingSchema = z.object({
|
|
include: z.array(z.string()).default([]),
|
|
|
|
name: z.string().default("Unnamed playbill"),
|
|
author: z.string().default("Someone"),
|
|
description: z.string().default("No description provided"),
|
|
version: z.string().default("0.0.0"),
|
|
|
|
files: z.array(z.string()).default(["**/*.yaml", "**/*.md"]),
|
|
omitDefaultStyles: z.boolean().default(false),
|
|
styles: z
|
|
.union([z.string(), z.array(z.string())])
|
|
.transform((val) => (Array.isArray(val) ? val : [val]))
|
|
.default([]),
|
|
terms: z.record(z.string(), z.string()).default({}),
|
|
});
|
|
|
|
export interface BoundPlaybill {
|
|
// File information
|
|
bindingFilePath: string;
|
|
bindingFileDirname: string;
|
|
files: string[];
|
|
componentFiles: ComponentFile[];
|
|
|
|
// Binding properties
|
|
name: string;
|
|
author: string;
|
|
version: string;
|
|
description: string;
|
|
|
|
omitDefaultStyles: boolean;
|
|
styles: string;
|
|
|
|
playbill: Playbill;
|
|
}
|
|
|
|
/**
|
|
* Given a path, find the binding.yaml file
|
|
* If the path is to a directory, check for a binding.yaml inside
|
|
* If the path is to a file, check if it exists and is a binding.yaml
|
|
* Otherwise throw an error
|
|
*
|
|
* @param bindingPath - The path to the binding file
|
|
* @returns The absolute resolved path to the binding file
|
|
*/
|
|
export async function resolveBindingPath(bindingPath: string): Promise<string> {
|
|
// If the path does not specify a filename, use binding.yaml
|
|
const inputLocation = Bun.file(bindingPath);
|
|
|
|
// If it is a directory, try again seeking a binding.yaml inside
|
|
const stat = await inputLocation.stat();
|
|
if (stat.isDirectory()) {
|
|
return resolveBindingPath(path.resolve(bindingPath, "binding.yaml"));
|
|
}
|
|
|
|
// If it doesnt exist, bail
|
|
if (!(await inputLocation.exists())) {
|
|
throw new FileNotFoundError(bindingPath);
|
|
}
|
|
|
|
// Resolve the path
|
|
return path.resolve(bindingPath);
|
|
}
|
|
|
|
export async function loadFromBinding(
|
|
bindingPath: string,
|
|
): Promise<BoundPlaybill> {
|
|
const resolvedBindingPath = await resolveBindingPath(bindingPath);
|
|
const yamlContent = await loadYAMLFileOrFail(resolvedBindingPath);
|
|
const binding = BindingSchema.parse(yamlContent);
|
|
const fileGlobs = binding.files;
|
|
const bindingFileDirname = path.dirname(resolvedBindingPath);
|
|
|
|
const playbill = new Playbill(
|
|
binding.name,
|
|
binding.author,
|
|
binding.description,
|
|
binding.version,
|
|
);
|
|
|
|
// If this is extending another binding, load that first
|
|
for (const includePath of binding.include) {
|
|
const pathFromHere = path.resolve(bindingFileDirname, includePath);
|
|
const extendBindingPath = await resolveBindingPath(pathFromHere);
|
|
const loadedBinding = await loadFromBinding(extendBindingPath);
|
|
playbill.extend(loadedBinding.playbill);
|
|
}
|
|
|
|
// Load any specified stylesheets
|
|
const loadedStyles: string[] = [];
|
|
for (const style of binding.styles) {
|
|
const cssFilePath = path.resolve(bindingFileDirname, style);
|
|
const cssFile = Bun.file(cssFilePath);
|
|
const cssFileExists = await cssFile.exists();
|
|
|
|
if (cssFileExists) {
|
|
loadedStyles.push(await cssFile.text());
|
|
} else {
|
|
throw new FileNotFoundError(cssFilePath);
|
|
}
|
|
}
|
|
const styles = loadedStyles.join("\n");
|
|
|
|
// Scan for all files matching the globs
|
|
const allFilePaths: string[] = [];
|
|
|
|
for (const thisGlob of fileGlobs) {
|
|
const glob = new Glob(thisGlob);
|
|
|
|
const results = glob.scanSync({
|
|
cwd: bindingFileDirname,
|
|
absolute: true,
|
|
followSymlinks: true,
|
|
onlyFiles: true,
|
|
});
|
|
|
|
allFilePaths.push(...results);
|
|
}
|
|
|
|
// Exclude the binding.yaml file
|
|
const filePathsWithoutBindingFile = allFilePaths.filter((filePath) => {
|
|
return filePath !== resolvedBindingPath;
|
|
});
|
|
|
|
// Load discovered component files
|
|
const componentFiles: ComponentFile[] = [];
|
|
const fileLoadPromises: Promise<void>[] = [];
|
|
|
|
for (const filepath of filePathsWithoutBindingFile) {
|
|
const componentFile = new ComponentFile(filepath);
|
|
componentFiles.push(componentFile);
|
|
fileLoadPromises.push(componentFile.load());
|
|
}
|
|
|
|
await Promise.all(fileLoadPromises);
|
|
|
|
// Aggregate all loaded components
|
|
const loadedComponents: AnyPlaybillComponent[] = [];
|
|
for (const file of componentFiles) {
|
|
loadedComponents.push(...file.components);
|
|
}
|
|
|
|
// Add all components to the playbill
|
|
// (This method ensures proper addition order maintain correctness)
|
|
playbill.addManyComponents(loadedComponents);
|
|
|
|
// Add all definitions
|
|
for (const [term, definition] of Object.entries(binding.terms)) {
|
|
playbill.addDefinition(term, definition);
|
|
}
|
|
|
|
return {
|
|
bindingFilePath: bindingPath,
|
|
bindingFileDirname,
|
|
files: filePathsWithoutBindingFile,
|
|
componentFiles,
|
|
|
|
name: binding.name,
|
|
author: binding.author ?? "Anonymous",
|
|
description: binding.description ?? "",
|
|
version: binding.version,
|
|
|
|
omitDefaultStyles: binding.omitDefaultStyles,
|
|
styles,
|
|
|
|
playbill,
|
|
};
|
|
}
|