Watch mode support
This commit is contained in:
parent
2032103412
commit
84b8f1c9d6
8 changed files with 157 additions and 101 deletions
81
src/args.ts
Normal file
81
src/args.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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({
|
|
@ -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";
|
||||
|
130
src/index.ts
130
src/index.ts
|
@ -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);
|
||||
|
|
|
@ -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,22 +132,21 @@ async function renderDocument(
|
|||
|
||||
let css = "";
|
||||
|
||||
if (binding.html?.stylesheet) {
|
||||
const cssFilePath = path.resolve(
|
||||
binding.bindingFileDirname,
|
||||
binding.html.stylesheet,
|
||||
);
|
||||
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>
|
||||
css += `<style>
|
||||
${await cssFile.text()}
|
||||
</style>`;
|
||||
} else {
|
||||
throw new FileNotFoundError(cssFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
|
|
5
src/types.ts
Normal file
5
src/types.ts
Normal 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";
|
|
@ -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));
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue