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 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({
|
|
@ -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";
|
||||||
|
|
130
src/index.ts
130
src/index.ts
|
@ -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);
|
||||||
|
|
|
@ -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
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 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));
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue