Watch mode support

This commit is contained in:
Endeavorance 2025-03-10 15:22:47 -04:00
parent 2032103412
commit 84b8f1c9d6
8 changed files with 157 additions and 101 deletions

81
src/args.ts Normal file
View file

@ -0,0 +1,81 @@
import { parseArgs } from "node:util";
import { RendererSchema, type Renderer } from "./types";
import { MuseError } from "./errors";
export interface CLIArguments {
inputFilePath: string;
options: {
write: boolean;
check: boolean;
outfile: string;
help: boolean;
minify: boolean;
renderer: Renderer;
watch: boolean;
};
}
export function parseCLIArguments(argv: string[]): CLIArguments {
const { values: options, positionals: args } = parseArgs({
args: argv,
options: {
write: {
short: "w",
type: "boolean",
default: false,
},
check: {
short: "c",
type: "boolean",
default: false,
},
outfile: {
short: "o",
type: "string",
default: "playbill.json",
},
help: {
short: "h",
default: false,
type: "boolean",
},
minify: {
short: "m",
default: false,
type: "boolean",
},
renderer: {
short: "r",
default: "json",
type: "string",
},
watch: {
default: false,
type: "boolean",
},
},
strict: true,
allowPositionals: true,
});
const parsedRenderer = RendererSchema.safeParse(options.renderer);
if (!parsedRenderer.success) {
throw new MuseError(`Invalid renderer: ${parsedRenderer.data}`);
}
return {
inputFilePath: args[0] ?? "./binding.yaml",
options: {
write: options.write,
check: options.check,
outfile: options.outfile,
help: options.help,
minify: options.minify,
renderer: parsedRenderer.data,
watch: options.watch,
},
};
}

View file

