This commit is contained in:
Endeavorance 2025-02-24 14:03:04 -05:00
parent 45580288e1
commit 8b276cc226
5 changed files with 145 additions and 119 deletions

View file

@ -4,7 +4,7 @@
"": { "": {
"name": "@proscenium/muse", "name": "@proscenium/muse",
"dependencies": { "dependencies": {
"@proscenium/playbill": "0.0.2", "@proscenium/playbill": "0.0.5",
"chalk": "^5.4.1", "chalk": "^5.4.1",
"yaml": "^2.7.0", "yaml": "^2.7.0",
"zod": "^3.24.1", "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=="], "@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.2", "https://git.astral.camp/api/packages/proscenium/npm/%40proscenium%2Fplaybill/-/0.0.2/playbill-0.0.2.tgz", { "peerDependencies": { "typescript": "^5" } }, "sha512-zoYCHbm7DGmDRbB+tkQGPQTA5eR0xDHfyv+cp+3JfLy8n/6hOJvx+pG51emD1jBP+CecUE819nFriJqDf+ow0A=="], "@proscenium/playbill": ["@proscenium/playbill@0.0.5", "https://git.astral.camp/api/packages/proscenium/npm/%40proscenium%2Fplaybill/-/0.0.5/playbill-0.0.5.tgz", { "peerDependencies": { "typescript": "^5" } }, "sha512-4TzEe3LpZ2WAep/OWtuNoGiRb2NilfBBN9KQ65/sYSkmBq4Wtdploqo/7+R1Twja/AvOBGgw+Art12beywhqtQ=="],
"@types/bun": ["@types/bun@1.2.3", "", { "dependencies": { "bun-types": "1.2.3" } }, "sha512-054h79ipETRfjtsCW9qJK8Ipof67Pw9bodFWmkfkaUaRiIQ1dIV2VTlheshlBx3mpKr0KeK8VqnMMCtgN9rQtw=="], "@types/bun": ["@types/bun@1.2.3", "", { "dependencies": { "bun-types": "1.2.3" } }, "sha512-054h79ipETRfjtsCW9qJK8Ipof67Pw9bodFWmkfkaUaRiIQ1dIV2VTlheshlBx3mpKr0KeK8VqnMMCtgN9rQtw=="],

View file

@ -14,7 +14,7 @@
"chalk": "^5.4.1", "chalk": "^5.4.1",
"yaml": "^2.7.0", "yaml": "^2.7.0",
"zod": "^3.24.1", "zod": "^3.24.1",
"@proscenium/playbill": "0.0.2" "@proscenium/playbill": "0.0.5"
}, },
"scripts": { "scripts": {
"fmt": "bunx --bun biome check --fix", "fmt": "bunx --bun biome check --fix",

View file

@ -3,31 +3,36 @@ import { Glob } from "bun";
import YAML from "yaml"; import YAML from "yaml";
import z from "zod"; import z from "zod";
const BindingSchema = z.object({ const BindingFileSchema = z.object({
$binding: z.literal("playbill"), $binding: z.literal("playbill"),
id: z.string(), id: z.string(),
name: z.string().default("Unnamed playbill"), name: z.string().default("Unnamed playbill"),
author: z.string().optional(), author: z.string().optional(),
description: z.string().optional(),
version: z.string(), version: z.string(),
files: z.array(z.string()).default(["**/*.yaml", "**/*.md"]), files: z.array(z.string()).default(["**/*.yaml", "**/*.md"]),
}); });
type Binding = z.infer<typeof BindingSchema>; type BindingFileContent = z.infer<typeof BindingFileSchema>;
export interface PlaybillBinding { export interface PlaybillBinding {
info: Binding; _raw: BindingFileContent;
bindingPath: string; bindingFilePath: string;
files: string[]; includedFiles: string[];
id: string;
name: string;
author: string;
version: string;
description: string;
} }
export async function loadBindingFile( export function parseBindingFile(
text: string,
filePath: string, filePath: string,
): Promise<PlaybillBinding> { ): PlaybillBinding {
const file = Bun.file(filePath);
const text = await file.text();
const parsed = YAML.parse(text); const parsed = YAML.parse(text);
const binding = BindingSchema.parse(parsed); const binding = BindingFileSchema.parse(parsed);
const fileGlobs = binding.files; const fileGlobs = binding.files;
const bindingFileDirname = filePath.split("/").slice(0, -1).join("/"); const bindingFileDirname = filePath.split("/").slice(0, -1).join("/");
@ -52,8 +57,14 @@ export async function loadBindingFile(
}); });
return { return {
info: binding, _raw: binding,
bindingPath: filePath, bindingFilePath: filePath,
files: filteredFilePaths, includedFiles: filteredFilePaths,
id: binding.id,
name: binding.name,
author: binding.author ?? "Anonymous",
description: binding.description ?? "",
version: binding.version,
}; };
} }

View file

