From 16660823ea040bf9f419b73f96614ab209eda9fa Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Thu, 3 Apr 2025 10:16:08 -0400 Subject: [PATCH] Organize new setup --- demo/main/processors/announce-slam-dunks.js | 1 + demo/main/processors/scream.js | 1 + package.json | 4 +- src/{cli.ts => cli/index.ts} | 45 ++++---- src/{ => core}/binding.ts | 118 ++++++++++---------- src/{ => core}/parse.ts | 16 ++- src/core/processor.ts | 42 +++++++ src/errors.ts | 3 +- src/exports.ts | 4 +- src/types.ts | 57 ---------- tsconfig.json | 16 ++- 11 files changed, 165 insertions(+), 142 deletions(-) rename src/{cli.ts => cli/index.ts} (77%) rename src/{ => core}/binding.ts (51%) rename src/{ => core}/parse.ts (85%) create mode 100644 src/core/processor.ts diff --git a/demo/main/processors/announce-slam-dunks.js b/demo/main/processors/announce-slam-dunks.js index 24cdb96..56824e5 100644 --- a/demo/main/processors/announce-slam-dunks.js +++ b/demo/main/processors/announce-slam-dunks.js @@ -1,4 +1,5 @@ export const name = "Announce Slam Dunks"; +export const description = "Get hype when a file has slam dunks"; export const process = (binding, { dunks }) => { const slamDunkKey = dunks?.key ?? "slamDunks"; diff --git a/demo/main/processors/scream.js b/demo/main/processors/scream.js index 11f177d..17e0bac 100644 --- a/demo/main/processors/scream.js +++ b/demo/main/processors/scream.js @@ -1,4 +1,5 @@ export const name = "Scream"; + export function process(binding) { const modifiedEntries = binding.entries.map(entry => { if (binding.markdownKey in entry.data) { diff --git a/package.json b/package.json index 13a34be..ba1d46c 100644 --- a/package.json +++ b/package.json @@ -32,9 +32,9 @@ "clean": "rm -rf dist", "types": "dts-bundle-generator -o dist/muse.d.ts --project ./tsconfig.json ./src/exports.ts", "transpile": "bun build ./src/exports.ts --outfile ./dist/muse.js", - "compile": "bun build ./src/cli.ts --outfile ./dist/muse --compile", + "compile": "bun run clean && bun build ./src/cli/index.ts --outfile ./dist/muse --compile", "compile:install": "bun run compile && mv ./dist/muse ~/.local/bin/muse", "build": "bun run clean && bun run transpile && bun run types", - "demo": "bun run ./src/cli.ts -- ./demo/main" + "demo": "bun run ./src/cli/index.ts -- ./demo/main" } } diff --git a/src/cli.ts b/src/cli/index.ts similarity index 77% rename from src/cli.ts rename to src/cli/index.ts index c8380ab..6e631a0 100644 --- a/src/cli.ts +++ b/src/cli/index.ts @@ -1,9 +1,14 @@ import { parseArgs } from "node:util"; import chalk from "chalk"; -import { version } from "../package.json"; -import { loadBinding } from "./binding"; -import { MuseError } from "./errors"; -import type { Binding, CLIArguments } from "./types"; +import { type Binding, loadBinding } from "#core/binding"; +import { MuseError } from "#errors"; +import { version } from "../../package.json"; + +interface CLIFlags { + help: boolean; + verbose: boolean; + stdout: boolean; +} interface Loggers { log: (msg: string) => void; @@ -36,7 +41,7 @@ Options: * * @throws {CLIError} if the arguments are invalid */ -export function parseCLIArguments(argv: string[]): CLIArguments { +function parseCLIArguments(argv: string[]): [string, CLIFlags] { const { values: options, positionals: args } = parseArgs({ args: argv, options: { @@ -60,17 +65,12 @@ export function parseCLIArguments(argv: string[]): CLIArguments { allowPositionals: true, }); - return { - inputFilePath: args[0] ?? "./binding.yaml", - flags: { - help: options.help, - verbose: options.verbose, - stdout: options.stdout, - }, - }; + return [args[0] ?? "./", options]; } + async function processBinding( - { inputFilePath, flags }: CLIArguments, + inputFilePath: string, + flags: CLIFlags, { log, verbose }: Loggers, ) { // Load the binding @@ -90,10 +90,16 @@ async function processBinding( for (const processor of binding.processors) { const lastStep = processedSteps[processedSteps.length - 1]; - const { process, name } = processor; + const { process, name, description } = processor; - log(chalk.bold(`↪ ${name}`) + chalk.dim(` (${processor.description})`)); - const thisStep = await process(lastStep, binding.options, flags); + let processorLogLine = chalk.bold(`↪ ${name}`); + + if (description && description.length > 0) { + processorLogLine += chalk.dim(` (${description})`); + } + + log(processorLogLine); + const thisStep = await process(lastStep, binding.options); processedSteps.push(thisStep); } @@ -111,8 +117,7 @@ async function processBinding( } async function main(): Promise { - const cliArguments = parseCLIArguments(Bun.argv.slice(2)); - const { flags } = cliArguments; + const [inputFilePath, flags] = parseCLIArguments(Bun.argv.slice(2)); // If --help is specified, print usage and exit if (flags.help) { @@ -127,7 +132,7 @@ async function main(): Promise { } : () => {}; - const lastProcessResult = await processBinding(cliArguments, { + const lastProcessResult = await processBinding(inputFilePath, flags, { log: logFn, verbose: verboseFn, }); diff --git a/src/binding.ts b/src/core/binding.ts similarity index 51% rename from src/binding.ts rename to src/core/binding.ts index ae2bcc2..eee86b1 100644 --- a/src/binding.ts +++ b/src/core/binding.ts @@ -1,39 +1,49 @@ import path, { dirname } from "node:path"; import { Glob } from "bun"; -import { MuseError, MuseFileNotFoundError } from "./errors"; -import { parseMuseFile } from "./parse"; -import { - type Binding, - BindingSchema, - type MuseEntry, - type MuseProcessor, -} from "./types"; -import { resolveFilePath } from "./util"; +import { z } from "zod"; +import { MuseError, MuseFileNotFoundError } from "#errors"; +import type { UnknownRecord } from "#types"; +import { resolveFilePath } from "#util"; +import { type MuseEntry, parseMuseFile } from "./parse"; +import { type MuseProcessor, loadProcessor } from "./processor"; -async function loadProcessor( - bindingDirname: string, - processorPath: string, -): Promise { - const resolvedProcessorPath = processorPath.startsWith("http") - ? processorPath - : path.resolve(bindingDirname, processorPath); - const { process, name, description } = await import(resolvedProcessorPath); +// Function to parse an unknown object into parts of a binding +export const BindingSchema = z.object({ + contentKey: z.string().default("content"), + sources: z + .array(z.string()) + .default(["**/*.yaml", "**/*.md", "**/*.toml", "**/*.json"]), + options: z.record(z.string(), z.any()).default({}), + processors: z.array(z.string()).default([]), + renderer: z.string().optional(), +}); - if (!process || typeof process !== "function") { - throw new MuseError( - `Processor at ${processorPath} does not export a process() function`, - ); - } +/** + * The core object of a Muse binding + * Contains information about the binding file, the configuration of + * the project, and a list of entries to be processed + */ +export interface Binding { + /** The raw data read from the binding file */ + readonly _raw: z.infer; - const processorName = String(name ?? "Unnamed Processor"); - const processorDescription = String(description ?? "No description provided"); + /** The full file path to the binding file */ + readonly bindingPath: string; - return { - filePath: resolvedProcessorPath, - process: process as MuseProcessor["process"], - name: processorName, - description: processorDescription, - }; + /** The key used to for default content in unstructured files */ + readonly contentKey: string; + + /** All entries loaded from the binding file */ + readonly entries: MuseEntry[]; + + /** A list of processors to be applied to the entries */ + readonly processors: MuseProcessor[]; + + /** Arbitrary metadata for processors to use to cache project data */ + readonly meta: MetaShape; + + /** Configuration options for Muse and all processors */ + readonly options: UnknownRecord; } /** @@ -46,47 +56,39 @@ async function loadProcessor( * @returns The absolute resolved path to the binding file */ async function resolveBindingPath(bindingPath: string): Promise { - // If the path does not specify a filename, use binding.yaml + const DEFAULT_BINDING_FILES = [ + "binding.json", + "binding.yaml", + "binding.toml", + "binding.md", + ] as const; const inputLocation = Bun.file(bindingPath); // If it is a directory, try for the four main supported types const stat = await inputLocation.stat(); if (stat.isDirectory()) { - const jsonPath = await resolveFilePath( - path.resolve(bindingPath, "binding.json"), - ); - if (jsonPath) { - return jsonPath; + for (const bindingFile of DEFAULT_BINDING_FILES) { + const filePath = path.resolve(bindingPath, bindingFile); + const resolvedPath = await resolveFilePath(filePath); + if (resolvedPath) { + return resolvedPath; + } } - const yamlPath = await resolveFilePath( - path.resolve(bindingPath, "binding.yaml"), + throw new MuseFileNotFoundError( + bindingPath, + "Failed to find a binding file at the provided directory.", ); - if (yamlPath) { - return yamlPath; - } - - const tomlPath = await resolveFilePath( - path.resolve(bindingPath, "binding.toml"), - ); - if (tomlPath) { - return tomlPath; - } - - const mdPath = await resolveFilePath( - path.resolve(bindingPath, "binding.md"), - ); - if (mdPath) { - return mdPath; - } - - throw new MuseFileNotFoundError(bindingPath); } + // If not a directory, check if its a file that exists const exists = await inputLocation.exists(); if (!exists) { - throw new MuseFileNotFoundError(bindingPath); + throw new MuseFileNotFoundError( + bindingPath, + "Provided binding path does not exist.", + ); } // If it is a file, return the path diff --git a/src/parse.ts b/src/core/parse.ts similarity index 85% rename from src/parse.ts rename to src/core/parse.ts index 509bf63..128de7f 100644 --- a/src/parse.ts +++ b/src/core/parse.ts @@ -2,7 +2,15 @@ import { extname, normalize } from "node:path"; import EMDY from "@endeavorance/emdy"; import TOML from "smol-toml"; import YAML from "yaml"; -import type { MuseEntry, UnknownRecord } from "./types"; +import type { UnknownRecord } from "#types"; +import { MuseFileNotFoundError } from "#errors"; + +export interface MuseEntry { + _raw: string; + filePath: string; + data: Record; + meta: MetaShape; +} function parseYAMLEntries(text: string): UnknownRecord[] { const parsedDocs = YAML.parseAllDocuments(text); @@ -62,6 +70,12 @@ export async function parseMuseFile( const filePath = normalize(rawFilePath); const file = Bun.file(filePath); const fileType = extname(filePath).slice(1); + + const fileExists = await file.exists(); + if (!fileExists) { + throw new MuseFileNotFoundError(filePath, "Failed to load source file."); + } + const rawFileContent = await file.text(); const partial = { diff --git a/src/core/processor.ts b/src/core/processor.ts new file mode 100644 index 0000000..1df9753 --- /dev/null +++ b/src/core/processor.ts @@ -0,0 +1,42 @@ +import path from "node:path"; +import { MuseError } from "#errors"; +import type { Binding } from "./binding"; + +export type MuseProcessorFn = ( + binding: Binding, + opts: Record, +) => Promise; + +export interface MuseProcessor { + readonly filePath: string; + readonly name: string; + readonly description: string; + readonly process: MuseProcessorFn; +} + +export async function loadProcessor( + bindingDirname: string, + processorPath: string, +): Promise { + // Resolve local paths, use URLs as-is + const resolvedProcessorPath = processorPath.startsWith("http") + ? processorPath + : path.resolve(bindingDirname, processorPath); + const { process, name, description } = await import(resolvedProcessorPath); + + if (!process || typeof process !== "function") { + throw new MuseError( + `Processor at ${processorPath} does not export a process() function`, + ); + } + + const processorName = String(name ?? "Unnamed Processor"); + const processorDescription = String(description ?? ""); + + return { + filePath: resolvedProcessorPath, + process: process as MuseProcessor["process"], + name: processorName, + description: processorDescription, + }; +} diff --git a/src/errors.ts b/src/errors.ts index 47d98d5..9ad54ac 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -20,10 +20,11 @@ export class MuseFileError extends MuseError { } export class MuseFileNotFoundError extends MuseFileError { - constructor(filePath: string) { + constructor(filePath: string, details?: string) { super("File not found", filePath); this.message = "File not found"; this.filePath = filePath; + this.details = details ?? "No additional information."; } } diff --git a/src/exports.ts b/src/exports.ts index a05b13f..65319af 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -1,3 +1,3 @@ -export type * from "./types"; -export * from "./errors"; +export type * from "#types"; +export * from "#errors"; export const DEFAULT_CONTENT_KEY = "content"; diff --git a/src/types.ts b/src/types.ts index 7aabb16..b57ec2f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,60 +1,3 @@ import { z } from "zod"; export type UnknownRecord = Record; - -export interface CLIFlags { - help: boolean; - verbose: boolean; - stdout: boolean; -} - -export interface CLIArguments { - inputFilePath: string; - flags: CLIFlags; -} - -export const BindingSchema = z.object({ - contentKey: z.string().default("content"), - sources: z - .array(z.string()) - .default(["**/*.yaml", "**/*.md", "**/*.toml", "**/*.json"]), - options: z.record(z.string(), z.any()).default({}), - processors: z.array(z.string()).default([]), - renderer: z.string().optional(), -}); - -export type MuseProcessorFn = ( - binding: Binding, - opts: Record, - flags: CLIFlags, -) => Promise; - -export type MuseRenderFn = ( - binding: Binding, - opts: UnknownRecord, - flags: CLIFlags, -) => Promise; - -export interface MuseProcessor { - readonly filePath: string; - readonly name: string; - readonly description: string; - readonly process: MuseProcessorFn; -} - -export interface MuseEntry { - _raw: string; - filePath: string; - data: Record; - meta: MetaShape; -} - -export interface Binding { - readonly _raw: z.infer; - readonly bindingPath: string; - readonly contentKey: string; - readonly entries: MuseEntry[]; - readonly processors: MuseProcessor[]; - readonly meta: MetaShape; - readonly options: UnknownRecord; -} diff --git a/tsconfig.json b/tsconfig.json index f2b0046..0f25433 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,11 +26,25 @@ "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false, "baseUrl": "./src", - "paths": {}, + "paths": { + "#core/*": [ + "./core/*.ts" + ], + "#util": [ + "./util.ts" + ], + "#types": [ + "./types.ts" + ], + "#errors": [ + "./errors.ts" + ] + }, "outDir": "./dist", }, "include": [ "src/**/*.ts", "src/**/*.tsx", + "src/preload/http-plugin.js", ] }