diff --git a/src/args.ts b/src/args.ts new file mode 100644 index 0000000..4e10de6 --- /dev/null +++ b/src/args.ts @@ -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, + }, + }; +} diff --git a/src/binding.ts b/src/filetypes/binding.ts similarity index 96% rename from src/binding.ts rename to src/filetypes/binding.ts index b6da0a6..03fdbff 100644 --- a/src/binding.ts +++ b/src/filetypes/binding.ts @@ -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({ diff --git a/src/markdown.ts b/src/filetypes/markdown.ts similarity index 100% rename from src/markdown.ts rename to src/filetypes/markdown.ts diff --git a/src/resource.ts b/src/filetypes/resource.ts similarity index 96% rename from src/resource.ts rename to src/filetypes/resource.ts index fd921f5..d990d92 100644 --- a/src/resource.ts +++ b/src/filetypes/resource.ts @@ -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"; diff --git a/src/index.ts b/src/index.ts index c7713a5..c308b2d 100644 --- a/src/index.ts +++ b/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 { - // -- 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 { 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 { // -- 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 { return ExitCode.Success; } +async function main(): Promise { + 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); diff --git a/src/renderers/html.ts b/src/renderers/html.ts index f3dc59c..6784a39 100644 --- a/src/renderers/html.ts +++ b/src/renderers/html.ts @@ -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 = ``; - } else { - throw new FileNotFoundError(cssFilePath); + } else { + throw new FileNotFoundError(cssFilePath); + } } } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..0e9ca14 --- /dev/null +++ b/src/types.ts @@ -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"; diff --git a/src/files.ts b/src/util/files.ts similarity index 74% rename from src/files.ts rename to src/util/files.ts index 6f54852..eac5957 100644 --- a/src/files.ts +++ b/src/util/files.ts @@ -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 { const fileToLoad = Bun.file(filePath); @@ -19,3 +20,7 @@ export async function loadYAMLFileOrFail(filePath: string): Promise { throw new FileError("Failed to parse YAML file", filePath, `${error}`); } } + +export function fullDirname(inputPath: string): string { + return path.resolve(path.dirname(inputPath)); +}