From 49e82d0a1397c794e3c17fe8eeab0e23b1733077 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Fri, 7 Mar 2025 15:01:52 -0500 Subject: [PATCH] Add extending by binding --- bun.lock | 4 +- extend-me/binding.yaml | 5 ++ extend-me/spires/lore/mortals.md | 7 +++ package.json | 2 +- playbill/binding.yaml | 1 + playbill/spires/methods/gourmond.yaml | 3 +- src/binding.ts | 85 ++++++++++++++++++++++++--- src/errors.ts | 14 ++++- src/files.ts | 21 +++++++ src/index.ts | 84 ++------------------------ src/resource.ts | 30 ++++++---- src/usage.ts | 1 + 12 files changed, 155 insertions(+), 102 deletions(-) create mode 100644 extend-me/binding.yaml create mode 100644 extend-me/spires/lore/mortals.md create mode 100644 src/files.ts diff --git a/bun.lock b/bun.lock index 3452ee7..425f54c 100644 --- a/bun.lock +++ b/bun.lock @@ -4,7 +4,7 @@ "": { "name": "@proscenium/muse", "dependencies": { - "@proscenium/playbill": "0.0.4", + "@proscenium/playbill": "0.0.9", "chalk": "^5.4.1", "yaml": "^2.7.0", "zod": "^3.24.1", @@ -37,7 +37,7 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], - "@proscenium/playbill": ["@proscenium/playbill@0.0.4", "https://git.astral.camp/api/packages/proscenium/npm/%40proscenium%2Fplaybill/-/0.0.4/playbill-0.0.4.tgz", { "peerDependencies": { "typescript": "^5" } }, "sha512-wLH04ac77LdCcYgkFmJwRDmRJ4W1I3W+KrbQ1oYQoTDY81Rrv+//ndx9HUMtXEDjM4oTcmhP3q4SxOLZgE744Q=="], + "@proscenium/playbill": ["@proscenium/playbill@0.0.9", "https://git.astral.camp/api/packages/proscenium/npm/%40proscenium%2Fplaybill/-/0.0.9/playbill-0.0.9.tgz", { "peerDependencies": { "typescript": "^5" } }, "sha512-Wx8q/1AM5LOSaFjwoRagU/J6lAGNZz2yorLrY0TrqHyuWKwkpzzPx0EAY9bBuq/+BDiebw2n84Mp+gHFJ5JyeQ=="], "@types/bun": ["@types/bun@1.2.3", "", { "dependencies": { "bun-types": "1.2.3" } }, "sha512-054h79ipETRfjtsCW9qJK8Ipof67Pw9bodFWmkfkaUaRiIQ1dIV2VTlheshlBx3mpKr0KeK8VqnMMCtgN9rQtw=="], diff --git a/extend-me/binding.yaml b/extend-me/binding.yaml new file mode 100644 index 0000000..3a7235b --- /dev/null +++ b/extend-me/binding.yaml @@ -0,0 +1,5 @@ +$binding: playbill +id: root +name: Root Playbill +author: "Endeavorance " +version: 0.0.1 diff --git a/extend-me/spires/lore/mortals.md b/extend-me/spires/lore/mortals.md new file mode 100644 index 0000000..8e9ef23 --- /dev/null +++ b/extend-me/spires/lore/mortals.md @@ -0,0 +1,7 @@ +--- +$define: lore +id: mortals +name: Mortals +--- + +Fleshy af diff --git a/package.json b/package.json index ee1dc1b..92aa680 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "chalk": "^5.4.1", "yaml": "^2.7.0", "zod": "^3.24.1", - "@proscenium/playbill": "0.0.4" + "@proscenium/playbill": "0.0.9" }, "scripts": { "fmt": "bunx --bun biome check --fix", diff --git a/playbill/binding.yaml b/playbill/binding.yaml index fd2edbc..bff6837 100644 --- a/playbill/binding.yaml +++ b/playbill/binding.yaml @@ -1,4 +1,5 @@ $binding: playbill +extends: ../extend-me/binding.yaml id: the-great-spires name: The Great Spires author: "Endeavorance " diff --git a/playbill/spires/methods/gourmond.yaml b/playbill/spires/methods/gourmond.yaml index ba16391..3f8f1f2 100644 --- a/playbill/spires/methods/gourmond.yaml +++ b/playbill/spires/methods/gourmond.yaml @@ -11,8 +11,7 @@ $define: ability id: identify-poison name: Identify Poison type: action -costs: - ap: 1 +ap: 1 roll: focus boons: - You know the antidote to the detected poison diff --git a/src/binding.ts b/src/binding.ts index 2a734fc..22ca3b1 100644 --- a/src/binding.ts +++ b/src/binding.ts @@ -1,10 +1,17 @@ import path from "node:path"; import { Glob } from "bun"; -import YAML from "yaml"; import z from "zod"; +import { + getEmptyPlaybill, + type AnyResource, + type Playbill, +} from "@proscenium/playbill"; +import { loadYAMLFileOrFail } from "./files"; +import { loadResourceFile } from "./resource"; const BindingFileSchema = z.object({ $binding: z.literal("playbill"), + extends: z.string().optional(), id: z.string(), name: z.string().default("Unnamed playbill"), author: z.string().optional(), @@ -17,25 +24,38 @@ type BindingFileContent = z.infer; export interface PlaybillBinding { _raw: BindingFileContent; + + // File information bindingFilePath: string; includedFiles: string[]; + // Binding properties id: string; name: string; author: string; version: string; description: string; + + playbill: Playbill; } -export function parseBindingFile( - text: string, +export async function loadFromBinding( filePath: string, -): PlaybillBinding { - const parsed = YAML.parse(text); - const binding = BindingFileSchema.parse(parsed); +): Promise { + const yamlContent = await loadYAMLFileOrFail(filePath); + const binding = BindingFileSchema.parse(yamlContent); const fileGlobs = binding.files; const bindingFileDirname = filePath.split("/").slice(0, -1).join("/"); + let basePlaybill: Playbill | null = null; + + // If this is extending another binding, load that first + if (binding.extends) { + console.log(binding); + const extendBindingPath = path.resolve(bindingFileDirname, binding.extends); + basePlaybill = (await loadFromBinding(extendBindingPath)).playbill; + } + const allFilePaths: string[] = []; for (const thisGlob of fileGlobs) { @@ -52,19 +72,68 @@ export function parseBindingFile( } const bindingFileAbsolutePath = path.resolve(filePath); - const filteredFilePaths = allFilePaths.filter((filePath) => { + const filePathsWithoutBindingFile = allFilePaths.filter((filePath) => { return filePath !== bindingFileAbsolutePath; }); + // -- LOAD ASSOCIATED RESOURCE FILES -- // + const loadedResources: AnyResource[] = []; + for (const filepath of filePathsWithoutBindingFile) { + const loaded = await loadResourceFile(filepath); + loadedResources.push(...loaded); + } + + // -- COMPILE PLAYBILL FROM RESOURCES --// + const playbill = basePlaybill ?? getEmptyPlaybill(); + + playbill.id = binding.id; + playbill.name = binding.name; + playbill.author = binding.author ?? "Anonymous"; + playbill.version = binding.version; + playbill.description = binding.description ?? ""; + + for (const resource of loadedResources) { + switch (resource.$define) { + case "ability": + playbill.abilities.push(resource); + break; + case "species": + playbill.species.push(resource); + break; + case "entity": + playbill.entities.push(resource); + break; + case "item": + playbill.items.push(resource); + break; + case "tag": + playbill.tags.push(resource); + break; + case "method": + playbill.methods.push(resource); + break; + case "lore": + playbill.lore.push(resource); + break; + case "rule": + playbill.rules.push(resource); + break; + case "playbill": + throw new Error("Cannot load playbills rn dawg"); + } + } + return { _raw: binding, bindingFilePath: filePath, - includedFiles: filteredFilePaths, + includedFiles: filePathsWithoutBindingFile, id: binding.id, name: binding.name, author: binding.author ?? "Anonymous", description: binding.description ?? "", version: binding.version, + + playbill, }; } diff --git a/src/errors.ts b/src/errors.ts index eb5bcbb..e688ebf 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -4,7 +4,7 @@ export class MuseError extends Error { } } -export class ResourceFileError extends MuseError { +export class FileError extends MuseError { filePath: string; details?: string; @@ -18,3 +18,15 @@ export class ResourceFileError extends MuseError { return `-- ${this.message} --\nFile Path: ${this.filePath}\nDetails: ${this.details}`; } } + +export class MalformedResourceFileError extends FileError { } + +export class FileNotFoundError extends FileError { + constructor(filePath: string) { + super("File not found", filePath); + this.message = "File not found"; + this.filePath = filePath; + } +} + +export class CLIError extends MuseError { } diff --git a/src/files.ts b/src/files.ts new file mode 100644 index 0000000..6f54852 --- /dev/null +++ b/src/files.ts @@ -0,0 +1,21 @@ +import YAML from "yaml"; +import { FileError, FileNotFoundError } from "./errors"; + +export async function loadFileOrFail(filePath: string): Promise { + const fileToLoad = Bun.file(filePath); + const fileExists = await fileToLoad.exists(); + + if (!fileExists) { + throw new FileNotFoundError(`File not found: ${filePath}`); + } + + return await fileToLoad.text(); +} + +export async function loadYAMLFileOrFail(filePath: string): Promise { + try { + return YAML.parse(await loadFileOrFail(filePath)); + } catch (error) { + throw new FileError("Failed to parse YAML file", filePath, `${error}`); + } +} diff --git a/src/index.ts b/src/index.ts index 0ec2a2a..2cd7db7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,34 +1,15 @@ import { parseArgs } from "node:util"; -import { - type AnyResource, - ValidatedPlaybillSchema, - checkDirectives, - getEmptyPlaybill, -} from "@proscenium/playbill"; +import { ValidatedPlaybillSchema, checkDirectives } from "@proscenium/playbill"; import chalk from "chalk"; -import { parseBindingFile } from "./binding"; -import { loadResourceFile } from "./resource"; +import { loadFromBinding } from "./binding"; import { usage } from "./usage"; -import { ResourceFileError } from "./errors"; +import { CLIError, MuseError } from "./errors"; enum ExitCode { Success = 0, Error = 1, } -class CLIError extends Error { } - -async function loadFileOrFail(filePath: string): Promise { - const fileToLoad = Bun.file(filePath); - const fileExists = await fileToLoad.exists(); - - if (!fileExists) { - throw new Error(`File not found: ${filePath}`); - } - - return await fileToLoad.text(); -} - async function main(): Promise { // -- ARGUMENT PARSING -- // const { values: options, positionals: args } = parseArgs({ @@ -97,68 +78,15 @@ async function main(): Promise { verboseLog(`Building Playbill with binding: ${bindingPath}`); - const bindingFileContent = await loadFileOrFail(bindingPath); - const binding = parseBindingFile(bindingFileContent, bindingPath); + const binding = await loadFromBinding(bindingPath); verboseLog( `↳ Binding loaded with ${binding.includedFiles.length} associated resources`, ); - // -- RESOURCE FILES -- // - verboseLog("Loading resources"); - const loadedResources: AnyResource[] = []; - for (const filepath of binding.includedFiles) { - verboseLog(`↳ Loading ${filepath}`); - const loaded = await loadResourceFile(filepath); - verboseLog(` ↳ Loaded ${loaded.length} resources`); - loadedResources.push(...loaded); - } - - verboseLog("Constructing playbill"); - - // -- COMPILE RESOURCES --// - const playbill = getEmptyPlaybill(); - - playbill.id = binding.id; - playbill.name = binding.name; - playbill.author = binding.author; - playbill.version = binding.version; - playbill.description = binding.description; - - for (const resource of loadedResources) { - switch (resource.$define) { - case "ability": - playbill.abilities.push(resource); - break; - case "species": - playbill.species.push(resource); - break; - case "entity": - playbill.entities.push(resource); - break; - case "item": - playbill.items.push(resource); - break; - case "tag": - playbill.tags.push(resource); - break; - case "method": - playbill.methods.push(resource); - break; - case "lore": - playbill.lore.push(resource); - break; - case "rule": - playbill.rules.push(resource); - break; - case "playbill": - throw new Error("Cannot load playbills rn dawg"); - } - } - // -- VALDATE PLAYBILL -- // verboseLog("Validating playbill"); - const validatedPlaybill = ValidatedPlaybillSchema.safeParse(playbill); + const validatedPlaybill = ValidatedPlaybillSchema.safeParse(binding.playbill); if (!validatedPlaybill.success) { console.error("Error validating playbill"); @@ -202,7 +130,7 @@ try { const exitCode = await main(); process.exit(exitCode); } catch (error) { - if (error instanceof ResourceFileError) { + if (error instanceof MuseError) { console.error(chalk.red(error.fmt())); } else { console.error("An unexpected error occurred"); diff --git a/src/resource.ts b/src/resource.ts index 228e7b9..2ebe522 100644 --- a/src/resource.ts +++ b/src/resource.ts @@ -5,8 +5,9 @@ import { } from "@proscenium/playbill"; import YAML, { YAMLParseError } from "yaml"; import { ZodError } from "zod"; -import { ResourceFileError } from "./errors"; +import { MalformedResourceFileError } from "./errors"; import { extractFrontmatter, extractMarkdown } from "./markdown"; +import { loadFileOrFail } from "./files"; type FileFormat = "yaml" | "markdown"; @@ -18,13 +19,13 @@ function loadYamlResourceFile(filePath: string, text: string): AnyResource[] { const parsedDocs = YAML.parseAllDocuments(text); if (parsedDocs.some((doc) => doc.toJS() === null)) { - throw new ResourceFileError("Encountered NULL resource", filePath); + throw new MalformedResourceFileError("Encountered NULL resource", filePath); } const errors = parsedDocs.flatMap((doc) => doc.errors); if (errors.length > 0) { - throw new ResourceFileError( + throw new MalformedResourceFileError( "Error parsing YAML resource", filePath, errors.map((e) => e.message).join(", "), @@ -35,10 +36,20 @@ function loadYamlResourceFile(filePath: string, text: string): AnyResource[] { for (const doc of parsedDocs) { const raw = doc.toJS(); - const parsed = AnyResourceSchema.parse(raw); - const type = parsed.$define; + const parsed = AnyResourceSchema.safeParse(raw); + + if (!parsed.success) { + throw new MalformedResourceFileError( + "Failed to parse resource", + filePath, + `Schema validation failed: ${parsed.error.message}`, + ); + } + + const parsedResource = parsed.data; + const type = parsedResource.$define; const schemaToUse = ResourceMap[type]; - const validated = schemaToUse.parse(parsed); + const validated = schemaToUse.parse(parsedResource); collection.push(validated); } @@ -62,7 +73,7 @@ function loadMarkdownResourceFile( return [parsed]; } catch (e) { if (e instanceof YAMLParseError) { - throw new ResourceFileError( + throw new MalformedResourceFileError( "Error parsing Markdown frontmatter", filePath, e.message, @@ -70,7 +81,7 @@ function loadMarkdownResourceFile( } if (e instanceof ZodError) { - throw new ResourceFileError( + throw new MalformedResourceFileError( "Error parsing resource file", filePath, e.message, @@ -92,8 +103,7 @@ function loadMarkdownResourceFile( export async function loadResourceFile( filePath: string, ): Promise { - const file = Bun.file(filePath); - const text = await file.text(); + const text = await loadFileOrFail(filePath); const format = determineFileFormat(filePath); switch (format) { diff --git a/src/usage.ts b/src/usage.ts index a9a5a1a..05ba37d 100644 --- a/src/usage.ts +++ b/src/usage.ts @@ -13,5 +13,6 @@ Options: --write Write the output to a file. If not specified, outputs to stdout --outfile Specify the output file path [default: playbill.json] --verbose, -v Verbose output + --minify, -m Minify the output JSON --help, -h Show this help message `.trim();