@ -6,12 +6,12 @@ import {
type UnknownResource,
type Playbill,
} from "@proscenium/playbill";
import { loadYAMLFileOrFail } from "./files";
import { loadYAMLFileOrFail } from "../util/files";
import { loadResourceFile } from "./resource";
import { FileNotFoundError } from "./errors";
import { FileNotFoundError } from "../errors";
const HTMLRenderOptionsSchema = z.object({
stylesheet: z.string().optional(),
styles: z.array(z.string()).optional(),
});
const BindingFileSchema = z.object({

View file

@ -5,9 +5,9 @@ import {
} from "@proscenium/playbill";
import YAML, { YAMLParseError } from "yaml";
import { ZodError } from "zod";
import { MalformedResourceFileError } from "./errors";
import { MalformedResourceFileError } from "../errors";
import { extractFrontmatter, extractMarkdown } from "./markdown";
import { loadFileOrFail } from "./files";
import { loadFileOrFail } from "../util/files";
type FileFormat = "yaml" | "markdown";

View file

@ -1,101 +1,27 @@
import { parseArgs } from "node:util";
import {
serializePlaybill,
ValidatedPlaybillSchema,
} from "@proscenium/playbill";
import chalk from "chalk";
import { loadFromBinding, resolveBindingPath } from "./binding";
import { watch } from "node:fs/promises";
import { loadFromBinding, resolveBindingPath } from "./filetypes/binding";
import { usage } from "./usage";
import { CLIError, MuseError } from "./errors";
import { renderAsHTML } from "./renderers/html";
import { parseCLIArguments, type CLIArguments } from "./args";
import { fullDirname } from "./util/files";
enum ExitCode {
Success = 0,
Error = 1,
}
async function main(): Promise<number> {
// -- ARGUMENT PARSING -- //
const { values: options, positionals: args } = parseArgs({
args: Bun.argv.slice(2),
options: {
write: {
short: "w",
type: "boolean",
default: false,
},
check: {
short: "c",
type: "boolean",
default: false,
},
outfile: {
short: "o",
type: "string",
default: "playbill.json",
},
help: {
short: "h",
default: false,
type: "boolean",
},
verbose: {
short: "v",
default: false,
type: "boolean",
},
minify: {
short: "m",
default: false,
type: "boolean",
},
renderer: {
short: "r",
default: "json",
type: "string",
},
},
strict: true,
allowPositionals: true,
});
// -- HELP TEXT -- //
if (options.help) {
console.log(usage);
return ExitCode.Success;
}
// -- ARG VALIDATION -- //
if (options.check && options.write) {
throw new CLIError("Cannot use --check and --write together");
}
const VERBOSE = options.verbose;
/**
* Log a message if the vervose flag has been set
* @param msg - The message to log
*/
function verboseLog(msg: string) {
if (VERBOSE) {
console.log(chalk.dim(msg));
}
}
async function processBinding({ inputFilePath, options }: CLIArguments) {
// -- BINDING FILE -- //
const bindingPathInput = args[0] ?? "./binding.yaml";
const bindingPath = await resolveBindingPath(bindingPathInput);
verboseLog(`Building Playbill with binding: ${bindingPath}`);
const bindingPath = await resolveBindingPath(inputFilePath);
const binding = await loadFromBinding(bindingPath);
verboseLog(
`↳ Binding loaded with ${binding.includedFiles.length} associated resources`,
);
// -- VALDATE PLAYBILL -- //
verboseLog("Validating playbill");
const validatedPlaybill = ValidatedPlaybillSchema.safeParse(binding.playbill);
if (!validatedPlaybill.success) {
@ -104,8 +30,6 @@ async function main(): Promise<number> {
return ExitCode.Error;
}
verboseLog("Playbill validated");
// -- EXIT EARLY IF JUST CHECKING VALIDATION --//
if (options.check) {
console.log(chalk.green("Playbill validated successfully"));
@ -129,6 +53,7 @@ async function main(): Promise<number> {
// -- WRITE TO DISK OR STDOUT --//
if (options.write) {
await Bun.write(options.outfile, serializedPlaybill);
return ExitCode.Success;
}
@ -136,6 +61,47 @@ async function main(): Promise<number> {
return ExitCode.Success;
}
async function main(): Promise<number> {
const cliArguments = parseCLIArguments(Bun.argv.slice(2));
const { options } = cliArguments;
// -- HELP TEXT -- //
if (options.help) {
console.log(usage);
return ExitCode.Success;
}
// -- ARG VALIDATION -- //
if (options.check && options.write) {
throw new CLIError("Cannot use --check and --write together");
}
console.log(`Processing ${cliArguments.inputFilePath}`);
let lastProcessResult = await processBinding(cliArguments);
if (options.watch) {
const watchDir = fullDirname(cliArguments.inputFilePath);
console.log(`Watching ${watchDir} for changes`);
const watcher = watch(watchDir, {
recursive: true,
});
for await (const event of watcher) {
console.log(`Detected ${event.eventType} on ${event.filename}`);
lastProcessResult = await processBinding(cliArguments);
if (lastProcessResult === ExitCode.Error) {
console.error(`Error processing ${event.filename}`);
} else {
console.log("Reprocessed changes");
}
}
return ExitCode.Success;
}
return ExitCode.Success;
}
try {
const exitCode = await main();
process.exit(exitCode);

View file

@ -6,8 +6,8 @@ import type {
Rule,
ValidatedPlaybill,
} from "@proscenium/playbill";
import { markdownToHtml } from "../markdown";
import type { PlaybillBinding } from "../binding";
import { markdownToHtml } from "../filetypes/markdown";
import type { PlaybillBinding } from "../filetypes/binding";
import rehypeParse from "rehype-parse";
import { unified } from "unified";
import rehypeFormat from "rehype-format";
@ -132,20 +132,19 @@ async function renderDocument(
let css = "";
if (binding.html?.stylesheet) {
const cssFilePath = path.resolve(
binding.bindingFileDirname,
binding.html.stylesheet,
);
const cssFile = Bun.file(cssFilePath);
const cssFileExists = await cssFile.exists();
if (binding.html?.styles) {
for (const style of binding.html.styles) {
const cssFilePath = path.resolve(binding.bindingFileDirname, style);
const cssFile = Bun.file(cssFilePath);
const cssFileExists = await cssFile.exists();
if (cssFileExists) {
css = `<style>
if (cssFileExists) {
css += `<style>
${await cssFile.text()}
</style>`;
} else {
throw new FileNotFoundError(cssFilePath);
} else {
throw new FileNotFoundError(cssFilePath);
}
}
}

5
src/types.ts Normal file
View file

@ -0,0 +1,5 @@
import { z } from "zod";
const Renderers = ["json", "html"] as const;
export const RendererSchema = z.enum(Renderers);
export type Renderer = "json" | "html";

View file

@ -1,5 +1,6 @@
import YAML from "yaml";
import { FileError, FileNotFoundError } from "./errors";
import { FileError, FileNotFoundError } from "../errors";
import path from "node:path";
export async function loadFileOrFail(filePath: string): Promise<string> {
const fileToLoad = Bun.file(filePath);
@ -19,3 +20,7 @@ export async function loadYAMLFileOrFail(filePath: string): Promise<unknown> {
throw new FileError("Failed to parse YAML file", filePath, `${error}`);
}
}
export function fullDirname(inputPath: string): string {
return path.resolve(path.dirname(inputPath));
}