Update plugin shape
This commit is contained in:
parent
504c1e6f3c
commit
ae1b9e262e
12 changed files with 181 additions and 149 deletions
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
||||
async function processBinding(
|
||||
inputFilePath: string,
|
||||
flags: CLIFlags,
|
||||
{ log, verbose }: Loggers,
|
||||
) {
|
||||
// Load the binding
|
||||
const 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 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;
|
||||
|
||||
function getStepLogLine(plugin: MusePlugin): string {
|
||||
const { name, description } = plugin;
|
||||
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);
|
||||
return processorLogLine;
|
||||
}
|
||||
|
||||
async function processBinding(
|
||||
inputFilePath: string,
|
||||
flags: CLIFlags,
|
||||
{ log, verbose }: Loggers,
|
||||
) {
|
||||
// Load the binding
|
||||
let binding = await loadBinding(inputFilePath);
|
||||
verbose(`Binding ${binding.bindingPath}`);
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
|
|
@ -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<MetaShape = UnknownRecord> {
|
|||
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<string> {
|
|||
}
|
||||
}
|
||||
|
||||
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<Binding> {
|
|||
)
|
||||
).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<Binding> {
|
|||
contentKey: parsedBinding.contentKey,
|
||||
entries,
|
||||
options: parsedBinding.options,
|
||||
processors: processors,
|
||||
plugins,
|
||||
meta: {},
|
||||
};
|
||||
}
|
||||
|
|
32
src/core/files.ts
Normal file
32
src/core/files.ts
Normal file
|
@ -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<LoadedFile> {
|
||||
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),
|
||||
};
|
||||
}
|
|
@ -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<MetaShape = UnknownRecord> {
|
||||
_raw: string;
|
||||
|
@ -12,6 +11,20 @@ export interface MuseEntry<MetaShape = UnknownRecord> {
|
|||
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<MuseEntry[]> {
|
||||
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 [
|
||||
{
|
||||
return parseMarkdownEntry(content, contentKey).map((data) => ({
|
||||
...partial,
|
||||
data: parsed,
|
||||
},
|
||||
];
|
||||
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,
|
||||
}));
|
||||
|
|
52
src/core/plugins.ts
Normal file
52
src/core/plugins.ts
Normal file
|
@ -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<Binding>;
|
||||
|
||||
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<MusePlugin> {
|
||||
// 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"),
|
||||
};
|
||||
}
|
|
@ -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<string, unknown>,
|
||||
) => Promise<Binding>;
|
||||
|
||||
export interface MuseProcessor {
|
||||
readonly filePath: string;
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly process: MuseProcessorFn;
|
||||
}
|
||||
|
||||
export async function loadProcessor(
|
||||
bindingDirname: string,
|
||||
processorPath: string,
|
||||
): Promise<MuseProcessor> {
|
||||
// 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,
|
||||
};
|
||||
}
|
|
@ -1,3 +1 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export type UnknownRecord = Record<string, unknown>;
|
|
@ -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.";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -33,9 +33,6 @@
|
|||
"#util": [
|
||||
"./util.ts"
|
||||
],
|
||||
"#types": [
|
||||
"./types.ts"
|
||||
],
|
||||
"#errors": [
|
||||
"./errors.ts"
|
||||
]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue