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 UnknownResource,
type Playbill, type Playbill,
} from "@proscenium/playbill"; } from "@proscenium/playbill";
import { loadYAMLFileOrFail } from "./files"; import { loadYAMLFileOrFail } from "../util/files";
import { loadResourceFile } from "./resource"; import { loadResourceFile } from "./resource";
import { FileNotFoundError } from "./errors"; import { FileNotFoundError } from "../errors";
const HTMLRenderOptionsSchema = z.object({ const HTMLRenderOptionsSchema = z.object({
stylesheet: z.string().optional(), styles: z.array(z.string()).optional(),
}); });
const BindingFileSchema = z.object({ const BindingFileSchema = z.object({

View file

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

View file

@ -1,101 +1,27 @@
import { parseArgs } from "node:util";
import { import {
serializePlaybill, serializePlaybill,
ValidatedPlaybillSchema, ValidatedPlaybillSchema,
} from "@proscenium/playbill"; } from "@proscenium/playbill";
import chalk from "chalk"; 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 { usage } from "./usage";
import { CLIError, MuseError } from "./errors"; import { CLIError, MuseError } from "./errors";
import { renderAsHTML } from "./renderers/html"; import { renderAsHTML } from "./renderers/html";
import { parseCLIArguments, type CLIArguments } from "./args";
import { fullDirname } from "./util/files";
enum ExitCode { enum ExitCode {
Success = 0, Success = 0,
Error = 1, Error = 1,
} }
async function main(): Promise<number> { async function processBinding({ inputFilePath, options }: CLIArguments) {
// -- 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));
}
}
// -- BINDING FILE -- // // -- BINDING FILE -- //
const bindingPathInput = args[0] ?? "./binding.yaml"; const bindingPath = await resolveBindingPath(inputFilePath);
const bindingPath = await resolveBindingPath(bindingPathInput);
verboseLog(`Building Playbill with binding: ${bindingPath}`);
const binding = await loadFromBinding(bindingPath); const binding = await loadFromBinding(bindingPath);
verboseLog(
`↳ Binding loaded with ${binding.includedFiles.length} associated resources`,
);
// -- VALDATE PLAYBILL -- // // -- VALDATE PLAYBILL -- //
verboseLog("Validating playbill");
const validatedPlaybill = ValidatedPlaybillSchema.safeParse(binding.playbill); const validatedPlaybill = ValidatedPlaybillSchema.safeParse(binding.playbill);
if (!validatedPlaybill.success) { if (!validatedPlaybill.success) {
@ -104,8 +30,6 @@ async function main(): Promise<number> {
return ExitCode.Error; return ExitCode.Error;
} }
verboseLog("Playbill validated");
// -- EXIT EARLY IF JUST CHECKING VALIDATION --// // -- 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"));
@ -129,6 +53,7 @@ async function main(): Promise<number> {
// -- WRITE TO DISK OR STDOUT --// // -- WRITE TO DISK OR STDOUT --//
if (options.write) { if (options.write) {
await Bun.write(options.outfile, serializedPlaybill); await Bun.write(options.outfile, serializedPlaybill);
return ExitCode.Success; return ExitCode.Success;
} }
@ -136,6 +61,47 @@ async function main(): Promise<number> {
return ExitCode.Success; 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 { try {
const exitCode = await main(); const exitCode = await main();
process.exit(exitCode); process.exit(exitCode);

View file

@ -6,8 +6,8 @@ import type {
Rule, Rule,
ValidatedPlaybill, ValidatedPlaybill,
} from "@proscenium/playbill"; } from "@proscenium/playbill";
import { markdownToHtml } from "../markdown"; import { markdownToHtml } from "../filetypes/markdown";
import type { PlaybillBinding } from "../binding"; import type { PlaybillBinding } from "../filetypes/binding";
import rehypeParse from "rehype-parse"; import rehypeParse from "rehype-parse";
import { unified } from "unified"; import { unified } from "unified";
import rehypeFormat from "rehype-format"; import rehypeFormat from "rehype-format";
@ -132,20 +132,19 @@ async function renderDocument(
let css = ""; let css = "";
if (binding.html?.stylesheet) { if (binding.html?.styles) {
const cssFilePath = path.resolve( for (const style of binding.html.styles) {
binding.bindingFileDirname, const cssFilePath = path.resolve(binding.bindingFileDirname, style);
binding.html.stylesheet, const cssFile = Bun.file(cssFilePath);
); const cssFileExists = await cssFile.exists();
const cssFile = Bun.file(cssFilePath);
const cssFileExists = await cssFile.exists();
if (cssFileExists) { if (cssFileExists) {
css = `<style> css += `<style>
${await cssFile.text()} ${await cssFile.text()}
</style>`; </style>`;
} else { } else {
throw new FileNotFoundError(cssFilePath); 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 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> { export async function loadFileOrFail(filePath: string): Promise<string> {
const fileToLoad = Bun.file(filePath); 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}`); throw new FileError("Failed to parse YAML file", filePath, `${error}`);
} }
} }
export function fullDirname(inputPath: string): string {
return path.resolve(path.dirname(inputPath));
}