Clean up
This commit is contained in:
parent
45580288e1
commit
8b276cc226
5 changed files with 145 additions and 119 deletions
4
bun.lock
4
bun.lock
|
@ -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=="],
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
106
src/index.ts
106
src/index.ts
|
@ -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);
|
||||||
|
|
113
src/resource.ts
113
src/resource.ts
|
@ -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];
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue