Muse owns muse-shaped files now

This commit is contained in:
Endeavorance 2025-03-17 17:20:30 -04:00
parent 3e2ab6ec73
commit bf444ccb1a
23 changed files with 1288 additions and 201 deletions

View file

@ -1,19 +1,24 @@
import path from "node:path";
import {
UnvalidatedPlaybillSchema,
type UnknownComponent,
type UnvalidatedPlaybill,
type Ability,
type Blueprint,
type Item,
type Method,
Playbill,
type Resource,
type Rule,
type Species,
} from "@proscenium/playbill";
import { Glob } from "bun";
import z from "zod";
import { FileNotFoundError } from "#lib/errors";
import { loadYAMLFileOrFail } from "#util/files";
import { loadResourceFile } from "./resource";
import { loadYAMLFileOrFail } from "#util";
import { ComponentFile } from "./component-file";
import { FileNotFoundError } from "./errors";
const BindingFileSchema = z.object({
const BindingSchema = z.object({
extend: z.string().optional(),
id: z.string(),
id: z.string().default("playbill"),
name: z.string().default("Unnamed playbill"),
author: z.string().optional(),
description: z.string().optional(),
@ -23,10 +28,10 @@ const BindingFileSchema = z.object({
styles: z.array(z.string()).optional(),
});
type BindingFileContent = z.infer<typeof BindingFileSchema>;
type Binding = z.infer<typeof BindingSchema>;
export interface PlaybillBinding {
_raw: BindingFileContent;
export interface BoundPlaybill {
_raw: Binding;
// File information
bindingFilePath: string;
@ -40,11 +45,20 @@ export interface PlaybillBinding {
version: string;
description: string;
styles?: string[];
styles?: string;
playbill: UnvalidatedPlaybill;
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);
@ -66,29 +80,40 @@ export async function resolveBindingPath(bindingPath: string): Promise<string> {
export async function loadFromBinding(
bindingPath: string,
): Promise<PlaybillBinding> {
): Promise<BoundPlaybill> {
const resolvedBindingPath = await resolveBindingPath(bindingPath);
const yamlContent = await loadYAMLFileOrFail(resolvedBindingPath);
const binding = BindingFileSchema.parse(yamlContent);
const binding = BindingSchema.parse(yamlContent);
const fileGlobs = binding.files;
const bindingFileDirname = path.dirname(resolvedBindingPath);
let playbill: UnvalidatedPlaybill = UnvalidatedPlaybillSchema.parse({
$define: "playbill",
id: "blank",
name: "Unnamed playbill",
description: "Unnamed Playbill",
version: "0.0.1",
});
const playbill = new Playbill();
// If this is extending another binding, load that first
if (binding.extend) {
const pathFromHere = path.resolve(bindingFileDirname, binding.extend);
const extendBindingPath = await resolveBindingPath(pathFromHere);
const loadedBinding = await loadFromBinding(extendBindingPath);
playbill = loadedBinding.playbill;
playbill.extend(loadedBinding.playbill);
}
// Load any specified stylesheets
let styles = "";
if (binding.styles && binding.styles.length > 0) {
for (const style of binding.styles) {
const cssFilePath = path.resolve(bindingFileDirname, style);
const cssFile = Bun.file(cssFilePath);
const cssFileExists = await cssFile.exists();
if (cssFileExists) {
styles += `${await cssFile.text()}\n`;
} else {
throw new FileNotFoundError(cssFilePath);
}
}
}
// Scan for all files matching the globs
const allFilePaths: string[] = [];
for (const thisGlob of fileGlobs) {
@ -104,31 +129,110 @@ export async function loadFromBinding(
allFilePaths.push(...results);
}
const bindingFileAbsolutePath = path.resolve(bindingPath);
// Exclude the binding.yaml file
const filePathsWithoutBindingFile = allFilePaths.filter((filePath) => {
return filePath !== bindingFileAbsolutePath;
return filePath !== resolvedBindingPath;
});
// -- LOAD ASSOCIATED RESOURCE FILES -- //
const loadedResources: UnknownComponent[] = [];
// Load discovered component files
const componentFiles: ComponentFile[] = [];
const fileLoadPromises: Promise<void>[] = [];
for (const filepath of filePathsWithoutBindingFile) {
const loaded = await loadResourceFile(filepath);
loadedResources.push(...loaded);
const componentFile = new ComponentFile(filepath);
componentFiles.push(componentFile);
fileLoadPromises.push(componentFile.load());
}
playbill.id = binding.id;
await Promise.all(fileLoadPromises);
// Decorate the playbill with the binding metadata
playbill.name = binding.name;
playbill.author = binding.author ?? "Anonymous";
playbill.version = binding.version;
playbill.description = binding.description ?? "";
for (const resource of loadedResources) {
const resourceType = resource.$define;
const resourceMap = playbill[resourceType] as Record<
string,
typeof resource
>;
resourceMap[resource.id] = resource;
const abilities: Ability[] = [];
const bluprints: Blueprint[] = [];
const glossaries: Record<string, string[]>[] = [];
const methods: Method[] = [];
const species: Species[] = [];
const items: Item[] = [];
const rules: Rule[] = [];
const resources: Resource[] = [];
// Aggregate all components before adding to playbill to allow for
// ensuring components are added in a valid order
for (const file of componentFiles) {
// Load components from the file into the playbill
for (const component of file.components) {
switch (component.type) {
case "ability":
abilities.push(component.component);
break;
case "blueprint":
bluprints.push(component.component);
break;
case "glossary":
glossaries.push(component.component);
break;
case "method":
methods.push(component.component);
break;
case "species":
species.push(component.component);
break;
case "item":
items.push(component.component);
break;
case "rule":
rules.push(component.component);
break;
case "resource":
resources.push(component.component);
break;
default:
throw new Error("Unknown component type");
}
}
}
// Add resources first
for (const resource of resources) {
playbill.addResource(resource);
}
// Add abilities before methods, species, and blueprints
for (const ability of abilities) {
playbill.addAbility(ability);
}
// Add species before blueprints
for (const specie of species) {
playbill.addSpecies(specie);
}
// Add methods after abilities
for (const method of methods) {
playbill.addMethod(method);
}
// Add items
for (const item of items) {
playbill.addItem(item);
}
// Add blueprints after methods, species, items, and abilities
for (const blueprint of bluprints) {
playbill.addBlueprint(blueprint);
}
// Add all definitions
for (const glossary of glossaries) {
for (const [term, definitions] of Object.entries(glossary)) {
for (const definition of definitions) {
playbill.addDefinition(term, definition);
}
}
}
return {
@ -143,7 +247,7 @@ export async function loadFromBinding(
description: binding.description ?? "",
version: binding.version,
styles: binding.styles,
styles: styles.length > 0 ? styles : undefined,
playbill,
};