@ -1,20 +1,34 @@
import { parseArgs } from "node:util"; import { parseArgs } from "node:util";
import { import {
type AnyResource, type AnyResource,
DirectiveError,
ValidatedPlaybillSchema, ValidatedPlaybillSchema,
compileScript,
getEmptyPlaybill, getEmptyPlaybill,
} from "@proscenium/playbill"; } from "@proscenium/playbill";
import chalk from "chalk"; import chalk from "chalk";
import { loadBindingFile } from "./binding"; import { parseBindingFile } from "./binding";
import { loadResourceFile } from "./resource"; import { loadResourceFile } from "./resource";
import { usage } from "./usage"; import { usage } from "./usage";
class CLIError extends Error {} enum ExitCode {
Success = 0,
Error = 1,
}
async function main(): Promise<boolean> { class CLIError extends Error { }
// Parse command line arguments
async function loadFileOrFail(filePath: string): Promise<string> {
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<number> {
// -- ARGUMENT PARSING -- //
const { values: options, positionals: args } = parseArgs({ const { values: options, positionals: args } = parseArgs({
args: Bun.argv.slice(2), args: Bun.argv.slice(2),
options: { options: {
@ -53,11 +67,13 @@ async function main(): Promise<boolean> {
allowPositionals: true, allowPositionals: true,
}); });
// -- HELP TEXT -- //
if (options.help) { if (options.help) {
console.log(usage); console.log(usage);
return true; return ExitCode.Success;
} }
// -- ARG VALIDATION -- //
if (options.check && options.write) { if (options.check && options.write) {
throw new CLIError("Cannot use --check and --write together"); throw new CLIError("Cannot use --check and --write together");
} }
@ -74,30 +90,22 @@ async function main(): Promise<boolean> {
} }
} }
// -- BINDING FILE -- //
const bindingPath = args[0] ?? "./binding.yaml"; const bindingPath = args[0] ?? "./binding.yaml";
verboseLog(`Building Playbill with binding: ${bindingPath}`); verboseLog(`Building Playbill with binding: ${bindingPath}`);
// Check if the binding file exists const bindingFileContent = await loadFileOrFail(bindingPath);
const bindingFileCheck = Bun.file(bindingPath); const binding = parseBindingFile(bindingFileContent, bindingPath);
const bindingFileExists = await bindingFileCheck.exists();
if (!bindingFileExists) {
throw new Error(`Binding file not found: ${bindingPath}`);
}
const binding = await loadBindingFile(bindingPath);
verboseLog( verboseLog(
`↳ Binding loaded with ${binding.files.length} associated resources`, `↳ Binding loaded with ${binding.includedFiles.length} associated resources`,
); );
const loadedResources: AnyResource[] = []; // -- RESOURCE FILES -- //
verboseLog("Loading resources"); verboseLog("Loading resources");
const loadedResources: AnyResource[] = [];
// Load resources listed in the binding file for (const filepath of binding.includedFiles) {
for (const filepath of binding.files) {
verboseLog(`↳ Loading ${filepath}`); verboseLog(`↳ Loading ${filepath}`);
const loaded = await loadResourceFile(filepath); const loaded = await loadResourceFile(filepath);
verboseLog(` ↳ Loaded ${loaded.length} resources`); verboseLog(` ↳ Loaded ${loaded.length} resources`);
@ -106,13 +114,14 @@ async function main(): Promise<boolean> {
verboseLog("Constructing playbill"); verboseLog("Constructing playbill");
// Consjtruct the playbill object // -- COMPILE RESOURCES --//
const playbill = getEmptyPlaybill(); const playbill = getEmptyPlaybill();
playbill.id = binding.info.id; playbill.id = binding.id;
playbill.name = binding.info.name; playbill.name = binding.name;
playbill.author = binding.info.author ?? "Anonymous"; playbill.author = binding.author;
playbill.version = binding.info.version; playbill.version = binding.version;
playbill.description = binding.description;
for (const resource of loadedResources) { for (const resource of loadedResources) {
switch (resource.$define) { switch (resource.$define) {
@ -145,66 +154,49 @@ async function main(): Promise<boolean> {
} }
} }
// Evaluate directives in descriptions for all resources // -- VALDATE PLAYBILL -- //
verboseLog("Processing descriptions");
for (const resource of loadedResources) {
try {
resource.description = compileScript(resource.description, playbill);
} catch (error) {
if (error instanceof DirectiveError) {
console.error(
`Error processing directives in ${resource.$define}/${resource.id}`,
);
console.error(error.message);
} else {
console.error(error);
}
return false;
}
}
// Validate the playbill object
verboseLog("Validating playbill"); verboseLog("Validating playbill");
const validatedPlaybill = ValidatedPlaybillSchema.safeParse(playbill); const validatedPlaybill = ValidatedPlaybillSchema.safeParse(playbill);
if (!validatedPlaybill.success) { if (!validatedPlaybill.success) {
console.error("Error validating playbill"); console.error("Error validating playbill");
console.error(validatedPlaybill.error.errors); console.error(validatedPlaybill.error.errors);
return false; return ExitCode.Error;
} }
verboseLog("Playbill validated"); verboseLog("Playbill validated");
// If --check is set, exit here // -- EXIT EARLY IF JUST CHECKING VALIDATION --//
if (options.check) { if (options.check) {
console.log(chalk.green("Playbill validated successfully")); console.log(chalk.green("Playbill validated successfully"));
return true; return ExitCode.Error;
} }
// -- SERIALIZE TO JSON --//
const finalizedPlaybill = JSON.stringify( const finalizedPlaybill = JSON.stringify(
validatedPlaybill.data, validatedPlaybill.data,
null, null,
options.minify ? undefined : 2, options.minify ? undefined : 2,
); );
// -- WRITE TO DISK OR STDOUT --//
if (options.write) { if (options.write) {
await Bun.write(options.outfile, finalizedPlaybill); await Bun.write(options.outfile, finalizedPlaybill);
return true; return ExitCode.Success;
} }
console.log(finalizedPlaybill); console.log(finalizedPlaybill);
return true; return ExitCode.Success;
} }
try { try {
const success = await main(); const exitCode = await main();
if (success) { process.exit(exitCode);
process.exit(0);
} else {
process.exit(1);
}
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
console.error("Uncaught error:");
console.error(chalk.red(error.message)); console.error(chalk.red(error.message));
console.error(error.stack);
} else { } else {
console.error("An unknown error occurred"); console.error("An unknown error occurred");
console.error(error); console.error(error);

View file

@ -3,12 +3,71 @@ import {
AnyResourceSchema, AnyResourceSchema,
ResourceMap, ResourceMap,
} from "@proscenium/playbill"; } from "@proscenium/playbill";
import YAML from "yaml"; import YAML, { YAMLParseError } from "yaml";
import { extractFrontmatter, extractMarkdown } from "./markdown"; import { extractFrontmatter, extractMarkdown } from "./markdown";
type FileFormat = "yaml" | "markdown"; type FileFormat = "yaml" | "markdown";
export class ResourceFileError extends Error {} export class ResourceFileError extends Error { }
export class NullResourceError extends Error {} export class NullResourceError extends Error { }
function determineFileFormat(filePath: string): FileFormat {
return filePath.split(".").pop() === "md" ? "markdown" : "yaml";
}
function loadYamlResourceFile(filePath: string, text: string): AnyResource[] {
const parsedDocs = YAML.parseAllDocuments(text);
if (parsedDocs.some((doc) => doc.toJS() === null)) {
throw new NullResourceError(`Null resource defined in ${filePath}`);
}
const errors = parsedDocs.flatMap((doc) => doc.errors);
if (errors.length > 0) {
throw new ResourceFileError(`Error parsing ${filePath}: ${errors}`);
}
const collection: AnyResource[] = [];
for (const doc of parsedDocs) {
if (doc.errors.length > 0) {
throw new ResourceFileError(`Error parsing ${filePath}: ${doc.errors}`);
}
const raw = doc.toJS();
const parsed = AnyResourceSchema.parse(raw);
const type = parsed.$define;
const schemaToUse = ResourceMap[type];
const validated = schemaToUse.parse(parsed);
collection.push(validated);
}
return collection;
}
function loadMarkdownResourceFile(
filePath: string,
text: string,
): AnyResource[] {
try {
const frontmatter = extractFrontmatter(text);
const markdown = extractMarkdown(text);
const together = {
...frontmatter,
description: markdown,
};
const parsed = AnyResourceSchema.parse(together);
return [parsed];
} catch (e) {
if (e instanceof YAMLParseError) {
throw new ResourceFileError(`Error parsing frontmatter in ${filePath}`);
}
throw e;
}
}
/** /**
* Load and process all documents in a yaml file at the given path * Load and process all documents in a yaml file at the given path
@ -22,49 +81,13 @@ export async function loadResourceFile(
filePath: string, filePath: string,
): Promise<AnyResource[]> { ): Promise<AnyResource[]> {
const file = Bun.file(filePath); const file = Bun.file(filePath);
const format: FileFormat =
filePath.split(".").pop() === "md" ? "markdown" : "yaml";
const text = await file.text(); const text = await file.text();
const format = determineFileFormat(filePath);
if (format === "yaml") { switch (format) {
const parsedDocs = YAML.parseAllDocuments(text); case "yaml":
return loadYamlResourceFile(filePath, text);
if (parsedDocs.some((doc) => doc.toJS() === null)) { case "markdown":
throw new NullResourceError(`Null resource defined in ${filePath}`); return loadMarkdownResourceFile(filePath, text);
}
const errors = parsedDocs.flatMap((doc) => doc.errors);
if (errors.length > 0) {
throw new ResourceFileError(`Error parsing ${filePath}: ${errors}`);
}
const collection: AnyResource[] = [];
for (const doc of parsedDocs) {
if (doc.errors.length > 0) {
throw new ResourceFileError(`Error parsing ${filePath}: ${doc.errors}`);
}
const raw = doc.toJS();
const parsed = AnyResourceSchema.parse(raw);
const type = parsed.$define;
const schemaToUse = ResourceMap[type];
const validated = schemaToUse.parse(parsed);
collection.push(validated);
}
return collection;
} }
const frontmatter = extractFrontmatter(text);
const markdown = extractMarkdown(text);
const together = {
...frontmatter,
description: markdown,
};
const parsed = AnyResourceSchema.parse(together);
return [parsed];
} }