From afbb0cbaa587060b6b2fabf009df8712abd678be Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Wed, 2 Apr 2025 20:43:46 -0400 Subject: [PATCH 01/10] Update package.json --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e9fae55..13a34be 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@proscenium/muse", + "name": "@endeavorance/muse", "version": "0.1.0", "module": "dist/index.js", "exports": { @@ -31,9 +31,10 @@ "fmt": "bunx --bun biome check --fix", "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", - "build": "bun run clean && bun run compile && bun run types", - "build:install": "bun run build && mv muse ~/.local/bin/muse", + "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" } } From b5136c5d02bfa4fd5597811589ce77f5c39b34c3 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Thu, 3 Apr 2025 09:36:56 -0400 Subject: [PATCH 02/10] Clean up exports --- src/binding.ts | 44 +++++++++++++++----------------------------- src/errors.ts | 6 +++--- src/exports.ts | 1 + src/types.ts | 37 +++++++++++++++++++++---------------- 4 files changed, 40 insertions(+), 48 deletions(-) diff --git a/src/binding.ts b/src/binding.ts index 9499757..c1f9e96 100644 --- a/src/binding.ts +++ b/src/binding.ts @@ -1,6 +1,6 @@ import path, { dirname } from "node:path"; import { Glob } from "bun"; -import { FileNotFoundError, MuseError } from "./errors"; +import { MuseFileNotFoundError, MuseError } from "./errors"; import { parseMuseFile } from "./parse"; import { type Binding, @@ -33,6 +33,7 @@ async function loadProcessor( description: processorDescription, }; } + /** * Given a path, find the binding file * If the path is to a directory, check for a binding inside @@ -42,7 +43,7 @@ async function loadProcessor( * @param bindingPath - The path to the binding file * @returns The absolute resolved path to the binding file */ -export async function resolveBindingPath(bindingPath: string): Promise { +async function resolveBindingPath(bindingPath: string): Promise { // If the path does not specify a filename, use binding.yaml const inputLocation = Bun.file(bindingPath); @@ -77,13 +78,13 @@ export async function resolveBindingPath(bindingPath: string): Promise { return mdPath; } - throw new FileNotFoundError(bindingPath); + throw new MuseFileNotFoundError(bindingPath); } const exists = await inputLocation.exists(); if (!exists) { - throw new FileNotFoundError(bindingPath); + throw new MuseFileNotFoundError(bindingPath); } // If it is a file, return the path @@ -103,21 +104,8 @@ export async function loadBinding(initialPath: string): Promise { // Load and parse the Binding YAML content const parsedBinding = BindingSchema.parse(loadedBindingData[0].data); - // Prepare collections to load into - const extendsFrom: Binding[] = []; - const entries: MuseEntry[] = []; - - // Process imported bindings and bring in their entries - for (const includePath of parsedBinding.include) { - const pathFromHere = path.resolve(bindingDirname, includePath); - const extendBindingPath = await resolveBindingPath(pathFromHere); - const loadedBinding = await loadBinding(extendBindingPath); - extendsFrom.push(loadedBinding); - entries.push(...loadedBinding.entries); - } - // Aggregate file paths to import - const includedFilePaths = parsedBinding.files + const includedFilePaths = parsedBinding.sources .flatMap((thisGlob) => { return Array.from( new Glob(thisGlob).scanSync({ @@ -131,16 +119,15 @@ export async function loadBinding(initialPath: string): Promise { .filter((filePath) => filePath !== bindingPath); // Load files - const loadedEntries = await Promise.all( - includedFilePaths.map((filePath) => { - return parseMuseFile(filePath, { - contentKey: parsedBinding.contentKey, - }); - }), - ); - - // Add loaded entries to main list - entries.push(...loadedEntries.flat()); + const entries: MuseEntry[] = ( + await Promise.all( + includedFilePaths.map((filePath) => { + return parseMuseFile(filePath, { + contentKey: parsedBinding.contentKey, + }); + }), + ) + ).flat(); // Load and check processors const processors: MuseProcessor[] = await Promise.all( @@ -154,7 +141,6 @@ export async function loadBinding(initialPath: string): Promise { entries, options: parsedBinding.options, processors: processors, - imports: extendsFrom, meta: {}, }; } diff --git a/src/errors.ts b/src/errors.ts index 7c8c014..d5184d7 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -4,7 +4,7 @@ export class MuseError extends Error { } } -export class FileError extends MuseError { +export class MuseFileError extends MuseError { filePath: string; details?: string; @@ -19,7 +19,7 @@ export class FileError extends MuseError { } } -export class FileNotFoundError extends FileError { +export class MuseFileNotFoundError extends MuseFileError { constructor(filePath: string) { super("File not found", filePath); this.message = "File not found"; @@ -27,4 +27,4 @@ export class FileNotFoundError extends FileError { } } -export class CLIError extends MuseError {} +export class CLIError extends MuseError { } diff --git a/src/exports.ts b/src/exports.ts index 8a07b08..a05b13f 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -1,2 +1,3 @@ export type * from "./types"; +export * from "./errors"; export const DEFAULT_CONTENT_KEY = "content"; diff --git a/src/types.ts b/src/types.ts index f9faa6c..7aabb16 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +export type UnknownRecord = Record; + export interface CLIFlags { help: boolean; verbose: boolean; @@ -12,32 +14,27 @@ export interface CLIArguments { } export const BindingSchema = z.object({ - include: z.array(z.string()).default([]), contentKey: z.string().default("content"), - files: z + 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 interface Binding { - readonly _raw: z.infer; - readonly bindingPath: string; - readonly contentKey: string; - readonly entries: MuseEntry[]; - readonly meta: Record; - readonly options: Record; - readonly processors: MuseProcessor[]; - readonly imports: Binding[]; -} - 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; @@ -45,11 +42,19 @@ export interface MuseProcessor { readonly process: MuseProcessorFn; } -export interface MuseEntry { +export interface MuseEntry { _raw: string; filePath: string; data: Record; - meta: Record; + meta: MetaShape; } -export type UnknownRecord = Record; +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; +} From 3682a7a7633c15b18874d100b8327ba46c794153 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Thu, 3 Apr 2025 09:47:19 -0400 Subject: [PATCH 03/10] Add support for processors over http --- biome.json | 5 +---- bunfig.toml | 1 + src/binding.ts | 6 ++++-- src/cli.ts | 4 ++-- src/errors.ts | 2 +- src/parse.ts | 6 +++--- src/preload/http-plugin.js | 40 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 bunfig.toml create mode 100644 src/preload/http-plugin.js diff --git a/biome.json b/biome.json index 1814905..7988bf6 100644 --- a/biome.json +++ b/biome.json @@ -7,10 +7,7 @@ }, "files": { "ignoreUnknown": false, - "ignore": [ - "*.d.ts", - "*.json" - ], + "ignore": ["*.d.ts", "*.json"], "include": [ "./src/**/*.ts", "./src/**/*.tsx", diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..a7461c6 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1 @@ +preload = ["./src/preload/http-plugin.js"] diff --git a/src/binding.ts b/src/binding.ts index c1f9e96..ae2bcc2 100644 --- a/src/binding.ts +++ b/src/binding.ts @@ -1,6 +1,6 @@ import path, { dirname } from "node:path"; import { Glob } from "bun"; -import { MuseFileNotFoundError, MuseError } from "./errors"; +import { MuseError, MuseFileNotFoundError } from "./errors"; import { parseMuseFile } from "./parse"; import { type Binding, @@ -14,7 +14,9 @@ async function loadProcessor( bindingDirname: string, processorPath: string, ): Promise { - const resolvedProcessorPath = path.resolve(bindingDirname, processorPath); + const resolvedProcessorPath = processorPath.startsWith("http") + ? processorPath + : path.resolve(bindingDirname, processorPath); const { process, name, description } = await import(resolvedProcessorPath); if (!process || typeof process !== "function") { diff --git a/src/cli.ts b/src/cli.ts index 762d30e..c8380ab 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,8 +1,8 @@ import { parseArgs } from "node:util"; import chalk from "chalk"; -import { MuseError } from "./errors"; -import { loadBinding } from "./binding"; import { version } from "../package.json"; +import { loadBinding } from "./binding"; +import { MuseError } from "./errors"; import type { Binding, CLIArguments } from "./types"; interface Loggers { diff --git a/src/errors.ts b/src/errors.ts index d5184d7..47d98d5 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -27,4 +27,4 @@ export class MuseFileNotFoundError extends MuseFileError { } } -export class CLIError extends MuseError { } +export class CLIError extends MuseError {} diff --git a/src/parse.ts b/src/parse.ts index 842f6d2..509bf63 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -1,7 +1,7 @@ -import { normalize, extname } from "node:path"; -import YAML from "yaml"; -import TOML from "smol-toml"; +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"; function parseYAMLEntries(text: string): UnknownRecord[] { diff --git a/src/preload/http-plugin.js b/src/preload/http-plugin.js new file mode 100644 index 0000000..bb7f732 --- /dev/null +++ b/src/preload/http-plugin.js @@ -0,0 +1,40 @@ +const rx_any = /./; +const rx_http = /^https?:\/\//; +const rx_relative_path = /^\.\.?\//; +const rx_absolute_path = /^\//; + +async function load_http_module(href) { + return fetch(href).then((response) => + response + .text() + .then((text) => + response.ok + ? { contents: text, loader: "js" } + : Promise.reject( + new Error(`Failed to load module '${href}'': ${text}`), + ), + ), + ); +} + +Bun.plugin({ + name: "http_imports", + setup(build) { + build.onResolve({ filter: rx_relative_path }, (args) => { + if (rx_http.test(args.importer)) { + return { path: new URL(args.path, args.importer).href }; + } + }); + build.onResolve({ filter: rx_absolute_path }, (args) => { + if (rx_http.test(args.importer)) { + return { path: new URL(args.path, args.importer).href }; + } + }); + build.onLoad({ filter: rx_any, namespace: "http" }, (args) => + load_http_module(`http:${args.path}`), + ); + build.onLoad({ filter: rx_any, namespace: "https" }, (args) => + load_http_module(`https:${args.path}`), + ); + }, +}); From 16660823ea040bf9f419b73f96614ab209eda9fa Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Thu, 3 Apr 2025 10:16:08 -0400 Subject: [PATCH 04/10] 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", ] } From 504c1e6f3c340235d2ca4e38a17c683555a294ba Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Thu, 3 Apr 2025 10:16:48 -0400 Subject: [PATCH 05/10] 0.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ba1d46c..36bf54a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@endeavorance/muse", - "version": "0.1.0", + "version": "0.2.0", "module": "dist/index.js", "exports": { ".": { From ae1b9e262e2967516bb6927f0544a25bf6f249f6 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Thu, 3 Apr 2025 14:55:47 -0400 Subject: [PATCH 06/10] Update plugin shape --- demo/main/processors/announce-slam-dunks.js | 11 ++- demo/main/processors/scream.js | 4 +- src/cli/index.ts | 51 +++++++------- src/core/binding.ts | 24 +++---- src/core/files.ts | 32 +++++++++ src/core/parse.ts | 75 +++++++++------------ src/core/plugins.ts | 52 ++++++++++++++ src/core/processor.ts | 42 ------------ src/{ => core}/types.ts | 2 - src/errors.ts | 26 ++++--- src/exports.ts | 8 ++- tsconfig.json | 3 - 12 files changed, 181 insertions(+), 149 deletions(-) create mode 100644 src/core/files.ts create mode 100644 src/core/plugins.ts delete mode 100644 src/core/processor.ts rename src/{ => core}/types.ts (67%) diff --git a/demo/main/processors/announce-slam-dunks.js b/demo/main/processors/announce-slam-dunks.js index 56824e5..e1889a3 100644 --- a/demo/main/processors/announce-slam-dunks.js +++ b/demo/main/processors/announce-slam-dunks.js @@ -1,14 +1,19 @@ export const name = "Announce Slam Dunks"; export const description = "Get hype when a file has slam dunks"; -export const process = (binding, { dunks }) => { +export const step = ({ entries, options, ...rest }) => { + const { dunks } = options; const slamDunkKey = dunks?.key ?? "slamDunks"; - for (const entry of binding.entries) { + for (const entry of entries) { if (slamDunkKey in entry.data) { console.log(`Slam dunk!`); } } - return binding; + return { + entries, + options, + ...rest, + }; }; diff --git a/demo/main/processors/scream.js b/demo/main/processors/scream.js index 17e0bac..4a8a629 100644 --- a/demo/main/processors/scream.js +++ b/demo/main/processors/scream.js @@ -1,6 +1,4 @@ -export const name = "Scream"; - -export function process(binding) { +export function step(binding) { const modifiedEntries = binding.entries.map(entry => { if (binding.markdownKey in entry.data) { entry.data = { diff --git a/src/cli/index.ts b/src/cli/index.ts index 6e631a0..edb3d9d 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,6 +1,7 @@ import { parseArgs } from "node:util"; import chalk from "chalk"; -import { type Binding, loadBinding } from "#core/binding"; +import { loadBinding } from "#core/binding"; +import type { MusePlugin } from "#core/plugins"; import { MuseError } from "#errors"; import { version } from "../../package.json"; @@ -68,50 +69,46 @@ function parseCLIArguments(argv: string[]): [string, CLIFlags] { return [args[0] ?? "./", options]; } +function getStepLogLine(plugin: MusePlugin): string { + const { name, description } = plugin; + let processorLogLine = chalk.bold(`↪ ${name}`); + + if (description && description.length > 0) { + processorLogLine += chalk.dim(` (${description})`); + } + + return processorLogLine; +} + async function processBinding( inputFilePath: string, flags: CLIFlags, { log, verbose }: Loggers, ) { // Load the binding - const binding = await loadBinding(inputFilePath); - + let binding = await loadBinding(inputFilePath); verbose(`Binding ${binding.bindingPath}`); - const stepWord = binding.processors.length === 1 ? "step" : "steps"; - log( - `Processing ${binding.entries.length} entries with ${binding.processors.length} ${stepWord}`, - ); + const entryCount = binding.entries.length; + const stepCount = binding.plugins.length; + const stepWord = stepCount === 1 ? "step" : "steps"; + log(`Processing ${entryCount} entries with ${stepCount} ${stepWord}`); const processStart = performance.now(); - // Run the data through all processors - const processedSteps: Binding[] = [binding]; - for (const processor of binding.processors) { - const lastStep = processedSteps[processedSteps.length - 1]; - - const { process, name, description } = processor; - - 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); + // Run the data through relevant plugins + for (const plugin of binding.plugins) { + log(getStepLogLine(plugin)); + const { step } = plugin; + binding = await step(binding); } const processEnd = performance.now(); const processTime = ((processEnd - processStart) / 1000).toFixed(2); verbose(`Processing completed in ${processTime}s`); - const finalState = processedSteps[processedSteps.length - 1]; - const serialized = JSON.stringify(finalState.entries, null, 2); - if (flags.stdout) { - console.log(serialized); + console.log(JSON.stringify(binding, null, 2)); } return ExitCode.Success; } diff --git a/src/core/binding.ts b/src/core/binding.ts index eee86b1..b78292d 100644 --- a/src/core/binding.ts +++ b/src/core/binding.ts @@ -2,10 +2,10 @@ import path, { dirname } from "node:path"; import { Glob } from "bun"; 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"; +import { type MusePlugin, loadPlugin } from "./plugins"; +import type { UnknownRecord } from "./types"; // Function to parse an unknown object into parts of a binding export const BindingSchema = z.object({ @@ -37,7 +37,7 @@ export interface Binding { readonly entries: MuseEntry[]; /** A list of processors to be applied to the entries */ - readonly processors: MuseProcessor[]; + readonly plugins: MusePlugin[]; /** Arbitrary metadata for processors to use to cache project data */ readonly meta: MetaShape; @@ -75,20 +75,14 @@ async function resolveBindingPath(bindingPath: string): Promise { } } - throw new MuseFileNotFoundError( - bindingPath, - "Failed to find a binding file at the provided directory.", - ); + 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, - "Provided binding path does not exist.", - ); + throw new MuseFileNotFoundError(bindingPath); } // If it is a file, return the path @@ -133,9 +127,9 @@ export async function loadBinding(initialPath: string): Promise { ) ).flat(); - // Load and check processors - const processors: MuseProcessor[] = await Promise.all( - parsedBinding.processors.map(loadProcessor.bind(null, bindingDirname)), + // Load and check plugins + const plugins: MusePlugin[] = await Promise.all( + parsedBinding.processors.map(loadPlugin.bind(null, bindingDirname)), ); return { @@ -144,7 +138,7 @@ export async function loadBinding(initialPath: string): Promise { contentKey: parsedBinding.contentKey, entries, options: parsedBinding.options, - processors: processors, + plugins, meta: {}, }; } diff --git a/src/core/files.ts b/src/core/files.ts new file mode 100644 index 0000000..a32b877 --- /dev/null +++ b/src/core/files.ts @@ -0,0 +1,32 @@ +import { basename, dirname, extname, normalize } from "node:path"; +import { MuseFileNotFoundError } from "#errors"; + +export interface LoadedFile { + content: string; + filePath: string; + fileType: string; + dirname: string; + basename: string; +} + +export async function loadFileContent(filePath: string): Promise { + const normalizedPath = normalize(filePath); + const file = Bun.file(normalizedPath); + + const exists = await file.exists(); + if (!exists) { + throw new MuseFileNotFoundError(normalizedPath); + } + + const filecontent = await file.text(); + + const extension = extname(normalizedPath); + + return { + content: filecontent, + filePath: normalizedPath, + fileType: extension.slice(1), + dirname: dirname(normalizedPath), + basename: basename(normalizedPath, extension), + }; +} diff --git a/src/core/parse.ts b/src/core/parse.ts index 128de7f..6617db2 100644 --- a/src/core/parse.ts +++ b/src/core/parse.ts @@ -1,9 +1,8 @@ -import { extname, normalize } from "node:path"; import EMDY from "@endeavorance/emdy"; import TOML from "smol-toml"; import YAML from "yaml"; -import type { UnknownRecord } from "#types"; -import { MuseFileNotFoundError } from "#errors"; +import { loadFileContent } from "./files"; +import type { UnknownRecord } from "./types"; export interface MuseEntry { _raw: string; @@ -12,6 +11,20 @@ export interface MuseEntry { meta: MetaShape; } +function formatEntries(val: unknown): UnknownRecord[] { + if (Array.isArray(val) && val.every((el) => typeof el === "object")) { + return val; + } + + if (typeof val === "object" && val !== null) { + return [val as UnknownRecord]; + } + + throw new Error( + `Invalid data format. Entry files must define an object or array of objects, but found "${val}"`, + ); +} + function parseYAMLEntries(text: string): UnknownRecord[] { const parsedDocs = YAML.parseAllDocuments(text); @@ -32,31 +45,15 @@ function parseYAMLEntries(text: string): UnknownRecord[] { } function parseJSONEntries(text: string): UnknownRecord[] { - const parsed = JSON.parse(text); - - if (Array.isArray(parsed)) { - return parsed; - } - - if (typeof parsed === "object") { - return [parsed]; - } - - throw new Error("JSON resource must be an object or an array of objects"); + return formatEntries(JSON.parse(text)); } function parseTOMLEntries(text: string): UnknownRecord[] { - const parsed = TOML.parse(text); + return formatEntries(TOML.parse(text)); +} - if (Array.isArray(parsed)) { - return parsed; - } - - if (typeof parsed === "object") { - return [parsed]; - } - - throw new Error("TOML resource must be an object or an array of objects"); +function parseMarkdownEntry(text: string, contentKey: string): UnknownRecord[] { + return formatEntries(EMDY.parse(text, contentKey)); } interface ParseMuseFileOptions { @@ -67,49 +64,37 @@ export async function parseMuseFile( rawFilePath: string, { contentKey = "content" }: ParseMuseFileOptions = {}, ): Promise { - 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 { content, filePath, fileType } = await loadFileContent(rawFilePath); const partial = { - _raw: rawFileContent, + _raw: content, filePath, meta: {}, }; if (fileType === "md") { - const parsed = EMDY.parse(rawFileContent, contentKey); - return [ - { - ...partial, - data: parsed, - }, - ]; + return parseMarkdownEntry(content, contentKey).map((data) => ({ + ...partial, + data, + })); } if (fileType === "yaml") { - return parseYAMLEntries(rawFileContent).map((data) => ({ + return parseYAMLEntries(content).map((data) => ({ ...partial, data, })); } if (fileType === "json") { - return parseJSONEntries(rawFileContent).map((data) => ({ + return parseJSONEntries(content).map((data) => ({ ...partial, data, })); } if (fileType === "toml") { - return parseTOMLEntries(rawFileContent).map((data) => ({ + return parseTOMLEntries(content).map((data) => ({ ...partial, data, })); diff --git a/src/core/plugins.ts b/src/core/plugins.ts new file mode 100644 index 0000000..3f2ffed --- /dev/null +++ b/src/core/plugins.ts @@ -0,0 +1,52 @@ +import path from "node:path"; +import { MuseError, MusePluginError } from "#errors"; +import type { Binding } from "./binding"; + +export type MuseStepFn = (binding: Binding) => Promise; + +export interface MusePlugin { + readonly filePath: string; + readonly name: string; + readonly description: string; + readonly version: string; + readonly step: MuseStepFn; +} + +export async function loadPlugin( + bindingDirname: string, + pluginPath: string, +): Promise { + // Resolve local paths, use URLs as-is + const resolvedProcessorPath = pluginPath.startsWith("http") + ? pluginPath + : path.resolve(bindingDirname, pluginPath); + const { step, name, description, version } = await import( + resolvedProcessorPath + ); + + if (!step || typeof step !== "function") { + throw new MuseError( + `Processor at ${pluginPath} does not export a step() function`, + ); + } + + if (name && typeof name !== "string") { + throw new MusePluginError(pluginPath, "Plugin name is not a string."); + } + + if (description && typeof description !== "string") { + throw new MusePluginError(pluginPath, "Plugin description is not a string"); + } + + if (version && typeof version !== "string") { + throw new MusePluginError(pluginPath, "Plugin version is not a string"); + } + + return { + filePath: resolvedProcessorPath, + step: step as MuseStepFn, + name: String(name ?? pluginPath), + description: String(description ?? ""), + version: String(version ?? "0.0.0"), + }; +} diff --git a/src/core/processor.ts b/src/core/processor.ts deleted file mode 100644 index 1df9753..0000000 --- a/src/core/processor.ts +++ /dev/null @@ -1,42 +0,0 @@ -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/types.ts b/src/core/types.ts similarity index 67% rename from src/types.ts rename to src/core/types.ts index b57ec2f..35ba466 100644 --- a/src/types.ts +++ b/src/core/types.ts @@ -1,3 +1 @@ -import { z } from "zod"; - export type UnknownRecord = Record; diff --git a/src/errors.ts b/src/errors.ts index 9ad54ac..5f43323 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,30 +1,40 @@ export class MuseError extends Error { fmt(): string { - return this.message; + return `-- ERROR -- \nDetails: ${this.message}`; + } +} + +export class MusePluginError extends MuseError { + plugin: string; + + constructor(message: string, plugin: string) { + super(message); + this.plugin = plugin; + } + + fmt(): string { + return `-- PLUGIN ERROR --\Plugin: ${this.plugin}\nDetails: ${this.message}`; } } export class MuseFileError extends MuseError { filePath: string; - details?: string; - constructor(message: string, filePath: string, details?: string) { + constructor(message: string, filePath: string) { super(message); this.filePath = filePath; - this.details = details; } fmt(): string { - return `-- ${this.message} --\nFile Path: ${this.filePath}\nDetails: ${this.details}`; + return `-- FILE ERROR --\nFile Path: ${this.filePath}\nDetails: ${this.message}`; } } export class MuseFileNotFoundError extends MuseFileError { - constructor(filePath: string, details?: string) { + constructor(filePath: string) { super("File not found", filePath); - this.message = "File not found"; + this.message = "File does not exist"; this.filePath = filePath; - this.details = details ?? "No additional information."; } } diff --git a/src/exports.ts b/src/exports.ts index 65319af..f520a46 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -1,3 +1,9 @@ -export type * from "#types"; +// Content exports export * from "#errors"; + +// Constants export const DEFAULT_CONTENT_KEY = "content"; + +// Types +export type * from "#types"; +export type { MusePlugin } from "#core/plugins"; diff --git a/tsconfig.json b/tsconfig.json index 0f25433..a964c9c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,9 +33,6 @@ "#util": [ "./util.ts" ], - "#types": [ - "./types.ts" - ], "#errors": [ "./errors.ts" ] From 5b90a8e1b32e8f7e11644e7cca36350e80a51dd5 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Fri, 4 Apr 2025 13:50:43 -0400 Subject: [PATCH 07/10] Support for url based resources --- demo/main/binding.md | 3 + src/core/binding.ts | 94 ++++++++++------ src/core/files.ts | 2 + src/core/parse.ts | 104 ------------------ src/core/sources.ts | 254 +++++++++++++++++++++++++++++++++++++++++++ src/muse.ts | 20 ++++ 6 files changed, 342 insertions(+), 135 deletions(-) delete mode 100644 src/core/parse.ts create mode 100644 src/core/sources.ts create mode 100644 src/muse.ts diff --git a/demo/main/binding.md b/demo/main/binding.md index b660a51..cb097e7 100644 --- a/demo/main/binding.md +++ b/demo/main/binding.md @@ -3,6 +3,9 @@ markdownKey: description options: dunks: key: dunkaroos +sources: + - file: ./stats/*.toml + - url: https://git.astral.camp/endeavorance/emdy/raw/branch/main/package.json processors: - ./processors/scream.js - ./processors/announce-slam-dunks.js diff --git a/src/core/binding.ts b/src/core/binding.ts index b78292d..7db7c5e 100644 --- a/src/core/binding.ts +++ b/src/core/binding.ts @@ -1,21 +1,37 @@ import path, { dirname } from "node:path"; -import { Glob } from "bun"; import { z } from "zod"; import { MuseError, MuseFileNotFoundError } from "#errors"; import { resolveFilePath } from "#util"; -import { type MuseEntry, parseMuseFile } from "./parse"; +import { + type MuseEntry, + type MuseSource, + loadFromSource, + parseMuseFile, +} from "./sources"; import { type MusePlugin, loadPlugin } from "./plugins"; import type { UnknownRecord } from "./types"; +const SourceSchema = z.union([ + z.object({ + file: z.string(), + }), + z.object({ + url: z.string().url(), + }), +]); + +type SourceShorthand = z.infer; + // Function to parse an unknown object into parts of a binding -export const BindingSchema = z.object({ +const BindingSchema = z.object({ contentKey: z.string().default("content"), - sources: z - .array(z.string()) - .default(["**/*.yaml", "**/*.md", "**/*.toml", "**/*.json"]), + sources: z.array(SourceSchema).default([ + { + file: "./**/*.{json,yaml,toml,md}", + }, + ]), options: z.record(z.string(), z.any()).default({}), processors: z.array(z.string()).default([]), - renderer: z.string().optional(), }); /** @@ -27,6 +43,9 @@ export interface Binding { /** The raw data read from the binding file */ readonly _raw: z.infer; + /** Information about the sources used to load entries */ + readonly sources: MuseSource[]; + /** The full file path to the binding file */ readonly bindingPath: string; @@ -46,6 +65,24 @@ export interface Binding { readonly options: UnknownRecord; } +function sourceShorthandToSource(shorthand: SourceShorthand): MuseSource { + if ("file" in shorthand) { + return { + location: shorthand.file, + type: "file", + }; + } + + if ("url" in shorthand) { + return { + location: shorthand.url, + type: "url", + }; + } + + throw new MuseError("Invalid source shorthand"); +} + /** * Given a path, find the binding file * If the path is to a directory, check for a binding inside @@ -93,7 +130,11 @@ export async function loadBinding(initialPath: string): Promise { // Resolve the file location if possible const bindingPath = await resolveBindingPath(initialPath); const bindingDirname = dirname(bindingPath); - const loadedBindingData = await parseMuseFile(bindingPath); + const loadedBindingData = await parseMuseFile(bindingPath, { + contentKey: "content", + cwd: bindingDirname, + ignore: [], + }); if (loadedBindingData.length === 0) { throw new MuseError("No entries found in binding file"); @@ -102,30 +143,20 @@ export async function loadBinding(initialPath: string): Promise { // Load and parse the Binding YAML content const parsedBinding = BindingSchema.parse(loadedBindingData[0].data); - // Aggregate file paths to import - const includedFilePaths = parsedBinding.sources - .flatMap((thisGlob) => { - return Array.from( - new Glob(thisGlob).scanSync({ - cwd: bindingDirname, - absolute: true, - followSymlinks: true, - onlyFiles: true, - }), - ); - }) - .filter((filePath) => filePath !== bindingPath); + const sourceOptions = { + cwd: bindingDirname, + contentKey: parsedBinding.contentKey, + ignore: [bindingPath], + }; - // Load files - const entries: MuseEntry[] = ( - await Promise.all( - includedFilePaths.map((filePath) => { - return parseMuseFile(filePath, { - contentKey: parsedBinding.contentKey, - }); - }), - ) - ).flat(); + const sources = parsedBinding.sources.map(sourceShorthandToSource); + + const entries: MuseEntry[] = []; + + for (const source of sources) { + const entriesFromSource = await loadFromSource(source, sourceOptions); + entries.push(...entriesFromSource); + } // Load and check plugins const plugins: MusePlugin[] = await Promise.all( @@ -134,6 +165,7 @@ export async function loadBinding(initialPath: string): Promise { return { _raw: parsedBinding, + sources, bindingPath, contentKey: parsedBinding.contentKey, entries, diff --git a/src/core/files.ts b/src/core/files.ts index a32b877..89b2817 100644 --- a/src/core/files.ts +++ b/src/core/files.ts @@ -7,6 +7,7 @@ export interface LoadedFile { fileType: string; dirname: string; basename: string; + mimeType: string; } export async function loadFileContent(filePath: string): Promise { @@ -28,5 +29,6 @@ export async function loadFileContent(filePath: string): Promise { fileType: extension.slice(1), dirname: dirname(normalizedPath), basename: basename(normalizedPath, extension), + mimeType: file.type, }; } diff --git a/src/core/parse.ts b/src/core/parse.ts deleted file mode 100644 index 6617db2..0000000 --- a/src/core/parse.ts +++ /dev/null @@ -1,104 +0,0 @@ -import EMDY from "@endeavorance/emdy"; -import TOML from "smol-toml"; -import YAML from "yaml"; -import { loadFileContent } from "./files"; -import type { UnknownRecord } from "./types"; - -export interface MuseEntry { - _raw: string; - filePath: string; - data: Record; - meta: MetaShape; -} - -function formatEntries(val: unknown): UnknownRecord[] { - if (Array.isArray(val) && val.every((el) => typeof el === "object")) { - return val; - } - - if (typeof val === "object" && val !== null) { - return [val as UnknownRecord]; - } - - throw new Error( - `Invalid data format. Entry files must define an object or array of objects, but found "${val}"`, - ); -} - -function parseYAMLEntries(text: string): UnknownRecord[] { - const parsedDocs = YAML.parseAllDocuments(text); - - if (parsedDocs.some((doc) => doc.toJS() === null)) { - throw new Error("Encountered NULL resource"); - } - - const errors = parsedDocs.flatMap((doc) => doc.errors); - - if (errors.length > 0) { - throw new Error( - `Error parsing YAML resource: ${errors.map((e) => e.message).join(", ")}`, - ); - } - - const collection: UnknownRecord[] = parsedDocs.map((doc) => doc.toJS()); - return collection; -} - -function parseJSONEntries(text: string): UnknownRecord[] { - return formatEntries(JSON.parse(text)); -} - -function parseTOMLEntries(text: string): UnknownRecord[] { - return formatEntries(TOML.parse(text)); -} - -function parseMarkdownEntry(text: string, contentKey: string): UnknownRecord[] { - return formatEntries(EMDY.parse(text, contentKey)); -} - -interface ParseMuseFileOptions { - contentKey?: string; -} - -export async function parseMuseFile( - rawFilePath: string, - { contentKey = "content" }: ParseMuseFileOptions = {}, -): Promise { - const { content, filePath, fileType } = await loadFileContent(rawFilePath); - - const partial = { - _raw: content, - filePath, - meta: {}, - }; - - if (fileType === "md") { - return parseMarkdownEntry(content, contentKey).map((data) => ({ - ...partial, - data, - })); - } - - if (fileType === "yaml") { - return parseYAMLEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - if (fileType === "json") { - return parseJSONEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - if (fileType === "toml") { - return parseTOMLEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - throw new Error(`Unsupported file type: ${fileType}`); -} diff --git a/src/core/sources.ts b/src/core/sources.ts new file mode 100644 index 0000000..8b57103 --- /dev/null +++ b/src/core/sources.ts @@ -0,0 +1,254 @@ +import EMDY from "@endeavorance/emdy"; +import TOML from "smol-toml"; +import YAML from "yaml"; +import { loadFileContent } from "./files"; +import type { UnknownRecord } from "./types"; +import { Glob } from "bun"; + +export type SourceType = "file" | "url"; + +export interface MuseSource { + location: string; + type: SourceType; +} + +interface SourceOptions { + cwd: string; + contentKey: string; + ignore: string[]; +} + +export interface MuseEntry { + _raw: string; + location: string; + data: Record; + meta: MetaShape; + source: MuseSource; +} + +function formatEntries(val: unknown): UnknownRecord[] { + if (Array.isArray(val) && val.every((el) => typeof el === "object")) { + return val; + } + + if (typeof val === "object" && val !== null) { + return [val as UnknownRecord]; + } + + throw new Error( + `Invalid data format. Entry files must define an object or array of objects, but found "${val}"`, + ); +} + +function parseYAMLEntries(text: string): UnknownRecord[] { + const parsedDocs = YAML.parseAllDocuments(text); + + if (parsedDocs.some((doc) => doc.toJS() === null)) { + throw new Error("Encountered NULL resource"); + } + + const errors = parsedDocs.flatMap((doc) => doc.errors); + + if (errors.length > 0) { + throw new Error( + `Error parsing YAML resource: ${errors.map((e) => e.message).join(", ")}`, + ); + } + + const collection: UnknownRecord[] = parsedDocs.map((doc) => doc.toJS()); + return collection; +} + +function parseJSONEntries(text: string): UnknownRecord[] { + return formatEntries(JSON.parse(text)); +} + +function parseTOMLEntries(text: string): UnknownRecord[] { + return formatEntries(TOML.parse(text)); +} + +function parseMarkdownEntry(text: string, contentKey: string): UnknownRecord[] { + return formatEntries(EMDY.parse(text, contentKey)); +} + +export async function parseMuseFile( + rawFilePath: string, + { contentKey = "content" }: SourceOptions, +): Promise { + const { content, filePath, fileType } = await loadFileContent(rawFilePath); + + const partial = { + _raw: content, + source: { + location: filePath, + type: "file" as SourceType, + }, + location: filePath, + meta: {}, + }; + + if (fileType === "md") { + return parseMarkdownEntry(content, contentKey).map((data) => ({ + ...partial, + data, + })); + } + + if (fileType === "yaml") { + return parseYAMLEntries(content).map((data) => ({ + ...partial, + data, + })); + } + + if (fileType === "json") { + return parseJSONEntries(content).map((data) => ({ + ...partial, + data, + })); + } + + if (fileType === "toml") { + return parseTOMLEntries(content).map((data) => ({ + ...partial, + data, + })); + } + + throw new Error(`Unsupported file type: ${fileType}`); +} + +async function loadFromFileSource( + source: MuseSource, + options: SourceOptions, +): Promise { + const paths = Array.from( + new Glob(source.location).scanSync({ + cwd: options.cwd, + absolute: true, + followSymlinks: true, + onlyFiles: true, + }), + ); + + const filteredPaths = paths.filter((path) => !options.ignore.includes(path)); + + const entries: MuseEntry[] = []; + for (const filePath of filteredPaths) { + const fileEntries = await parseMuseFile(filePath, options); + entries.push(...fileEntries); + } + + return entries; +} + +function getFileExtensionFromURL(url: string): string | null { + const parsedUrl = new URL(url); + + const pathname = parsedUrl.pathname; + const filename = pathname.substring(pathname.lastIndexOf("/") + 1); + const extension = filename.substring(filename.lastIndexOf(".") + 1); + + return extension === filename ? null : extension; +} + +function mimeTypeToFileType(mimeType: string): string | null { + switch (mimeType) { + case "text/markdown": + return "md"; + case "text/yaml": + return "yaml"; + case "application/x-yaml": + return "yaml"; + case "application/json": + return "json"; + case "application/toml": + return "toml"; + default: + return null; + } +} + +async function loadFromURLSource( + source: MuseSource, + options: SourceOptions, +): Promise { + const { contentKey = "content" } = options; + const response = await fetch(source.location); + + if (!response.ok) { + throw new Error(`Failed to fetch URL: ${source.location}`); + } + const content = await response.text(); + const mimeType = response.headers.get("Content-Type") || "unknown"; + + const parseType = + mimeTypeToFileType(mimeType) ?? + getFileExtensionFromURL(source.location) ?? + "unknown"; + + const partial = { + _raw: content, + source: { + location: source.location, + type: "url" as SourceType, + }, + location: source.location, + meta: {}, + }; + + if (parseType === "md") { + return parseMarkdownEntry(content, contentKey).map((data) => ({ + ...partial, + data, + })); + } + + if (parseType === "yaml") { + return parseYAMLEntries(content).map((data) => ({ + ...partial, + data, + })); + } + + if (parseType === "json") { + return parseJSONEntries(content).map((data) => ({ + ...partial, + data, + })); + } + + if (parseType === "toml") { + return parseTOMLEntries(content).map((data) => ({ + ...partial, + data, + })); + } + + // If it doesnt match one of these, try brute force parsing + // and return the result if any of them work + try { + return parseMarkdownEntry(content, contentKey).map((data) => ({ + ...partial, + data, + })); + } catch { + // noop + } + + throw new Error(`Unsupported MIME type from URL source: ${mimeType}`); +} + +export async function loadFromSource( + source: MuseSource, + options: SourceOptions, +): Promise { + switch (source.type) { + case "file": + return loadFromFileSource(source, options); + case "url": + return loadFromURLSource(source, options); + default: + throw new Error(`Unsupported source type: ${source.type}`); + } +} diff --git a/src/muse.ts b/src/muse.ts new file mode 100644 index 0000000..d2b61f9 --- /dev/null +++ b/src/muse.ts @@ -0,0 +1,20 @@ +import { loadBinding, type Binding } from "#core/binding"; + +export class Muse { + bindingLoaded = false; + preliminaryBinding: Partial = {}; + additionalSources: string[] = []; + additionalPluginPaths: string[] = []; + + constructor(bindingConfig: Partial) { + this.preliminaryBinding = bindingConfig; + } + + source(newSource: string) { + this.additionalSources.push(newSource); + } + + plugin(pluginPath: string) { + this.additionalPluginPaths.push(pluginPath); + } +} From ffaeb4841eb133e124dfeb00a79c7fc83fe44687 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Fri, 4 Apr 2025 14:05:15 -0400 Subject: [PATCH 08/10] Clean up record management --- src/core/binding.ts | 4 +- src/core/records.ts | 107 +++++++++++++++++++++++++++ src/core/sources.ts | 173 ++++++++------------------------------------ src/core/types.ts | 1 - src/muse.ts | 2 +- 5 files changed, 139 insertions(+), 148 deletions(-) create mode 100644 src/core/records.ts delete mode 100644 src/core/types.ts diff --git a/src/core/binding.ts b/src/core/binding.ts index 7db7c5e..d0528b5 100644 --- a/src/core/binding.ts +++ b/src/core/binding.ts @@ -1,15 +1,15 @@ import path, { dirname } from "node:path"; import { z } from "zod"; +import type { UnknownRecord } from "#core/records"; import { MuseError, MuseFileNotFoundError } from "#errors"; import { resolveFilePath } from "#util"; +import { type MusePlugin, loadPlugin } from "./plugins"; import { type MuseEntry, type MuseSource, loadFromSource, parseMuseFile, } from "./sources"; -import { type MusePlugin, loadPlugin } from "./plugins"; -import type { UnknownRecord } from "./types"; const SourceSchema = z.union([ z.object({ diff --git a/src/core/records.ts b/src/core/records.ts new file mode 100644 index 0000000..891b18e --- /dev/null +++ b/src/core/records.ts @@ -0,0 +1,107 @@ +import EMDY from "@endeavorance/emdy"; +import TOML from "smol-toml"; +import YAML from "yaml"; +import type { LoadedFile } from "./files"; + +export type UnknownRecord = Record; + +/** + * Given an unknown shape, ensure it is an object or array of objects, + * then return it as an array of objects. + * + * @param val - The unknown value to format + * @returns - An array of objects + */ +function formatEntries(val: unknown): UnknownRecord[] { + if (Array.isArray(val) && val.every((el) => typeof el === "object")) { + return val; + } + + if (typeof val === "object" && val !== null) { + return [val as UnknownRecord]; + } + + throw new Error( + `Invalid data format. Entry files must define an object or array of objects, but found "${val}"`, + ); +} + +/** + * Parse one or more YAML documents from a string + * + * @param text - The YAML string to parse + * @returns - An array of parsed objects + */ +export function parseYAMLEntries(text: string): UnknownRecord[] { + const parsedDocs = YAML.parseAllDocuments(text); + + if (parsedDocs.some((doc) => doc.toJS() === null)) { + throw new Error("Encountered NULL resource"); + } + + const errors = parsedDocs.flatMap((doc) => doc.errors); + + if (errors.length > 0) { + throw new Error( + `Error parsing YAML resource: ${errors.map((e) => e.message).join(", ")}`, + ); + } + + const collection: UnknownRecord[] = parsedDocs.map((doc) => doc.toJS()); + return collection; +} + +/** + * Parse one or more JSON documents from a string + * + * @param text - The JSON string to parse + * @returns - An array of parsed objects + */ +export function parseJSONEntries(text: string): UnknownRecord[] { + return formatEntries(JSON.parse(text)); +} + +/** + * Parse one or more TOML documents from a string + * + * @param text - The TOML string to parse + * @returns - An array of parsed objects + */ +export function parseTOMLEntries(text: string): UnknownRecord[] { + return formatEntries(TOML.parse(text)); +} + +/** + * Parse one or more Markdown documents from a string + * + * @param text - The Markdown string to parse + * @returns - An array of parsed objects + */ +export function parseMarkdownEntry( + text: string, + contentKey: string, +): UnknownRecord[] { + return formatEntries(EMDY.parse(text, contentKey)); +} + +export function autoParseEntry(loadedFile: LoadedFile, contentKey: string) { + const { fileType, content } = loadedFile; + + if (fileType === "md") { + return parseMarkdownEntry(content, contentKey); + } + + if (fileType === "yaml") { + return parseYAMLEntries(content); + } + + if (fileType === "json") { + return parseJSONEntries(content); + } + + if (fileType === "toml") { + return parseTOMLEntries(content); + } + + throw new Error(`Unsupported file type: ${fileType}`); +} diff --git a/src/core/sources.ts b/src/core/sources.ts index 8b57103..0ab1495 100644 --- a/src/core/sources.ts +++ b/src/core/sources.ts @@ -1,12 +1,10 @@ -import EMDY from "@endeavorance/emdy"; -import TOML from "smol-toml"; -import YAML from "yaml"; -import { loadFileContent } from "./files"; -import type { UnknownRecord } from "./types"; import { Glob } from "bun"; +import { type LoadedFile, loadFileContent } from "./files"; +import { type UnknownRecord, autoParseEntry } from "./records"; export type SourceType = "file" | "url"; +/** A descriptor of a location to load into a Muse binding */ export interface MuseSource { location: string; type: SourceType; @@ -18,6 +16,7 @@ interface SourceOptions { ignore: string[]; } +/** A single loaded entry from a Muse source */ export interface MuseEntry { _raw: string; location: string; @@ -26,96 +25,27 @@ export interface MuseEntry { source: MuseSource; } -function formatEntries(val: unknown): UnknownRecord[] { - if (Array.isArray(val) && val.every((el) => typeof el === "object")) { - return val; - } - - if (typeof val === "object" && val !== null) { - return [val as UnknownRecord]; - } - - throw new Error( - `Invalid data format. Entry files must define an object or array of objects, but found "${val}"`, - ); -} - -function parseYAMLEntries(text: string): UnknownRecord[] { - const parsedDocs = YAML.parseAllDocuments(text); - - if (parsedDocs.some((doc) => doc.toJS() === null)) { - throw new Error("Encountered NULL resource"); - } - - const errors = parsedDocs.flatMap((doc) => doc.errors); - - if (errors.length > 0) { - throw new Error( - `Error parsing YAML resource: ${errors.map((e) => e.message).join(", ")}`, - ); - } - - const collection: UnknownRecord[] = parsedDocs.map((doc) => doc.toJS()); - return collection; -} - -function parseJSONEntries(text: string): UnknownRecord[] { - return formatEntries(JSON.parse(text)); -} - -function parseTOMLEntries(text: string): UnknownRecord[] { - return formatEntries(TOML.parse(text)); -} - -function parseMarkdownEntry(text: string, contentKey: string): UnknownRecord[] { - return formatEntries(EMDY.parse(text, contentKey)); -} - export async function parseMuseFile( rawFilePath: string, { contentKey = "content" }: SourceOptions, ): Promise { - const { content, filePath, fileType } = await loadFileContent(rawFilePath); + const file = await loadFileContent(rawFilePath); + const entries = autoParseEntry(file, contentKey); const partial = { - _raw: content, + _raw: file.content, source: { - location: filePath, + location: file.filePath, type: "file" as SourceType, }, - location: filePath, + location: file.filePath, meta: {}, }; - if (fileType === "md") { - return parseMarkdownEntry(content, contentKey).map((data) => ({ - ...partial, - data, - })); - } - - if (fileType === "yaml") { - return parseYAMLEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - if (fileType === "json") { - return parseJSONEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - if (fileType === "toml") { - return parseTOMLEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - throw new Error(`Unsupported file type: ${fileType}`); + return entries.map((data) => ({ + ...partial, + data, + })); } async function loadFromFileSource( @@ -152,23 +82,6 @@ function getFileExtensionFromURL(url: string): string | null { return extension === filename ? null : extension; } -function mimeTypeToFileType(mimeType: string): string | null { - switch (mimeType) { - case "text/markdown": - return "md"; - case "text/yaml": - return "yaml"; - case "application/x-yaml": - return "yaml"; - case "application/json": - return "json"; - case "application/toml": - return "toml"; - default: - return null; - } -} - async function loadFromURLSource( source: MuseSource, options: SourceOptions, @@ -182,10 +95,18 @@ async function loadFromURLSource( const content = await response.text(); const mimeType = response.headers.get("Content-Type") || "unknown"; - const parseType = - mimeTypeToFileType(mimeType) ?? - getFileExtensionFromURL(source.location) ?? - "unknown"; + const parseType = getFileExtensionFromURL(source.location) ?? "unknown"; + + const loadedFile: LoadedFile = { + content, + filePath: source.location, + fileType: parseType, + dirname: "", + basename: "", + mimeType, + }; + + const entries = autoParseEntry(loadedFile, contentKey); const partial = { _raw: content, @@ -197,46 +118,10 @@ async function loadFromURLSource( meta: {}, }; - if (parseType === "md") { - return parseMarkdownEntry(content, contentKey).map((data) => ({ - ...partial, - data, - })); - } - - if (parseType === "yaml") { - return parseYAMLEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - if (parseType === "json") { - return parseJSONEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - if (parseType === "toml") { - return parseTOMLEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - // If it doesnt match one of these, try brute force parsing - // and return the result if any of them work - try { - return parseMarkdownEntry(content, contentKey).map((data) => ({ - ...partial, - data, - })); - } catch { - // noop - } - - throw new Error(`Unsupported MIME type from URL source: ${mimeType}`); + return entries.map((data) => ({ + ...partial, + data, + })); } export async function loadFromSource( diff --git a/src/core/types.ts b/src/core/types.ts deleted file mode 100644 index 35ba466..0000000 --- a/src/core/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type UnknownRecord = Record; diff --git a/src/muse.ts b/src/muse.ts index d2b61f9..814dc78 100644 --- a/src/muse.ts +++ b/src/muse.ts @@ -1,4 +1,4 @@ -import { loadBinding, type Binding } from "#core/binding"; +import type { Binding } from "#core/binding"; export class Muse { bindingLoaded = false; From dfc65dacfaa4b148f90b247bfa62c12d758cbf8c Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Fri, 4 Apr 2025 14:07:59 -0400 Subject: [PATCH 09/10] Further cleanup --- src/core/binding.ts | 31 ++----------------------------- src/core/sources.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/core/binding.ts b/src/core/binding.ts index d0528b5..1a768e5 100644 --- a/src/core/binding.ts +++ b/src/core/binding.ts @@ -7,21 +7,12 @@ import { type MusePlugin, loadPlugin } from "./plugins"; import { type MuseEntry, type MuseSource, + SourceSchema, loadFromSource, parseMuseFile, + sourceShorthandToSource, } from "./sources"; -const SourceSchema = z.union([ - z.object({ - file: z.string(), - }), - z.object({ - url: z.string().url(), - }), -]); - -type SourceShorthand = z.infer; - // Function to parse an unknown object into parts of a binding const BindingSchema = z.object({ contentKey: z.string().default("content"), @@ -65,24 +56,6 @@ export interface Binding { readonly options: UnknownRecord; } -function sourceShorthandToSource(shorthand: SourceShorthand): MuseSource { - if ("file" in shorthand) { - return { - location: shorthand.file, - type: "file", - }; - } - - if ("url" in shorthand) { - return { - location: shorthand.url, - type: "url", - }; - } - - throw new MuseError("Invalid source shorthand"); -} - /** * Given a path, find the binding file * If the path is to a directory, check for a binding inside diff --git a/src/core/sources.ts b/src/core/sources.ts index 0ab1495..5c38e4b 100644 --- a/src/core/sources.ts +++ b/src/core/sources.ts @@ -1,6 +1,39 @@ import { Glob } from "bun"; import { type LoadedFile, loadFileContent } from "./files"; import { type UnknownRecord, autoParseEntry } from "./records"; +import { z } from "zod"; +import { MuseError } from "#errors"; + +export const SourceSchema = z.union([ + z.object({ + file: z.string(), + }), + z.object({ + url: z.string().url(), + }), +]); + +export type SourceShorthand = z.infer; + +export function sourceShorthandToSource( + shorthand: SourceShorthand, +): MuseSource { + if ("file" in shorthand) { + return { + location: shorthand.file, + type: "file", + }; + } + + if ("url" in shorthand) { + return { + location: shorthand.url, + type: "url", + }; + } + + throw new MuseError("Invalid source shorthand"); +} export type SourceType = "file" | "url"; From 2d3ba356b5052eb7df0d766fd7b534f3f283113c Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Fri, 4 Apr 2025 16:14:17 -0400 Subject: [PATCH 10/10] Update plugin shape again --- README.md | 61 ++++++++++++++++++++++++++++++++------------- package.json | 2 +- src/core/binding.ts | 8 ++---- src/exports.ts | 7 ++++-- src/muse.ts | 20 --------------- 5 files changed, 52 insertions(+), 46 deletions(-) delete mode 100644 src/muse.ts diff --git a/README.md b/README.md index df86874..d122169 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,17 @@ # Muse -A CLI for wrangling directories of source files. - -Like a static generator, but for whatever. +_Bind data into anything_ ## Overview -**Muse** is a CLI and toolchain for operating on directories of -json, yaml, toml, and markdown files. Each file can specify any shape -of data. Muse scans included files, parses them into data, and streams -the loaded data through processors and plugins. +**Muse** is a CLI and toolchain for operating on data collected from +json, yaml, toml, and markdown files. -Each processor can modify the data at compile time, as well as enact -side effects such as writing files to disk. +Muse scans included files, parses them into data, and streams +the loaded data through plugins. -Muse does not edit source files, it only reads them in. +Each plugin can modify the data as well as enact side effects +such as writing files to disk. ## Usage @@ -32,10 +29,9 @@ Options: Each Muse project should specify a `binding` file with the following shape: ```yaml -include: - - ../another-project # Optional -files: # Optional - - "**/*.yaml" +sources: # Optional + - file: "**/*.yaml" + - web: "https://example.com/data.json" contentKey: "content" # Optional options: # Optional someOption: someValue @@ -44,9 +40,9 @@ processors: - second-processor ``` -The `binding` file can be any of the supported file types for Muse. If -the CLI is invoked with a directory instead of a file, Muse will look -for a binding file in the directory with the following order: +The `binding` file can be any of the supported file types for Muse and can +be named anything. If the CLI is invoked with a directory instead of a file, +Muse will look for a binding file in the directory with the following order: 1. `binding.json` 2. `binding.yaml` @@ -68,3 +64,34 @@ Your markdown content here When loading markdown files, Muse will load the content of the file into a key called `content` by default. This can be changed in the binding configuration by setting a `contentKey` value as an override. + +## Plugin API + +Muse plugins are written in TypeScript/JavaScript and expose information +describing the plugin, as well as the functions to operate with: + +```typescript +export const name = "Plugin Name"; +export const description = "Plugin Description"; +export async function step(binding: Binding): Promise {} +``` + +The main function of a plugin is `step`, which takes a `Binding` object +and returns a modified `Binding` object. + +When returning a new Binding object, it is best practice to make immutable +changes: + +```typescript +export async function step(binding: Binding): Promise { + const newBinding = { ...binding }; + newBinding.options.someOption = "newValue"; + return { + ...binding, + meta: { + ...binding.meta, + customValue: true, + } + }; +} +``` diff --git a/package.json b/package.json index 36bf54a..e7e1428 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@endeavorance/muse", - "version": "0.2.0", + "version": "0.3.0", "module": "dist/index.js", "exports": { ".": { diff --git a/src/core/binding.ts b/src/core/binding.ts index 1a768e5..7652c0a 100644 --- a/src/core/binding.ts +++ b/src/core/binding.ts @@ -22,7 +22,7 @@ const BindingSchema = z.object({ }, ]), options: z.record(z.string(), z.any()).default({}), - processors: z.array(z.string()).default([]), + plugins: z.array(z.string()).default([]), }); /** @@ -31,9 +31,6 @@ const BindingSchema = z.object({ * 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; - /** Information about the sources used to load entries */ readonly sources: MuseSource[]; @@ -133,11 +130,10 @@ export async function loadBinding(initialPath: string): Promise { // Load and check plugins const plugins: MusePlugin[] = await Promise.all( - parsedBinding.processors.map(loadPlugin.bind(null, bindingDirname)), + parsedBinding.plugins.map(loadPlugin.bind(null, bindingDirname)), ); return { - _raw: parsedBinding, sources, bindingPath, contentKey: parsedBinding.contentKey, diff --git a/src/exports.ts b/src/exports.ts index f520a46..451c4f8 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -5,5 +5,8 @@ export * from "#errors"; export const DEFAULT_CONTENT_KEY = "content"; // Types -export type * from "#types"; -export type { MusePlugin } from "#core/plugins"; +export type * from "#core/binding"; +export type * from "#core/plugins"; +export type * from "#core/sources"; +export type * from "#core/records"; +export type * from "#core/files"; diff --git a/src/muse.ts b/src/muse.ts deleted file mode 100644 index 814dc78..0000000 --- a/src/muse.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Binding } from "#core/binding"; - -export class Muse { - bindingLoaded = false; - preliminaryBinding: Partial = {}; - additionalSources: string[] = []; - additionalPluginPaths: string[] = []; - - constructor(bindingConfig: Partial) { - this.preliminaryBinding = bindingConfig; - } - - source(newSource: string) { - this.additionalSources.push(newSource); - } - - plugin(pluginPath: string) { - this.additionalPluginPaths.push(pluginPath); - } -}