diff --git a/biome.json b/biome.json index cc381ce..b77b73c 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,8 @@ }, "files": { "ignoreUnknown": false, - "ignore": ["*.d.ts", "*.json"] + "ignore": ["*.d.ts", "*.json"], + "include": ["./src/**/*.ts"] }, "formatter": { "enabled": true, diff --git a/src/index.ts b/src/index.ts index c308b2d..0caf1bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,32 @@ +import { watch } from "node:fs/promises"; import { - serializePlaybill, ValidatedPlaybillSchema, + serializePlaybill, } from "@proscenium/playbill"; import chalk from "chalk"; -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"; +import { loadFromBinding, resolveBindingPath } from "#filetypes/binding"; +import { CLIError, MuseError } from "#lib/errors"; +import { renderAsHTML } from "#renderers/html"; +import { type CLIArguments, parseCLIArguments } from "#util/args"; +import { getAbsoluteDirname } from "#util/files"; +import { version } from "../package.json" with { type: "json" }; + +const usage = ` +${chalk.bold("muse")} - Compile and validate Playbills for Proscenium +${chalk.dim(`v${version}`)} + +Usage: + muse [/path/to/binding.yaml] + +Options: + --check Only load and check the current binding and resources, but do not compile + --write Write the output to a file. If not specified, outputs to stdout + --outfile Specify the output file path [default: playbill.json] + --verbose, -v Verbose output + --minify, -m Minify the output JSON + --watch Watch the directory for changes and recompile + --help, -h Show this help message +`.trim(); enum ExitCode { Success = 0, @@ -71,17 +88,14 @@ async function main(): Promise { 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); + const watchDir = getAbsoluteDirname(cliArguments.inputFilePath); + console.log(`Watching ${watchDir} for changes`); + const watcher = watch(watchDir, { recursive: true, }); diff --git a/src/filetypes/binding.ts b/src/lib/binding.ts similarity index 97% rename from src/filetypes/binding.ts rename to src/lib/binding.ts index 03fdbff..d445cdf 100644 --- a/src/filetypes/binding.ts +++ b/src/lib/binding.ts @@ -1,14 +1,14 @@ import path from "node:path"; +import { + type Playbill, + type UnknownResource, + getEmptyPlaybill, +} from "@proscenium/playbill"; import { Glob } from "bun"; import z from "zod"; -import { - getEmptyPlaybill, - type UnknownResource, - type Playbill, -} from "@proscenium/playbill"; -import { loadYAMLFileOrFail } from "../util/files"; +import { FileNotFoundError } from "#lib/errors"; +import { loadYAMLFileOrFail } from "#util/files"; import { loadResourceFile } from "./resource"; -import { FileNotFoundError } from "../errors"; const HTMLRenderOptionsSchema = z.object({ styles: z.array(z.string()).optional(), diff --git a/src/errors.ts b/src/lib/errors.ts similarity index 85% rename from src/errors.ts rename to src/lib/errors.ts index e688ebf..b92c44c 100644 --- a/src/errors.ts +++ b/src/lib/errors.ts @@ -19,7 +19,7 @@ export class FileError extends MuseError { } } -export class MalformedResourceFileError extends FileError { } +export class MalformedResourceFileError extends FileError {} export class FileNotFoundError extends FileError { constructor(filePath: string) { @@ -29,4 +29,4 @@ export class FileNotFoundError extends FileError { } } -export class CLIError extends MuseError { } +export class CLIError extends MuseError {} diff --git a/src/filetypes/markdown.ts b/src/lib/markdown.ts similarity index 83% rename from src/filetypes/markdown.ts rename to src/lib/markdown.ts index ae9d408..ae3bd79 100644 --- a/src/filetypes/markdown.ts +++ b/src/lib/markdown.ts @@ -27,6 +27,11 @@ export function extractFrontmatter(content: string) { return {}; } +/** + * Given a string of a markdown document, extract the markdown content + * @param content The raw markdown content + * @returns The markdown content without frontmatter + */ export function extractMarkdown(content: string): string { if (content.trim().indexOf("---") !== 0) { return content; @@ -54,6 +59,12 @@ ${markdown.trim()} `; } +/** + * Given a string of markdown, convert it to HTML + * + * @param markdown The markdown to convert + * @returns A promise resolving to the HTML representation of the markdown + */ export async function markdownToHtml(markdown: string): Promise { const rendered = await unified() .use(remarkParse) diff --git a/src/filetypes/resource.ts b/src/lib/resource.ts similarity index 96% rename from src/filetypes/resource.ts rename to src/lib/resource.ts index d990d92..e71ac8b 100644 --- a/src/filetypes/resource.ts +++ b/src/lib/resource.ts @@ -1,13 +1,13 @@ import { + ResourceMap, type UnknownResource, UnknownResourceSchema, - ResourceMap, } from "@proscenium/playbill"; import YAML, { YAMLParseError } from "yaml"; import { ZodError } from "zod"; -import { MalformedResourceFileError } from "../errors"; +import { MalformedResourceFileError } from "#lib/errors"; +import { loadFileOrFail } from "#util/files"; import { extractFrontmatter, extractMarkdown } from "./markdown"; -import { loadFileOrFail } from "../util/files"; type FileFormat = "yaml" | "markdown"; diff --git a/src/renderers/html.ts b/src/renderers/html.ts index 6784a39..8f05dc9 100644 --- a/src/renderers/html.ts +++ b/src/renderers/html.ts @@ -6,14 +6,14 @@ import type { Rule, ValidatedPlaybill, } from "@proscenium/playbill"; -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"; -import rehypeStringify from "rehype-stringify"; +import type { PlaybillBinding } from "lib/binding"; +import { markdownToHtml } from "lib/markdown"; import capitalize from "lodash-es/capitalize"; -import { FileNotFoundError } from "../errors"; +import rehypeFormat from "rehype-format"; +import rehypeParse from "rehype-parse"; +import rehypeStringify from "rehype-stringify"; +import { unified } from "unified"; +import { FileNotFoundError } from "#lib/errors"; async function renderGuide(guide: Guide): Promise { const html = await markdownToHtml(guide.description); diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 0e9ca14..0000000 --- a/src/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -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/usage.ts b/src/usage.ts deleted file mode 100644 index 05ba37d..0000000 --- a/src/usage.ts +++ /dev/null @@ -1,18 +0,0 @@ -import chalk from "chalk"; -import { version } from "../package.json" with { type: "json" }; - -export const usage = ` -${chalk.bold("muse")} - Compile and validate Playbills for Proscenium -${chalk.dim(`v${version}`)} - -Usage: - muse [/path/to/binding.yaml] - -Options: - --check Only load and check the current binding and resources, but do not compile - --write Write the output to a file. If not specified, outputs to stdout - --outfile Specify the output file path [default: playbill.json] - --verbose, -v Verbose output - --minify, -m Minify the output JSON - --help, -h Show this help message -`.trim(); diff --git a/src/args.ts b/src/util/args.ts similarity index 71% rename from src/args.ts rename to src/util/args.ts index 4e10de6..1ea64c5 100644 --- a/src/args.ts +++ b/src/util/args.ts @@ -1,7 +1,14 @@ import { parseArgs } from "node:util"; -import { RendererSchema, type Renderer } from "./types"; -import { MuseError } from "./errors"; +import { z } from "zod"; +import { CLIError, MuseError } from "#lib/errors"; +const Renderers = ["json", "html"] as const; +const RendererSchema = z.enum(Renderers); +type Renderer = z.infer; + +/** + * A shape representing the arguments passed to the CLI + */ export interface CLIArguments { inputFilePath: string; @@ -16,6 +23,14 @@ export interface CLIArguments { }; } +/** + * Given an array of CLI arguments, parse them into a structured object + * + * @param argv The arguments to parse + * @returns The parsed CLI arguments + * + * @throws {CLIError} if the arguments are invalid + */ export function parseCLIArguments(argv: string[]): CLIArguments { const { values: options, positionals: args } = parseArgs({ args: argv, @@ -59,6 +74,11 @@ export function parseCLIArguments(argv: string[]): CLIArguments { allowPositionals: true, }); + // -- ARG VALIDATION -- // + if (options.check && options.write) { + throw new CLIError("Cannot use --check and --write together"); + } + const parsedRenderer = RendererSchema.safeParse(options.renderer); if (!parsedRenderer.success) { diff --git a/src/util/files.ts b/src/util/files.ts index eac5957..952b8cc 100644 --- a/src/util/files.ts +++ b/src/util/files.ts @@ -1,7 +1,14 @@ -import YAML from "yaml"; -import { FileError, FileNotFoundError } from "../errors"; import path from "node:path"; +import YAML from "yaml"; +import { FileError, FileNotFoundError } from "../lib/errors"; +/** + * Load a file from the filesystem or throw an error if it does not exist. + * @param filePath The path to the file to load + * @returns The contents of the file as a string + * + * @throws {FileNotFoundError} if the file does not exist + */ export async function loadFileOrFail(filePath: string): Promise { const fileToLoad = Bun.file(filePath); const fileExists = await fileToLoad.exists(); @@ -13,6 +20,15 @@ export async function loadFileOrFail(filePath: string): Promise { return await fileToLoad.text(); } +/** + * Load and parse a YAML file or throw an error if the file doesnt exist + * or the yaml fails to parse + * @param filePath The path to the file to load + * @returns The parsed contents of the file + * + * @throws {FileNotFoundError} if the file does not exist + * @throws {FileError} if the file is not valid YAML + */ export async function loadYAMLFileOrFail(filePath: string): Promise { try { return YAML.parse(await loadFileOrFail(filePath)); @@ -21,6 +37,12 @@ export async function loadYAMLFileOrFail(filePath: string): Promise { } } -export function fullDirname(inputPath: string): string { - return path.resolve(path.dirname(inputPath)); +/** + * Returns the absolute path of the directory containing the given file + * + * @param filePath The path to the file + * @returns The absolute path of the directory containing the file + */ +export function getAbsoluteDirname(filePath: string): string { + return path.resolve(path.dirname(filePath)); } diff --git a/tsconfig.json b/tsconfig.json index 8b73555..e2db0a1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,18 @@ // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false + "noPropertyAccessFromIndexSignature": false, + "baseUrl": "./src", + "paths": { + "#lib/*": [ + "./lib/*" + ], + "#renderers/*": [ + "./renderers/*" + ], + "#util/*": [ + "./util/*" + ], + } } }