From b1aba5825de68284e9a1e4462b868eb98aa05b7f Mon Sep 17 00:00:00 2001 From: gesslar <1266935+gesslar@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:59:21 -0500 Subject: [PATCH] refactor: add flat-hierarchy src modules with actioneer pipeline Co-Authored-By: Claude Sonnet 4.6 --- dist/schema/bedoc.action.json | 42 ++ dist/types/Action.d.ts | 3 + dist/types/Action.d.ts.map | 1 + dist/types/BeDoc.d.ts | 208 ++++++++++ dist/types/BeDoc.d.ts.map | 1 + dist/types/Configuration.d.ts | 11 + dist/types/Configuration.d.ts.map | 1 + dist/types/ConfigurationParameters.d.ts | 3 + dist/types/ConfigurationParameters.d.ts.map | 1 + dist/types/Conveyor.d.ts | 27 ++ dist/types/Conveyor.d.ts.map | 1 + dist/types/Discovery.d.ts | 215 ++++++++++ dist/types/Discovery.d.ts.map | 1 + dist/types/Environment.d.ts | 3 + dist/types/Environment.d.ts.map | 1 + dist/types/Logger.d.ts | 47 +++ dist/types/Logger.d.ts.map | 1 + dist/types/Schema.d.ts | 3 + dist/types/Schema.d.ts.map | 1 + dist/types/cli.d.ts | 4 +- dist/types/cli.d.ts.map | 11 +- src/Action.js | 9 + src/BeDoc.js | 273 +++++++++++++ src/Configuration.js | 388 ++++++++++++++++++ src/ConfigurationParameters.js | 151 +++++++ src/Conveyor.js | 215 ++++++++++ src/Discovery.js | 425 ++++++++++++++++++++ src/Environment.js | 8 + src/Logger.js | 182 +++++++++ src/Schema.js | 6 + src/cli.js | 64 +-- tsconfig.types.json | 42 ++ 32 files changed, 2312 insertions(+), 37 deletions(-) create mode 100644 dist/schema/bedoc.action.json create mode 100644 dist/types/Action.d.ts create mode 100644 dist/types/Action.d.ts.map create mode 100644 dist/types/BeDoc.d.ts create mode 100644 dist/types/BeDoc.d.ts.map create mode 100644 dist/types/Configuration.d.ts create mode 100644 dist/types/Configuration.d.ts.map create mode 100644 dist/types/ConfigurationParameters.d.ts create mode 100644 dist/types/ConfigurationParameters.d.ts.map create mode 100644 dist/types/Conveyor.d.ts create mode 100644 dist/types/Conveyor.d.ts.map create mode 100644 dist/types/Discovery.d.ts create mode 100644 dist/types/Discovery.d.ts.map create mode 100644 dist/types/Environment.d.ts create mode 100644 dist/types/Environment.d.ts.map create mode 100644 dist/types/Logger.d.ts create mode 100644 dist/types/Logger.d.ts.map create mode 100644 dist/types/Schema.d.ts create mode 100644 dist/types/Schema.d.ts.map create mode 100644 src/Action.js create mode 100644 src/BeDoc.js create mode 100644 src/Configuration.js create mode 100644 src/ConfigurationParameters.js create mode 100644 src/Conveyor.js create mode 100644 src/Discovery.js create mode 100644 src/Environment.js create mode 100644 src/Logger.js create mode 100644 src/Schema.js create mode 100644 tsconfig.types.json diff --git a/dist/schema/bedoc.action.json b/dist/schema/bedoc.action.json new file mode 100644 index 0000000..3752862 --- /dev/null +++ b/dist/schema/bedoc.action.json @@ -0,0 +1,42 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://schema.gesslar.dev/v1/bedoc-action.json", + "title": "BeDoc Action Schema", + "type": "object", + "properties": { + "accepts": { + "type": "object", + "properties": { + "type": { + "const": "object" + } + }, + "required": [ + "type" + ] + }, + "provides": { + "type": "object", + "properties": { + "type": { + "const": "object" + } + }, + "required": [ + "type" + ] + } + }, + "oneOf": [ + { + "required": [ + "accepts" + ] + }, + { + "required": [ + "provides" + ] + } + ] +} diff --git a/dist/types/Action.d.ts b/dist/types/Action.d.ts new file mode 100644 index 0000000..3137c71 --- /dev/null +++ b/dist/types/Action.d.ts @@ -0,0 +1,3 @@ +declare const _default: object; +export default _default; +//# sourceMappingURL=Action.d.ts.map \ No newline at end of file diff --git a/dist/types/Action.d.ts.map b/dist/types/Action.d.ts.map new file mode 100644 index 0000000..2db4b08 --- /dev/null +++ b/dist/types/Action.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"Action.d.ts","sourceRoot":"","sources":["../../src/Action.js"],"names":[],"mappings":""} \ No newline at end of file diff --git a/dist/types/BeDoc.d.ts b/dist/types/BeDoc.d.ts new file mode 100644 index 0000000..ece7027 --- /dev/null +++ b/dist/types/BeDoc.d.ts @@ -0,0 +1,208 @@ +/** + * @import {Glog} from "@gesslar/toolkit" + */ +export default class BeDoc { + /** + * Create a new instance of Core. + * + * @param {object} args + * @param {object} args.options - The options passed into BeDoc + * @param {string} args.source - The environment BeDoc is running in + * @param {Glog} args.glog - The Glog logger instance + * @returns {Promise} A new instance of Core + */ + static "new"({ options, source, glog, validateBeDocSchema }: { + options: object; + source: string; + glog: { + new (options?: { + name?: string; + debugLevel?: number; + logLevel?: number; + prefix?: string; + colours?: object; + symbols?: object; + stackTrace?: boolean; + tagsAsStrings?: boolean; + displayName?: boolean; + env?: string; + }): { + setOptions(options: { + name?: string; + debugLevel?: number; + logLevel?: number; + prefix?: string; + colours?: object; + symbols?: object; + stackTrace?: boolean; + tagsAsStrings?: boolean; + displayName?: boolean; + }): /*elided*/ any; + withName(name: string): /*elided*/ any; + withLogLevel(level: number): /*elided*/ any; + withPrefix(prefix: string): /*elided*/ any; + withColours(colours?: object): /*elided*/ any; + withStackTrace(enabled?: boolean): /*elided*/ any; + withTagsAsStrings(enabled?: boolean): /*elided*/ any; + withSymbols(symbols?: object): /*elided*/ any; + noDisplayName(): /*elided*/ any; + use(prefix: string): object; + get name(): string; + get debugLevel(): number; + get options(): object; + newDebug(tag: string): Function; + debug(message: string, level?: number, ...arg: unknown[]): void; + info(message: string, ...arg: unknown[]): void; + warn(message: string, ...arg: unknown[]): void; + error(message: string, ...arg: unknown[]): void; + execute(...args: unknown[]): void; + colourize(strings: Array, ...values: unknown[]): void; + success(message: string, ...args: unknown[]): void; + group(...args: unknown[]): void; + groupEnd(): void; + groupDebug(message: string, level?: number): void; + groupInfo(message: string): void; + groupSuccess(message: string): void; + table(data: object | any[], labelOrOptions?: string | object, options?: { + properties?: Array; + showHeader?: boolean; + quotedStrings?: boolean; + }): void; + get colours(): any; + get raw(): object; + "__#private@#private": any; + }; + logLevel: number; + logPrefix: string; + colours: any; + stackTrace: boolean; + name: string; + tagsAsStrings: boolean; + symbols: any; + setLogPrefix(prefix: string): /*elided*/ any; + setLogLevel(level: number): /*elided*/ any; + withName(name: string): /*elided*/ any; + withColours(colours?: object): /*elided*/ any; + withStackTrace(enabled?: boolean): /*elided*/ any; + withTagsAsStrings(enabled?: boolean): /*elided*/ any; + withSymbols(symbols?: object): /*elided*/ any; + use(prefix: string): object; + create(options?: object): { + setOptions(options: { + name?: string; + debugLevel?: number; + logLevel?: number; + prefix?: string; + colours?: object; + symbols?: object; + stackTrace?: boolean; + tagsAsStrings?: boolean; + displayName?: boolean; + }): /*elided*/ any; + withName(name: string): /*elided*/ any; + withLogLevel(level: number): /*elided*/ any; + withPrefix(prefix: string): /*elided*/ any; + withColours(colours?: object): /*elided*/ any; + withStackTrace(enabled?: boolean): /*elided*/ any; + withTagsAsStrings(enabled?: boolean): /*elided*/ any; + withSymbols(symbols?: object): /*elided*/ any; + noDisplayName(): /*elided*/ any; + use(prefix: string): object; + get name(): string; + get debugLevel(): number; + get options(): object; + newDebug(tag: string): Function; + debug(message: string, level?: number, ...arg: unknown[]): void; + info(message: string, ...arg: unknown[]): void; + warn(message: string, ...arg: unknown[]): void; + error(message: string, ...arg: unknown[]): void; + execute(...args: unknown[]): void; + colourize(strings: Array, ...values: unknown[]): void; + success(message: string, ...args: unknown[]): void; + group(...args: unknown[]): void; + groupEnd(): void; + groupDebug(message: string, level?: number): void; + groupInfo(message: string): void; + groupSuccess(message: string): void; + table(data: object | any[], labelOrOptions?: string | object, options?: { + properties?: Array; + showHeader?: boolean; + quotedStrings?: boolean; + }): void; + get colours(): any; + get raw(): object; + "__#private@#private": any; + }; + execute(...args: unknown[]): void; + colourize(strings: Array, ...values: unknown[]): void; + success(message: string, ...args: unknown[]): void; + group(...args: unknown[]): void; + groupEnd(): void; + groupDebug(message: string, level?: number): void; + groupInfo(message: string): void; + groupSuccess(message: string): void; + table(data: object | any[], labelOrOptions?: string | object, options?: { + properties?: Array; + showHeader?: boolean; + quotedStrings?: boolean; + }): void; + setAlias(alias: string, colourCode: string): { + setOptions(options: { + name?: string; + debugLevel?: number; + logLevel?: number; + prefix?: string; + colours?: object; + symbols?: object; + stackTrace?: boolean; + tagsAsStrings?: boolean; + displayName?: boolean; + }): /*elided*/ any; + withName(name: string): /*elided*/ any; + withLogLevel(level: number): /*elided*/ any; + withPrefix(prefix: string): /*elided*/ any; + withColours(colours?: object): /*elided*/ any; + withStackTrace(enabled?: boolean): /*elided*/ any; + withTagsAsStrings(enabled?: boolean): /*elided*/ any; + withSymbols(symbols?: object): /*elided*/ any; + noDisplayName(): /*elided*/ any; + use(prefix: string): object; + get name(): string; + get debugLevel(): number; + get options(): object; + newDebug(tag: string): Function; + debug(message: string, level?: number, ...arg: unknown[]): void; + info(message: string, ...arg: unknown[]): void; + warn(message: string, ...arg: unknown[]): void; + error(message: string, ...arg: unknown[]): void; + execute(...args: unknown[]): void; + colourize(strings: Array, ...values: unknown[]): void; + success(message: string, ...args: unknown[]): void; + group(...args: unknown[]): void; + groupEnd(): void; + groupDebug(message: string, level?: number): void; + groupInfo(message: string): void; + groupSuccess(message: string): void; + table(data: object | any[], labelOrOptions?: string | object, options?: { + properties?: Array; + showHeader?: boolean; + quotedStrings?: boolean; + }): void; + get colours(): any; + get raw(): object; + "__#private@#private": any; + }; + get raw(): object; + }; + }): Promise; + constructor(glog: any); + processFiles(): Promise<{ + totalFiles: any; + succeeded: any; + warned: any; + errored: any; + duration: string; + }>; + #private; +} +//# sourceMappingURL=BeDoc.d.ts.map \ No newline at end of file diff --git a/dist/types/BeDoc.d.ts.map b/dist/types/BeDoc.d.ts.map new file mode 100644 index 0000000..8b0177a --- /dev/null +++ b/dist/types/BeDoc.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"BeDoc.d.ts","sourceRoot":"","sources":["../../src/BeDoc.js"],"names":[],"mappings":"AAQA;;GAEG;AAEH;IAcE;;;;;;;;OAQG;IACH,6DALG;QAAqB,OAAO,EAApB,MAAM;QACO,MAAM,EAAnB,MAAM;QACK,IAAI;;oBAsLk/G,CAAC;0BAA4B,CAAC;wBAA0B,CAAC;sBAAwB,CAAC;uBAAyB,CAAC;uBAAyB,CAAC;0BAA4B,CAAC;6BAAgC,CAAC;2BAA8B,CAAC;mBAAsB,CAAC;;;wBAAg+B,CAAC;8BAA4B,CAAC;4BAA0B,CAAC;0BAAwB,CAAC;2BAAyB,CAAC;2BAAyB,CAAC;8BAA4B,CAAC;iCAAgC,CAAC;+BAA8B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BAA+0O,CAAC;8BAAmC,CAAC;iCAAgC,CAAC;;;;;;;;;;;;;;;;;;;;;;;wBAAxnP,CAAC;8BAA4B,CAAC;4BAA0B,CAAC;0BAAwB,CAAC;2BAAyB,CAAC;2BAAyB,CAAC;8BAA4B,CAAC;iCAAgC,CAAC;+BAA8B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BAA+0O,CAAC;8BAAmC,CAAC;iCAAgC,CAAC;;;;;;;;;;;;;;;0BAAxgW,CAAC;0BAAmC,CAAC;6BAAgC,CAAC;;;;wBAA00G,CAAC;8BAA4B,CAAC;4BAA0B,CAAC;0BAAwB,CAAC;2BAAyB,CAAC;2BAAyB,CAAC;8BAA4B,CAAC;iCAAgC,CAAC;+BAA8B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BAA+0O,CAAC;8BAAmC,CAAC;iCAAgC,CAAC;;;;;;;;KArL31Y,GAAU,OAAO,CAAC,KAAK,CAAC,CAoB1B;IA/BD,uBAEC;IAyJD;;;;;;OAmCC;;CACF"} \ No newline at end of file diff --git a/dist/types/Configuration.d.ts b/dist/types/Configuration.d.ts new file mode 100644 index 0000000..69f13e9 --- /dev/null +++ b/dist/types/Configuration.d.ts @@ -0,0 +1,11 @@ +export default class Configuration { + validate({ options, source }: { + options: any; + source: any; + }): Promise<{ + status: string; + validated: boolean; + }>; + #private; +} +//# sourceMappingURL=Configuration.d.ts.map \ No newline at end of file diff --git a/dist/types/Configuration.d.ts.map b/dist/types/Configuration.d.ts.map new file mode 100644 index 0000000..c798264 --- /dev/null +++ b/dist/types/Configuration.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"Configuration.d.ts","sourceRoot":"","sources":["../../src/Configuration.js"],"names":[],"mappings":"AAUA;IACE;;;;;;OAwIC;;CAgPF"} \ No newline at end of file diff --git a/dist/types/ConfigurationParameters.d.ts b/dist/types/ConfigurationParameters.d.ts new file mode 100644 index 0000000..2c9fab4 --- /dev/null +++ b/dist/types/ConfigurationParameters.d.ts @@ -0,0 +1,3 @@ +export const ConfigurationParameters: object; +export const ConfigurationPriorityKeys: object; +//# sourceMappingURL=ConfigurationParameters.d.ts.map \ No newline at end of file diff --git a/dist/types/ConfigurationParameters.d.ts.map b/dist/types/ConfigurationParameters.d.ts.map new file mode 100644 index 0000000..246f152 --- /dev/null +++ b/dist/types/ConfigurationParameters.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"ConfigurationParameters.d.ts","sourceRoot":"","sources":["../../src/ConfigurationParameters.js"],"names":[],"mappings":"AAEA,6CA6IE;AAEF,+CAA6E"} \ No newline at end of file diff --git a/dist/types/Conveyor.d.ts b/dist/types/Conveyor.d.ts new file mode 100644 index 0000000..c50c7e3 --- /dev/null +++ b/dist/types/Conveyor.d.ts @@ -0,0 +1,27 @@ +export default class Conveyor { + constructor({ parse, print, glog, contract, output }: { + parse: any; + print: any; + glog: any; + contract: any; + output: any; + }); + /** + * Defines the per-file processing pipeline. + * + * @param {ActionBuilder} builder - The Actioneer builder instance. + */ + setup(builder: ActionBuilder): void; + /** + * Processes files through the parse→print pipeline with concurrency. + * + * @param {Array} files - List of files to process. + * @param {number} maxConcurrent - Maximum number of concurrent tasks. + * @returns {Promise} - Resolves with {succeeded, errored, warned}. + */ + convey(files: Array, maxConcurrent?: number): Promise; + #private; +} +import { ActionBuilder } from "@gesslar/actioneer"; +import { FileObject } from "@gesslar/toolkit"; +//# sourceMappingURL=Conveyor.d.ts.map \ No newline at end of file diff --git a/dist/types/Conveyor.d.ts.map b/dist/types/Conveyor.d.ts.map new file mode 100644 index 0000000..9aa4d45 --- /dev/null +++ b/dist/types/Conveyor.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"Conveyor.d.ts","sourceRoot":"","sources":["../../src/Conveyor.js"],"names":[],"mappings":"AAKA;IAOE;;;;;;OAMC;IAED;;;;OAIG;IACH,eAFW,aAAa,QASvB;IAED;;;;;;OAMG;IACH,cAJW,KAAK,CAAC,UAAU,CAAC,kBACjB,MAAM,GACJ,OAAO,CAAC,MAAM,CAAC,CAS3B;;CAsGF;8BAtJmD,oBAAoB;2BACzC,kBAAkB"} \ No newline at end of file diff --git a/dist/types/Discovery.d.ts b/dist/types/Discovery.d.ts new file mode 100644 index 0000000..554a1c5 --- /dev/null +++ b/dist/types/Discovery.d.ts @@ -0,0 +1,215 @@ +/** + * @import {Glog} from "@gesslar/toolkit" + */ +export default class Discovery { + /** + * Constructor for Discovery. + * + * @param {object} arg - Constructor argument + * @param {object} arg.options - BeDoc options + * @param {Glog} arg.glog - Glog instance + */ + constructor({ options, glog }: { + options: object; + glog: { + new (options?: { + name?: string; + debugLevel?: number; + logLevel?: number; + prefix?: string; + colours?: object; + symbols?: object; + stackTrace?: boolean; + tagsAsStrings?: boolean; + displayName?: boolean; + env?: string; + }): { + setOptions(options: { + name?: string; + debugLevel?: number; + logLevel?: number; + prefix?: string; + colours?: object; + symbols?: object; + stackTrace?: boolean; + tagsAsStrings?: boolean; + displayName?: boolean; + }): /*elided*/ any; + withName(name: string): /*elided*/ any; + withLogLevel(level: number): /*elided*/ any; + withPrefix(prefix: string): /*elided*/ any; + withColours(colours?: object): /*elided*/ any; + withStackTrace(enabled?: boolean): /*elided*/ any; + withTagsAsStrings(enabled?: boolean): /*elided*/ any; + withSymbols(symbols?: object): /*elided*/ any; + noDisplayName(): /*elided*/ any; + use(prefix: string): object; + get name(): string; + get debugLevel(): number; + get options(): object; + newDebug(tag: string): Function; + debug(message: string, level?: number, ...arg: unknown[]): void; + info(message: string, ...arg: unknown[]): void; + warn(message: string, ...arg: unknown[]): void; + error(message: string, ...arg: unknown[]): void; + execute(...args: unknown[]): void; + colourize(strings: Array, ...values: unknown[]): void; + success(message: string, ...args: unknown[]): void; + group(...args: unknown[]): void; + groupEnd(): void; + groupDebug(message: string, level?: number): void; + groupInfo(message: string): void; + groupSuccess(message: string): void; + table(data: object | any[], labelOrOptions?: string | object, options?: { + properties?: Array; + showHeader?: boolean; + quotedStrings?: boolean; + }): void; + get colours(): any; + get raw(): object; + "__#private@#private": any; + }; + logLevel: number; + logPrefix: string; + colours: any; + stackTrace: boolean; + name: string; + tagsAsStrings: boolean; + symbols: any; + setLogPrefix(prefix: string): /*elided*/ any; + setLogLevel(level: number): /*elided*/ any; + withName(name: string): /*elided*/ any; + withColours(colours?: object): /*elided*/ any; + withStackTrace(enabled?: boolean): /*elided*/ any; + withTagsAsStrings(enabled?: boolean): /*elided*/ any; + withSymbols(symbols?: object): /*elided*/ any; + use(prefix: string): object; + create(options?: object): { + setOptions(options: { + name?: string; + debugLevel?: number; + logLevel?: number; + prefix?: string; + colours?: object; + symbols?: object; + stackTrace?: boolean; + tagsAsStrings?: boolean; + displayName?: boolean; + }): /*elided*/ any; + withName(name: string): /*elided*/ any; + withLogLevel(level: number): /*elided*/ any; + withPrefix(prefix: string): /*elided*/ any; + withColours(colours?: object): /*elided*/ any; + withStackTrace(enabled?: boolean): /*elided*/ any; + withTagsAsStrings(enabled?: boolean): /*elided*/ any; + withSymbols(symbols?: object): /*elided*/ any; + noDisplayName(): /*elided*/ any; + use(prefix: string): object; + get name(): string; + get debugLevel(): number; + get options(): object; + newDebug(tag: string): Function; + debug(message: string, level?: number, ...arg: unknown[]): void; + info(message: string, ...arg: unknown[]): void; + warn(message: string, ...arg: unknown[]): void; + error(message: string, ...arg: unknown[]): void; + execute(...args: unknown[]): void; + colourize(strings: Array, ...values: unknown[]): void; + success(message: string, ...args: unknown[]): void; + group(...args: unknown[]): void; + groupEnd(): void; + groupDebug(message: string, level?: number): void; + groupInfo(message: string): void; + groupSuccess(message: string): void; + table(data: object | any[], labelOrOptions?: string | object, options?: { + properties?: Array; + showHeader?: boolean; + quotedStrings?: boolean; + }): void; + get colours(): any; + get raw(): object; + "__#private@#private": any; + }; + execute(...args: unknown[]): void; + colourize(strings: Array, ...values: unknown[]): void; + success(message: string, ...args: unknown[]): void; + group(...args: unknown[]): void; + groupEnd(): void; + groupDebug(message: string, level?: number): void; + groupInfo(message: string): void; + groupSuccess(message: string): void; + table(data: object | any[], labelOrOptions?: string | object, options?: { + properties?: Array; + showHeader?: boolean; + quotedStrings?: boolean; + }): void; + setAlias(alias: string, colourCode: string): { + setOptions(options: { + name?: string; + debugLevel?: number; + logLevel?: number; + prefix?: string; + colours?: object; + symbols?: object; + stackTrace?: boolean; + tagsAsStrings?: boolean; + displayName?: boolean; + }): /*elided*/ any; + withName(name: string): /*elided*/ any; + withLogLevel(level: number): /*elided*/ any; + withPrefix(prefix: string): /*elided*/ any; + withColours(colours?: object): /*elided*/ any; + withStackTrace(enabled?: boolean): /*elided*/ any; + withTagsAsStrings(enabled?: boolean): /*elided*/ any; + withSymbols(symbols?: object): /*elided*/ any; + noDisplayName(): /*elided*/ any; + use(prefix: string): object; + get name(): string; + get debugLevel(): number; + get options(): object; + newDebug(tag: string): Function; + debug(message: string, level?: number, ...arg: unknown[]): void; + info(message: string, ...arg: unknown[]): void; + warn(message: string, ...arg: unknown[]): void; + error(message: string, ...arg: unknown[]): void; + execute(...args: unknown[]): void; + colourize(strings: Array, ...values: unknown[]): void; + success(message: string, ...args: unknown[]): void; + group(...args: unknown[]): void; + groupEnd(): void; + groupDebug(message: string, level?: number): void; + groupInfo(message: string): void; + groupSuccess(message: string): void; + table(data: object | any[], labelOrOptions?: string | object, options?: { + properties?: Array; + showHeader?: boolean; + quotedStrings?: boolean; + }): void; + get colours(): any; + get raw(): object; + "__#private@#private": any; + }; + get raw(): object; + }; + }); + /** + * Discover actions from local or global node_modules + * + * @param {object} [specific] Configuration options for action discovery + * @param {FileObject} [specific.print] Print-related configuration options + * @param {FileObject} [specific.parse] Parse-related configuration options + * @param {Function} validateBeDocSchema - The validator function for BeDoc's action schema + * @returns {Promise} A map of discovered modules + */ + discoverActions(specific?: { + print?: FileObject; + parse?: FileObject; + }, validateBeDocSchema: Function): Promise; + satisfyCriteria(actions: any, validatedConfig: any): { + parse: any[]; + print: any[]; + }; + #private; +} +import { FileObject } from "@gesslar/toolkit"; +//# sourceMappingURL=Discovery.d.ts.map \ No newline at end of file diff --git a/dist/types/Discovery.d.ts.map b/dist/types/Discovery.d.ts.map new file mode 100644 index 0000000..74df665 --- /dev/null +++ b/dist/types/Discovery.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"Discovery.d.ts","sourceRoot":"","sources":["../../src/Discovery.js"],"names":[],"mappings":"AAOA;;GAEG;AAEH;IAME;;;;;;OAMG;IACH,+BAHG;QAAoB,OAAO,EAAnB,MAAM;QACI,IAAI;;oBAiQb,CAAC;0BACX,CAAA;wBAA0B,CAAC;sBACrB,CAAC;uBAAyB,CAAC;uBAC/B,CAAA;0BAA4B,CAAC;6BAG7B,CAAD;2BAA8B,CAAC;mBAE3B,CAAC;;;wBAuCN,CAAF;8BAEE,CAAF;4BAA0B,CAAC;0BAAwB,CAAC;2BAEpC,CAAC;2BACf,CAAJ;8BACM,CAAC;iCAAgC,CAAC;+BAA8B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BA6FizI,CAAC;8BAAmC,CAAC;iCAAgC,CAAC;;;;;;;;;;;;;;;;;;;;;;;wBAnG17I,CAAF;8BAEE,CAAF;4BAA0B,CAAC;0BAAwB,CAAC;2BAEpC,CAAC;2BACf,CAAJ;8BACM,CAAC;iCAAgC,CAAC;+BAA8B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BA6FizI,CAAC;8BAAmC,CAAC;iCAAgC,CAAC;;;;;;;;;;;;;;;0BA/Nl5I,CAAC;0BACjB,CAAC;6BAInB,CAAA;;;;wBAuHN,CAAF;8BAEE,CAAF;4BAA0B,CAAC;0BAAwB,CAAC;2BAEpC,CAAC;2BACf,CAAJ;8BACM,CAAC;iCAAgC,CAAC;+BAA8B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BA6FizI,CAAC;8BAAmC,CAAC;iCAAgC,CAAC;;;;;;;;KAlZ37I,EAIA;IAED;;;;;;;;OAQG;IACH,2BALG;QAA8B,KAAK,GAA3B,UAAU;QACY,KAAK,GAA3B,UAAU;KAClB,kCACU,OAAO,CAAC,MAAM,CAAC,CA4I3B;IAqJD;;;MAsDC;;CA6CF;2BAva2E,kBAAkB"} \ No newline at end of file diff --git a/dist/types/Environment.d.ts b/dist/types/Environment.d.ts new file mode 100644 index 0000000..bb6f66e --- /dev/null +++ b/dist/types/Environment.d.ts @@ -0,0 +1,3 @@ +declare const _default: object; +export default _default; +//# sourceMappingURL=Environment.d.ts.map \ No newline at end of file diff --git a/dist/types/Environment.d.ts.map b/dist/types/Environment.d.ts.map new file mode 100644 index 0000000..870818e --- /dev/null +++ b/dist/types/Environment.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"Environment.d.ts","sourceRoot":"","sources":["../../src/Environment.js"],"names":[],"mappings":""} \ No newline at end of file diff --git a/dist/types/Logger.d.ts b/dist/types/Logger.d.ts new file mode 100644 index 0000000..3d2f9bf --- /dev/null +++ b/dist/types/Logger.d.ts @@ -0,0 +1,47 @@ +export namespace loggerColours { + let debug: string[]; + let info: string; + let warn: string; + let error: string; + let reset: string; +} +/** + * Logger class + * + * Log levels: + * - debug: Debugging information + * - Debug levels + * - 0: No/critical debug information, not error level, but, should be + * logged + * - 1: Basic debug information, startup, shutdown, etc + * - 2: Intermediate debug information, discovery, starting to get more + * detailed + * - 3: Detailed debug information, parsing, processing, etc + * - 4: Very detailed debug information, nerd mode! + * - warn: Warning information + * - info: Informational information + * - error: Error information + */ +export default class Logger { + constructor(options: any); + vscodeError: any; + vscodeWarn: any; + vscodeInfo: any; + get name(): any; + get debugLevel(): number; + get options(): { + name: any; + debugLevel: number; + }; + setOptions(options: any): void; + lastStackLine(error?: Error, stepsRemoved?: number): ErrorStackParser.StackFrame; + extractFileFunction(level?: number): string; + newDebug(tag: any): any; + debug(message: any, level?: number, ...arg: any[]): void; + warn(message: any, ...arg: any[]): void; + info(message: any, ...arg: any[]): void; + error(message: any, ...arg: any[]): void; + #private; +} +import ErrorStackParser from "error-stack-parser"; +//# sourceMappingURL=Logger.d.ts.map \ No newline at end of file diff --git a/dist/types/Logger.d.ts.map b/dist/types/Logger.d.ts.map new file mode 100644 index 0000000..e6f60b2 --- /dev/null +++ b/dist/types/Logger.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"Logger.d.ts","sourceRoot":"","sources":["../../src/Logger.js"],"names":[],"mappings":";;;;;;;AA4CA;;;;;;;;;;;;;;;;GAgBG;AAEH;IAIE,0BAYC;IALK,iBAAiD;IACjD,gBAAkD;IAClD,gBAAsD;IAK5D,gBAEC;IAED,yBAEC;IAED;;;MAKC;IAED,+BAGC;IAWD,iFAIC;IAED,4CAsCC;IAED,wBAKC;IAED,yDAGC;IAED,wCAGC;IAED,wCAGC;IAED,yCAGC;;CACF;6BA5J4B,oBAAoB"} \ No newline at end of file diff --git a/dist/types/Schema.d.ts b/dist/types/Schema.d.ts new file mode 100644 index 0000000..ce65a42 --- /dev/null +++ b/dist/types/Schema.d.ts @@ -0,0 +1,3 @@ +declare const _default: object; +export default _default; +//# sourceMappingURL=Schema.d.ts.map \ No newline at end of file diff --git a/dist/types/Schema.d.ts.map b/dist/types/Schema.d.ts.map new file mode 100644 index 0000000..70e9623 --- /dev/null +++ b/dist/types/Schema.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"Schema.d.ts","sourceRoot":"","sources":["../../src/Schema.js"],"names":[],"mappings":""} \ No newline at end of file diff --git a/dist/types/cli.d.ts b/dist/types/cli.d.ts index dc4b876..faaadd5 100644 --- a/dist/types/cli.d.ts +++ b/dist/types/cli.d.ts @@ -1,3 +1,3 @@ #!/usr/bin/env node -export {} -//# sourceMappingURL=cli.d.ts.map +export {}; +//# sourceMappingURL=cli.d.ts.map \ No newline at end of file diff --git a/dist/types/cli.d.ts.map b/dist/types/cli.d.ts.map index b59dffd..b3f0b9b 100644 --- a/dist/types/cli.d.ts.map +++ b/dist/types/cli.d.ts.map @@ -1,10 +1 @@ -{ - "version": 3, - "file": "cli.d.ts", - "sourceRoot": "", - "sources": [ - "../../src/cli.js" - ], - "names": [], - "mappings": "" -} +{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.js"],"names":[],"mappings":""} \ No newline at end of file diff --git a/src/Action.js b/src/Action.js new file mode 100644 index 0000000..8e98f03 --- /dev/null +++ b/src/Action.js @@ -0,0 +1,9 @@ +import {Data} from "@gesslar/toolkit" + +export default Data.deepFreezeObject({ + actionTypes: ["parser", "formatter"], + actionMetaRequirements: { + parser: [{kind: "parser"}, "input"], + formatter: [{kind: "formatter"}, "format"], + }, +}) diff --git a/src/BeDoc.js b/src/BeDoc.js new file mode 100644 index 0000000..6ba5d61 --- /dev/null +++ b/src/BeDoc.js @@ -0,0 +1,273 @@ +import {Contract} from "@gesslar/negotiator" +import {Data, Sass, Tantrum} from "@gesslar/toolkit" +import {hrtime} from "node:process" + +import Configuration from "./Configuration.js" +import Conveyor from "./Conveyor.js" +import Discovery from "./Discovery.js" + +/** + * @import {FileObject, Glog} from "@gesslar/toolkit" + */ + +export default class BeDoc { + #glog + #options + #actionDefs + #validCrit + #validSchemas + #contract + #actions + #validateBeDocSchema + #hooks + #basePath + + constructor({basePath, glog}) { + this.#glog = glog + this.#basePath = basePath + } + + /** + * Create a new instance of Core. + * + * @param {object} args + * @param {object} args.options - The options passed into BeDoc + * @param {string} args.source - The environment BeDoc is running in + * @param {Glog} args.glog - The Glog logger instance + * @returns {Promise} A new instance of Core + */ + static async new({options, source, glog, validateBeDocSchema}) { + const {basePath} = options + + const bedoc = new this({basePath, glog}) + + await bedoc.#configure({options, source, glog, validateBeDocSchema}) + + const discovered = await bedoc.#discover() + + if(!discovered) { + return { + status: "fail", + message: "No matching actions specified or discovered.", + } + } + + return await (await bedoc.#negotiate()) + .#validateActions() + .#setupHooks() + } + + /** + * Validate configuration and store as options. + * + * @param {object} options - The raw options passed into BeDoc + * @param {string} source - The environment BeDoc is running in + */ + async #configure({options, source, glog, validateBeDocSchema}) { + this.#glog = glog + this.#validateBeDocSchema = validateBeDocSchema + + const config = new Configuration() + const validConfig = await config.validate({options, source}) + + if(validConfig.debug && validConfig.debugLevel > 0) + glog.withLogLevel(validConfig.debugLevel) + else + glog.withLogLevel(0) + + if(validConfig.status === "error") + throw Tantrum.new("BeDoc configuration failed", validConfig.errors) + + glog.debug("Creating new BeDoc instance with options: `%o`", 4, validConfig) + + this.#options = validConfig + } + + /** + * Discover and filter actions that match the configuration criteria. + * + * @returns {Promise} Whether matching actions were found + */ + async #discover() { + const glog = this.#glog + const options = this.#options + + const discovery = new Discovery({options, glog}) + + this.#actionDefs = await discovery.discoverActions({ + parser: options.parser, + formatter: options.formatter, + }, this.#validateBeDocSchema) + + this.#validCrit = discovery.satisfyCriteria(this.#actionDefs, options) + + glog.debug("Actions that met criteria %o", 4, this.#validCrit) + + return !Object.values(this.#validCrit).some(arr => arr.length === 0) + } + + /** + * Negotiate contracts between discovered parsers and formatters. + * + * @returns {Promise} This object for chaining. + */ + async #negotiate() { + const glog = this.#glog + const validSchemas = {parser: [], formatter: []} + + let formatters = this.#validCrit.formatter.length + + while(formatters--) { + const formatter = this.#validCrit.formatter[formatters] + const {terms: consumes} = formatter + const satisfied = [] + + for(const parser of this.#validCrit.parser) { + try { + const {terms: provides} = parser + + const contract = await Contract.negotiate(provides, consumes) + + satisfied.push({...parser, contract}) + } catch(err) { + glog.error(err) + + glog.debug("%o action incompatible with %o action", 3, + parser.action.default.meta.input, + formatter.action.default.meta.format + ) + } + } + + if(satisfied.length > 0) { + validSchemas.formatter.push(formatter) + validSchemas.parser.push(...satisfied) + } + } + + this.#validSchemas = validSchemas + + return this + } + + /** + * Validate that exactly one action per type was negotiated, then + * instantiate the action classes. + * + * @returns {BeDoc} This object for chaining. + */ + #validateActions() { + const glog = this.#glog + + const schemas = {} + + for(const [key, value] of Object.entries(this.#validSchemas)) { + if(value.length === 0) + throw Sass.new(`No matching '${key}' action found`) + + if(value.length > 1) + throw Sass.new(`Multiple matching '${key}' actions found`) + + schemas[key] = value[0] + } + + this.#validSchemas = schemas + this.#contract = schemas.parser.contract + + glog.debug("Contracts satisfied between parser and formatter", 2) + + const actions = {} + + for(const [, {action}] of Object.entries(this.#validSchemas)) { + const {kind} = action.default.meta + + glog.debug("Assigning %o action", 2, kind) + + actions[kind] = action.default + } + + this.#actions = actions + + return this + } + + async #setupHooks() { + /** @type {Glog} */ + const glog = this.#glog + + if(!this.#options.hooks) + return this + + /** @type {FileObject} */ + const hooksFile = this.#options.hooks + + if(!hooksFile) + return this + + if(!await hooksFile.exists) { + glog.warn(`File not found: ${hooksFile.path}`) + + return this + } + + const loaded = await hooksFile.import() + if(!loaded) + return this + + const hooks = {} + if(loaded.Parse && Data.isType(loaded.Parse, "Function")) + hooks.Parse = loaded.Parse + + if(loaded.Format && Data.isType(loaded.Format, "Function")) + hooks.Format = loaded.Format + + if(Data.isEmpty(hooks)) { + glog.warn(`No hooks found in ${hooksFile.path}`) + + return this + } + + this.#hooks = hooks + + return this + } + + async processFiles() { + const glog = this.#glog + + glog.debug("Starting file processing with conveyor", 1) + + const {input, output, maxConcurrent} = this.#options + + if(!input?.length) + throw Sass.new("No input files specified") + + const conveyor = new Conveyor({ + parser: this.#actions.parser, + formatter: this.#actions.formatter, + contract: this.#contract, + hooks: this.#hooks, + glog, + output, + basePath: this.#basePath + }) + + const processStart = hrtime.bigint() + const processResult = await conveyor.convey(input, maxConcurrent) + const processEnd = hrtime.bigint() + + glog.debug("Conveyor complete", 1) + + const result = { + totalFiles: input.length, + succeeded: processResult.succeeded, + warned: processResult.warned, + errored: processResult.errored, + duration: ((Number(processEnd - processStart)) / 1_000_000).toFixed(2), + } + + glog.debug("File processing complete", 1) + + return result + } +} diff --git a/src/Configuration.js b/src/Configuration.js new file mode 100644 index 0000000..87b91d5 --- /dev/null +++ b/src/Configuration.js @@ -0,0 +1,388 @@ +import {Collection, Data, DirectoryObject, FileObject, FileSystem as FS, Promised, Sass, Tantrum} from "@gesslar/toolkit" +import JSON5 from "json5" +import process from "node:process" + +import { + ConfigurationParameters, + ConfigurationPriorityKeys, +} from "./ConfigurationParameters.js" +import Environment from "./Environment.js" + +export default class Configuration { + async validate({options, source}) { + const {basePath: base} = options + const finalOptions = {} + + this.#mapEntryOptions({options, source}) + + // While the entry points do wrap the entire process in a try/catch, we + // should also do this here, so we can trap everything and instead + // of throwing, return friendly messages back! + + // If the configuration parameters are invalid, we can't proceed. No error + // collection is needed here, because the ConfigurationParameters object + // is a static object and should be correct. OUT WITH THE TRASH!!! (I mean + // the error collection, not the ConfigurationParameters object) + // (Edit: No, I mean the ConfigurationParameters object. It's trash. Fix it + // if you get this error.) + const configValidationErrors = this.#validateConfigurationParameters() + + if(configValidationErrors.length > 0) { + throw Tantrum.new( + `ConfigurationParameters validation errors`, + configValidationErrors.map(e => new Sass(e)) + ) + } + + const allOptions = await this.#findAllOptions(options) + + Object.assign(finalOptions, await this.#mergeOptions(allOptions)) + + this.#fixOptionValues(finalOptions) + + // Priority keys are those which must be processed first. They are + // specified in order of priority. + // Find them and add them to an array; the rest will be in pushed to the + // end of the priority array. + const orderedSections = [] + + ConfigurationPriorityKeys.forEach(key => { + if(!ConfigurationParameters[key]) + throw Sass.new(`Invalid priority key: ${key}`) + + if(finalOptions[key]) + orderedSections.push({key, value: finalOptions[key]}) + }) + + const remainingSections = Object.keys(ConfigurationParameters).filter( + key => !ConfigurationPriorityKeys.includes(key), + ) + + orderedSections.push( + ...remainingSections.map(key => { + return {key, value: finalOptions[key]} + }), + ) + + // Check exclusive options + for(const [key, param] of Object.entries(ConfigurationParameters)) { + if( + param.exclusiveOf && + finalOptions[key] && + finalOptions[param.exclusiveOf] + ) + throw new SyntaxError( + `Options \`${key}\` and \`${param.exclusiveOf}\` are mutually exclusive`, + ) + } + + // Check for mandatory values + for(const [key, {required}] of Object.entries(ConfigurationParameters)) { + if(required && !orderedSections.find(s => s.key === key)) + throw new SyntaxError(`Missing mandatory key \`${key}\``) + } + + for(const section of orderedSections) { + const {key} = section + + // Skipping config, we've already handled it + if(key === "config") + continue + + let {value} = section + const nothing = Data.isNothing(value) + const param = ConfigurationParameters[key] + const {required, path} = param + + if(nothing) { + if(required === true) + throw new SyntaxError(`Option \`${key}\` is required`) + else + continue + } + + // Additional path validation if needed + if(path && !nothing) { + const {mustExist, type: pathType} = path + + // Special for `input` and `exclude` because they can be a comma- + // separated list of glob patterns. + if(key === "input" || key === "exclude") { + if(Data.isType(value, "Array")) { + const settled = Promised.settle( + value.map(async pat => { + const {files} = await base.glob(pat) + + return files + }) + ) + + value = Promised.values(settled).flat() + } else if(Data.isType(value, "String")) { + const {files} = await base.glob(value) + value = files + } else { + throw new TypeError( + `Option \`${key}\` must be a string or an array of strings`, + ) + } + + finalOptions[key] = value + + continue + } else { + if(mustExist === true) { + finalOptions[key] = pathType === FS.fdType.FILE + ? new FileObject(value) + : new DirectoryObject(value) + } + } + } + } + + return { + status: "success", + validated: true, + ...finalOptions, + } + } + + #mapEntryOptions({options = {}, source}) { + // CLI already has done all the work via commander + if(source === Environment.CLI) + return options + + for(const [key, value] of Object.entries(options)) { + options[key] = {value, source} + } + + // We will need to inject some options if they are not available + const cwd = process.cwd() + const dir = new DirectoryObject(cwd) + + // Inject basePath if not available + if(!options.basePath) + options.basePath = {value: dir, source} + + // Add defaults which are missing + for(const [key, param] of Object.entries(ConfigurationParameters)) { + if(options[key] === undefined && param.default !== undefined) + options[key] = {value: param.default, source: "default"} + } + + return options + } + + /** + * Validate the ConfigurationParameters object. This is a sanity check to + * ensure that the ConfigurationParameters object is valid. + * + * @returns {string[]} Errors + */ + #validateConfigurationParameters() { + const errors = [] + + for(const [key, param] of Object.entries(ConfigurationParameters)) { + // Type + if(!param.type) { + errors.push(`Option \`${key}\` has no type`) + continue + } + + // Paths + if(param.subtype?.path) { + const pathType = param.subtype.path?.type + + // Check if pathType is defined + if(!pathType) + errors.push(`Option \`${key}\` has no path type`) + + // Check if pathType is a valid key in FdTypes + if(!FS.fdTypes.includes(pathType)) + errors.push(`Option \`${key}\` has invalid path type: ${pathType}`) + } + } + + return errors + } + + /** + * Find all options from all sources + * + * @param {object} entryOptions - The command line options. + * @returns {Promise} All options from all sources. + */ + async #findAllOptions(entryOptions) { + const {basePath} = entryOptions + const allOptions = [] + const environmentVariables = this.#getEnvironmentVariables() + + if(environmentVariables) + allOptions.push({source: "environment", options: environmentVariables}) + + const packageJson = entryOptions?.project + + if(packageJson) { + allOptions.push({source: "packageJson", options: packageJson}) + } else { + const packageJsonFile = basePath.getFile("package.json") + + if(await packageJsonFile.exists) { + const packageJson = await packageJsonFile.loadData() + + if(packageJson.bedoc) + allOptions.push({source: "packageJson", options: packageJson.bedoc}) + } + } + + // Then the config file, if the options specified a config file + const useConfig = + entryOptions?.config || + packageJson?.config || + environmentVariables?.config + + if(useConfig) { + const configFile = + packageJson?.config + ? new FileObject(packageJson.config) + : entryOptions.config?.value + ? new FileObject(entryOptions.config.value) + : null + + if(!configFile) + throw Sass.new("No config file specified") + + const configObject = await File.loadDataFile(configFile) + const subConfigName = + entryOptions?.sub || + packageJson?.sub || + environmentVariables?.sub + + // If we didn't specify a subconfiguration, let's just remove + // it so it doesn't pollute anything. + if(!subConfigName) + delete configObject.sub + + const finalConfig = subConfigName?.value + ? this.#resolveSubconfigs(configObject, subConfigName.value) + : configObject + + allOptions.push({source: "config", options: finalConfig}) + } + + allOptions.push({source: "entry", options: entryOptions}) + + return allOptions + } + + #resolveSubconfigs(configObject, subConfigName) { + const subConfig = configObject.sub?.find(sub => sub.name === subConfigName) + + if(!subConfig) + throw Sass.new(`No such subconfiguration \`${subConfigName}\``) + + // We don't need this anymore + delete subConfig.name + + for(const [key,val] of Object.entries(subConfig)) + configObject[key] = val + + return configObject + } + + /** + * Get environment variables + * + * @returns {object} Environment variables + */ + #getEnvironmentVariables() { + const environmentVariables = {} + const params = Object.keys(ConfigurationParameters).map(param => { + return { + param, + env: `bedoc_${param}`.toUpperCase(), + } + }) + + for(const param of params) { + if(process.env[param.env]) + environmentVariables[param.param] = process.env[param.env] + } + + return environmentVariables + } + + /** + * Merge all options into one object + * + * @param {object[]} allOptions - All options from all sources. + * @returns {Promise} The merged options. + */ + async #mergeOptions(allOptions) { + const entryIndex = allOptions.findIndex( + option => option.source && option.source === "entry", + ) + const entryOptions = allOptions[entryIndex].options + const nonEntryOptions = allOptions.filter( + option => option.source && option.source !== "entry", + ) + const optionsOnly = nonEntryOptions.map(option => option.options) + const mergedOptions = optionsOnly.reduce((acc, options) => { + for(const [key, value] of Object.entries(options)) + acc[key] = value + + return acc + }, {}) + + const mappedOptions = await Collection.mapObject(mergedOptions, + (option, value) => { + const {value: entryValue, source: entrySource} = + entryOptions[option] ?? { + value: undefined, + source: undefined, + } + + const entryDefaulted = entrySource === "default" + + if(entryValue && value !== entryValue) + return entryDefaulted ? value : entryValue + + return value + }) + + // Last, but not least, add any defaulted options that are not in the + // mapped options + for(const [key, value] of Object.entries(entryOptions ?? {})) { + if(!mappedOptions[key]) { + if(value.source) + mappedOptions[key] = value.value + } else { + if(value.source !== "default") + mappedOptions[key] = value.value + } + } + + return mappedOptions + } + + /** + * Fix option values. This operation is performed in place. + * + * @param {object} options - The options to fix. + */ + #fixOptionValues(options) { + for(const [key, param] of Object.entries(ConfigurationParameters)) { + // If the options passed includes this configuration parameter + if(options[key]) { + if(typeof options[key] === "string" && param.type !== "string") { + switch(param.type.toString()) { + case "boolean": + case "number": + options[key] = JSON5.parse(options[key]) + break + } + } + } + } + } +} diff --git a/src/ConfigurationParameters.js b/src/ConfigurationParameters.js new file mode 100644 index 0000000..3c50829 --- /dev/null +++ b/src/ConfigurationParameters.js @@ -0,0 +1,151 @@ +import {Data} from "@gesslar/toolkit" + +const ConfigurationParameters = Data.deepFreezeObject({ + input: { + short: "i", + param: "file", + description: "Comma-separated glob patterns to match files", + type: Data.newTypeSpec("string|string[]"), + required: true, + path: { + type: "file", + mustExist: true, + }, + }, + exclude: { + short: "x", + param: "file", + description: "Comma-separated glob patterns to exclude files", + type: Data.newTypeSpec("string|string[]"), + required: false, + }, + language: { + short: "l", + param: "lang", + description: "Language parser to use", + type: Data.newTypeSpec("string"), + required: false, + exclusiveOf: "parser", + }, + format: { + short: "f", + description: "Output format", + type: Data.newTypeSpec("string"), + required: false, + exclusiveOf: "formatter", + }, + maxConcurrent: { + short: "C", + param: "num", + description: "Maximum number of concurrent tasks", + type: Data.newTypeSpec("number"), + required: false, + default: 10, + }, + hooks: { + short: "k", + param: "file", + description: "Custom hooks JS file", + type: Data.newTypeSpec("string"), + required: false, + path: { + type: "file", + mustExist: true, + }, + }, + output: { + short: "o", + param: "dir", + description: "Output directory", + type: Data.newTypeSpec("string"), + required: false, + path: { + type: "directory", + mustExist: true, + }, + }, + parser: { + short: "p", + param: "file", + description: "Custom parser JS file", + type: Data.newTypeSpec("string"), + required: false, + exclusiveOf: "language", + path: { + type: "file", + mustExist: true, + }, + }, + formatter: { + short: "P", + param: "file", + description: "Custom formatter JS file", + type: Data.newTypeSpec("string"), + required: false, + exclusiveOf: "format", + path: { + type: "file", + mustExist: true, + }, + }, + hookTimeout: { + short: "T", + param: "ms", + description: "Timeout in milliseconds for hook execution", + type: Data.newTypeSpec("number"), + required: false, + default: 5000, + }, + mock: { + short: "m", + param: "dir", + description: "Path to mock parsers and formatters", + type: Data.newTypeSpec("string"), + required: false, + path: { + type: "directory", + mustExist: true, + }, + }, + config: { + short: "c", + param: "file", + description: "Use JSON config file", + type: Data.newTypeSpec("string"), + required: false, + path: { + type: "file", + mustExist: true, + }, + }, + sub: { + short: "s", + param: "name", + description: "Specify a subconfiguration", + type: Data.newTypeSpec("string"), + required: false, + dependent: "config", + }, + debug: { + short: "d", + description: "Enable debug mode", + type: Data.newTypeSpec("boolean"), + required: false, + default: false, + }, + debugLevel: { + short: "D", + param: "level", + description: "Debug level", + type: Data.newTypeSpec("number"), + required: false, + default: 0, + }, +}) + +const ConfigurationPriorityKeys = Data.deepFreezeObject(["exclude", "input"]) + +export { + ConfigurationParameters, + ConfigurationPriorityKeys +} diff --git a/src/Conveyor.js b/src/Conveyor.js new file mode 100644 index 0000000..f258013 --- /dev/null +++ b/src/Conveyor.js @@ -0,0 +1,215 @@ +import {ActionBuilder, ActionRunner, ACTIVITY} from "@gesslar/actioneer" +import {DirectoryObject, FileObject, Sass} from "@gesslar/toolkit" + +/** + * @import {Glog} from "@gesslar/toolkit" + * @import {Contract} from "@gesslar/negotiator" + */ + +const {IF} = ACTIVITY + +export default class Conveyor { + #parser + #formatter + + /** @type {Glog} */ + #glog + + /** @type {DirectoryObject} */ + #output + + /** @type {Contract} */ + #contract + + #hooks + #basePath + + constructor({basePath, parser, formatter, hooks, glog, contract, output}) { + this.#parser = parser + this.#formatter = formatter + this.#glog = glog + this.#output = output + this.#contract = contract + this.#hooks = hooks + this.#basePath = basePath + } + + /** + * Defines the per-file processing pipeline. + * + * @param {ActionBuilder} builder - The Actioneer builder instance. + */ + setup(builder) { + builder + .do("read", this.#readFile) + .do("parse", this.#parseFile) + .do("validate", this.#validateContracts) + .do("format", this.#formatFile) + .do("write", IF, this.#shouldWrite, this.#writeOutput) + } + + /** + * Processes files through the parser→formatter pipeline with concurrency. + * + * @param {Array} files - List of files to process. + * @param {number} [maxConcurrent] - Maximum number of files to process at a time. + * @returns {Promise} - Resolves with {succeeded, errored, warned}. + */ + async convey(files, maxConcurrent = 10) { + const glog = this.#glog + + const builder = new ActionBuilder(this) + const runner = new ActionRunner(builder) + .addSetup(async() => { + if(!await this.#output.exists) { + glog.info(`Directory '${this.#output.path}' does not exist. Creating.`) + + await this.#output.assureExists({recursive: true}) + } + }) + + const contexts = files.map(file => ({file})) + const settled = await runner.pipe(contexts, maxConcurrent) + + return this.#categorize(settled, files) + } + + // -- Pipeline activities -------------------------------------------------- + + #readFile = async ctx => { + const glog = this.#glog + const content = await ctx.file.read() + + const local = ctx.file.relativeTo(this.#basePath) + const size = await ctx.file.size() + + glog.info(`Wrote output ${local} (${size.toLocaleString()} bytes)`) + + return {...ctx, content} + } + + #parseFile = async ctx => { + try { + if(ctx.error) + return ctx + + const {content} = ctx + const builder = new ActionBuilder(new this.#parser()) + + if(this.#hooks?.Parse) + builder.withHooks(new this.#hooks.Parse()) + + const runner = new ActionRunner(builder) + const result = await runner.run(content) + + return Object.assign(ctx, {...result}) + } catch(error) { + return {...ctx, status: "error", error: Sass.new(`Parsing file ${ctx.file}`, error)} + } + } + + #validateContracts = ctx => { + if(ctx.error) + return ctx + + try { + this.#contract.validate(ctx) + } catch(err) { + if(err) { + throw Sass.new(`Parser validation for ${ctx.file.path}`, err) + } + } + + return ctx + } + + #formatFile = async ctx => { + if(ctx.error) + return ctx + + const {functions} = ctx + const builder = new ActionBuilder(new this.#formatter()) + + if(ctx.error) + throw Sass.new(`Formatting file ${ctx.file}`, ctx.error) + + if(ctx.error) + return ctx + + if(this.#hooks?.Format) + builder.withHooks(new this.#hooks.Format()) + + const runner = new ActionRunner(builder) + const formatResult = await runner.run(functions) + + return Object.assign(ctx, {formatResult}) + } + + #shouldWrite = ctx => { + if(ctx.error) + return ctx + + const result = this.#output != null && ctx?.formatResult + + if(result) + return result + + Object.assign(ctx, {status: "warning", warning: `No output content for ${ctx.file.path}`}) + + return false + } + + #writeOutput = async ctx => { + if(ctx.error) + return ctx + + const glog = this.#glog + const {file, formatResult} = ctx + const destExtension = this.#formatter.meta.extension ?? "txt" + const outputFile = new FileObject(`${file.module}.${destExtension}`, this.#output) + await outputFile.write(formatResult) + + const local = outputFile.relativeTo(this.#basePath) + const size = formatResult.length + + glog.info(`Wrote output ${local} (${size.toLocaleString()} bytes)`) + + return {...ctx, status: "success", outputFile} + } + + // -- Result categorization ------------------------------------------------ + + #categorize(settled, files) { + const succeeded = [] + const warned = [] + const errored = [] + + for(let i = 0; i < settled.length; i++) { + const entry = settled[i] + const file = files[i] + + if(entry.status === "rejected") { + errored.push({input: file, error: entry.reason}) + continue + } + + const val = entry.value + + switch(val?.status) { + case "success": + succeeded.push({input: file, output: val.outputFile}) + break + case "warning": + warned.push({input: file, warning: val.warning}) + break + case "error": + errored.push({input: file, error: val.error}) + break + default: + errored.push({input: file, error: new Error(`Unknown status: ${val?.status}`)}) + } + } + + return {succeeded, errored, warned} + } +} diff --git a/src/Discovery.js b/src/Discovery.js new file mode 100644 index 0000000..e092a15 --- /dev/null +++ b/src/Discovery.js @@ -0,0 +1,425 @@ +import {Terms} from "@gesslar/negotiator" +import {Collection, Data, DirectoryObject, FileObject, Promised, Sass} from "@gesslar/toolkit" +import {execSync} from "child_process" + +import Action from "./Action.js" +import {Schemer} from "@gesslar/negotiator/browser" + +/** + * @import {Glog} from "@gesslar/toolkit" + */ + +export default class Discovery { + /** @type {Glog} */ + #glog + + #options + + /** + * Constructor for Discovery. + * + * @param {object} arg - Constructor argument + * @param {object} arg.options - BeDoc options + * @param {Glog} arg.glog - Glog instance + */ + constructor({options = {}, glog}) { + this.#options = options + this.#glog = glog + } + + /** + * Discover actions from local or global node_modules + * + * @param {object} [specific] Configuration options for action discovery + * @param {FileObject} [specific.formatter] Print-related configuration options + * @param {FileObject} [specific.parser] Parse-related configuration options + * @param {Function} validateBeDocSchema - The validator function for BeDoc's action schema + * @returns {Promise} A map of discovered modules + */ + async discoverActions(specific = {}, validateBeDocSchema) { + const glog = this.#glog + + glog.debug("Discovering actions", 2) + + glog.debug("Specific modules provided: %o", 2, Object.values(specific).filter(Boolean).length) + glog.debug("Specific modules provided: %j", 4, specific) + + const files = [] + const options = this.#options + + if(options?.mockPath) { + glog.debug("Discovering mock actions in %o", 2, options.mockPath) + + files.push( + ...(await FS.getFiles([ + `${options.mockPath}/bedoc-*-formatter.js`, + `${options.mockPath}/bedoc-*-parser.js`, + ])), + ) + } else { + glog.debug("Mock path not set, discovering actions in node_modules", 2) + + glog.debug("Looking for actions in project's package.json", 2) + if(options.project) { + const exported = (options.project.actions || []) + .map(m => new FileObject(m, options.basePath)) + .flat() + + glog.debug("Found %o modules in project's package.json", 2, exported.length) + glog.debug("Found modules in project's package.json: %o", 2, exported) + + files.push(...exported) + } else { + glog.debug("No modules found in project's package.json", 2) + } + + glog.debug("Looking for modules in node_modules (global and locally installed)", 2) + const directories = [ + execSync("npm root").toString().trim(), + execSync("npm root -g").toString().trim(), + ] + .filter(Boolean) + .map(d => new DirectoryObject(d)) + + const nodeModulesDirs = await Data.asyncFilter(directories, d => d.exists) + + glog.debug("Found %o directories to search for actions", 2, directories.length) + + glog.debug("Directories to search for actions: %o", 4, directories) + + for(const nodeModulesDir of nodeModulesDirs) { + const dirsToSearch = [] + const {directories: moduleDirs} = await nodeModulesDir.read() + + glog.debug("Found %o directories in %o", 2, moduleDirs.length, nodeModulesDir.path) + + // Handle scoped packages (e.g., @bedoc/something) + const scopedDirs = moduleDirs.filter(d => d.name.startsWith("@")) + + dirsToSearch.push(...moduleDirs) + + // If we find a scope (e.g., "@bedoc"), look inside it for bedoc modules + for(const scopedDir of scopedDirs) { + const {directories: scopedPackages} = await scopedDir.read() + + glog.debug("Found %o directories under scoped package %o", 2, directories.length, scopedDir.name) + glog.debug("Found directories under scoped package %o\n%o", 2, scopedDir.path, scopedPackages.map(d => d.path)) + + dirsToSearch.push(...scopedPackages) + } + + glog.debug("Found %o directories to search for actions", 2, dirsToSearch.length) + glog.debug("Found directories to search for actions: %o", 4, dirsToSearch) + + const visibleDirs = dirsToSearch.filter(d => !d.name.startsWith(".")) + + for(const dir of visibleDirs) { + const packageJsonFile = new FileObject("package.json", dir) + + if(!await packageJsonFile.exists) + continue + + const packageJson = await packageJsonFile.loadData() + + if(!packageJson.bedoc) + continue + + const {actions} = packageJson.bedoc ?? null + + if(!actions || !Array.isArray(actions)) + continue + + const moduleFileObjects = actions.map(f => new FileObject(f, dir)) + const actionObjects = await Data.asyncFilter( + moduleFileObjects, f => f.exists) + + glog.debug("Discovered %o modules from package.json file: %o", 2, + actions.length, + packageJsonFile.path + ) + + glog.debug("Discovered from package.json files: %o", 3, actions) + + files.push(...actionObjects) + } + } + } + + glog.debug("Discovered %o modules", 2, files.length) + glog.debug("Discovered modules", 2, files.map(f => f.path)) + glog.debug("Discovered modules %o", 3, files) + + const loaded = await this.#loadActionsAndContracts(files, specific) + + for(const [kind, actions] of Object.entries(loaded)) { + glog.debug("%o %o", 4, kind, actions) + + for(const {file, terms} of actions) { + + try { + const isValid = validateBeDocSchema(terms) + if(!isValid) { + const {errors} = validateBeDocSchema + const report = Schemer.reportValidationErrors(errors) + + throw Sass.new(report) + } + + } catch(error) { + glog.error(error) + + throw Sass.new(`Validating schema for ${file.path}`, error) + } + } + } + + return loaded + } + + /** + * Process the discovered file objects and return the action and their + * respective contracts. + * + * @param {Array} moduleFiles - The module file objects to process + * @param {{parser: FileObject, formatter: FileObject}} specificModules - The specific modules to load + * @returns {Promise} The discovered actions + */ + async #loadActionsAndContracts(moduleFiles, specificModules) { + const glog = this.#glog + + glog.debug("Loading actions and contracts", 2) + glog.debug("Loading %o module files", 2, moduleFiles.length) + glog.debug("Specific modules to load: %o", 4, specificModules) + + const resultActions = await Collection.allocateObject( + Action.actionTypes, + Action.actionTypes.map(() => new Array()) + ) + + // Tag the specific actions to load, so we can filter them later + for(const [type, file] of Object.entries(specificModules)) { + if(file) { + glog.debug("Tagging specific module `%o` as `%o`", 3, file.path, type) + file.specificType = file.specificType || [] + file.specificType.push(type) + } + } + + const toLoad = [ + ...moduleFiles, + ...Object.values(specificModules).filter(Boolean), + ] + + glog.debug("Loading %o discovered modules", 2, toLoad.length) + glog.debug("Modules to load: %o", 4, toLoad) + + // Load the BeDoc action schema once for validating all contract terms + // const schemaValidator = await this.#loadSchemaValidator() + + const settledLoading = await Promised.settle( + toLoad.map(async file => { + const action = await file.import() + + if(!action.default?.meta) + return null + + const {terms: actionTerms} = action.default.meta + + const terms = await Terms.parse(actionTerms, file.parent) + + return {file, action, terms} + }) + ) + + if(Promised.hasRejected(settledLoading)) + Promised.throw(settledLoading) + + const loadedActions = Promised.values(settledLoading).filter(Boolean) + + glog.debug("Loaded %o actions", 2, loadedActions.length) + glog.debug("Loaded actions", 4, loadedActions) + + const filteredActions = [] + + for(const actionType of Action.actionTypes) { + const moduleFile = specificModules[actionType] + const matchingActions = [] + + if(moduleFile) { + glog.debug("Filtering actions for specific: %o", 2, actionType) + const found = loadedActions.find( + e => e.file.specificType?.includes(actionType) && + e.action.default.meta.kind === actionType + ) + + if(!found) + throw Sass.new(`Could not find specific action: ${moduleFile.path}`) + + matchingActions.push(found) + } else { + glog.debug("No specific action required for %o", 2, actionType) + + const found = loadedActions.filter( + e => e.action.default.meta.kind === actionType + ) + + matchingActions.push(...found) + } + + glog.debug("Filtered %o actions for %o", 2, + matchingActions.length, actionType + ) + + filteredActions.push(...matchingActions) + } + + glog.debug("Filtered %o actions", 2, filteredActions.length) + glog.debug("Filtered actions %o", 4, filteredActions) + + // Now check the metas for validity + for(const filtered of filteredActions) { + const {action, terms, file: moduleFile} = filtered + const {meta} = action.default + const {kind} = meta + + glog.debug("Checking %o action", 2, kind) + + const isValid = this.#validMeta(kind, {action, terms}) + + glog.debug("Meta in action %o in %o is %o", 3, + kind, moduleFile.module, isValid ? "valid" : "invalid" + ) + + if(isValid) { + glog.debug("Action is a valid %o action", 3, kind) + + resultActions[kind].push({ + file: moduleFile, + action, + terms, + }) + } else { + glog.debug("Action is not a valid %o action", 3, kind) + } + + glog.debug("Processed %o action", 2, kind) + } + + for(const actionType of Action.actionTypes) { + const total = resultActions[actionType].length + + glog.debug("Found %o %o actions", 2, total, actionType) + } + + const total = Object.keys(resultActions).reduce((acc, curr) => { + return acc + resultActions[curr].length + }, 0) + + glog.debug("Loaded %o action definitions from %o modules", 2, + total, moduleFiles.length + ) + + return resultActions + } + + satisfyCriteria(actions, validatedConfig) { + const glog = this.#glog + + glog.debug("Available actions to check %o", 4, actions) + + const satisfied = {parser: [], formatter: []} + const toMatch = { + // TODO: investigate + parser: {metaKey: "input", configKey: "language", config: "parser"}, + formatter: {metaKey: "format", configKey: "format", config: "formatter"}, + } + + glog.debug("Satisfying criteria for actions", 2) + for(const [actionType, search] of Object.entries(toMatch)) { + glog.debug("Satisfying criteria for %o actions", 2, actionType) + + const {metaKey, configKey, config} = search + + glog.debug("Meta key: %o, Config key: %o", 3, metaKey, configKey) + + // First let's check if we wanted something specific + if(validatedConfig[config]) { + glog.debug("Checking for specific %o action", 3, actionType) + const found = actions[actionType].find( + a => a.file.specificType.includes(actionType) + ) + + if(found) { + glog.debug("Found specific %o action", 3, actionType) + satisfied[actionType].push(found) + continue + } + + glog.debug("No specific %o action found", 3, actionType) + } + + // Hmm! We didn't find anything specific. Let's check the criterion + glog.debug("Checking for %o actions with meta key %o", 3, actionType, metaKey) + glog.debug("Validated config to check against: %O", 3, validatedConfig) + + const found = actions[actionType].filter(a => { + glog.debug("Meta criterion value: %o", 4, a.action.default.meta[metaKey]) + glog.debug("Config criterion value: %o", 4, validatedConfig[configKey]) + + return a.action.default.meta[metaKey] === validatedConfig[configKey] + }) + glog.debug("Found %o %o actions with criterion %o", 3, found.length, actionType, metaKey) + + // Shove them into the result! + satisfied[actionType].push(...found) + + // That should about cover it! + } + + return satisfied + } + + /** + * Validates the meta requirements for an action + * + * @param {string} actionType The action type to validate + * @param {object} toValidate - The action object to validate + * @returns {boolean} Whether the action object meets the meta requirements + */ + #validMeta(actionType, toValidate) { + const glog = this.#glog + + glog.debug("Checking meta requirements for %o", 3, actionType) + + const requirements = Action.actionMetaRequirements[actionType] + + if(!requirements) + throw Sass.new( + `No meta requirements found for action type \`${actionType}\``, + ) + + for(const requirement of requirements) { + glog.debug("Checking requirement %o", 4, requirement) + + if(Data.isType(requirement, "object")) { + for(const [key, value] of Object.entries(requirement)) { + glog.debug("Checking object requirement %o", 4, {key, value}) + + if(toValidate.action.default.meta[key] !== value) + return false + + glog.debug("Requirement met: %o", 4, {key, value}) + } + } else if(Data.isType(requirement, "string")) { + glog.debug("Checking string requirement: %o", 4, requirement) + + if(!toValidate.action.default.meta[requirement]) + return false + + glog.debug("Requirement met: %o", 4, requirement) + } + } + + return true + } +} diff --git a/src/Environment.js b/src/Environment.js new file mode 100644 index 0000000..4d4f799 --- /dev/null +++ b/src/Environment.js @@ -0,0 +1,8 @@ +import {Data} from "@gesslar/toolkit" + +export default Data.deepFreezeObject({ + EXTENSION: "extension", + NPM: "npm", + ACTION: "action", + CLI: "cli", +}) diff --git a/src/Logger.js b/src/Logger.js new file mode 100644 index 0000000..e2ba83c --- /dev/null +++ b/src/Logger.js @@ -0,0 +1,182 @@ +/* + For formatting console info, see: + https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args + + * %s: String will be used to convert all values except BigInt, Object and -0. + BigInt values will be represented with an n and Objects that have no + user defined toString function are inspected using util.inspect() with + options { depth: 0, colors: false, compact: 3 }. + * %d: Number will be used to convert all values except BigInt and Symbol. + * %i: parseInt(value, 10) is used for all values except BigInt and Symbol. + * %f: parseFloat(value) is used for all values expect Symbol. + * %j: JSON. Replaced with the string '[Circular]' if the argument contains + circular references. + * %o: Object. A string representation of an object with generic JavaScript + object formatting. Similar to util.inspect() with options { showHidden: + true, showProxy: true }. This will show the full object including non- + enumerable properties and proxies. + * %O: Object. A string representation of an object with generic JavaScript + object formatting. Similar to util.inspect() without options. This will + show the full object not including non-enumerable properties and + proxies. + * %%: single percent sign ('%'). This does not consume an argument. + +*/ + +import ErrorStackParser from "error-stack-parser" +import console from "node:console" +import {Environment} from "./BeDoc.js" +import {FileObject, Util} from "@gesslar/toolkit" + +export const loggerColours = { + debug: [ + "\x1b[38;5;19m", // Debug level 0: Dark blue + "\x1b[38;5;27m", // Debug level 1: Medium blue + "\x1b[38;5;33m", // Debug level 2: Light blue + "\x1b[38;5;39m", // Debug level 3: Teal + "\x1b[38;5;44m", // Debug level 4: Blue-tinted cyan + ], + info: "\x1b[38;5;36m", // Medium Spring Green + warn: "\x1b[38;5;214m", // Orange1 + error: "\x1b[38;5;196m", // Red1 + reset: "\x1b[0m", // Reset +} + +/** + * Logger class + * + * Log levels: + * - debug: Debugging information + * - Debug levels + * - 0: No/critical debug information, not error level, but, should be + * logged + * - 1: Basic debug information, startup, shutdown, etc + * - 2: Intermediate debug information, discovery, starting to get more + * detailed + * - 3: Detailed debug information, parsing, processing, etc + * - 4: Very detailed debug information, nerd mode! + * - warn: Warning information + * - info: Informational information + * - error: Error information + */ + +export default class Logger { + #name = null + #debugLevel = 0 + + constructor(options) { + this.#name = "BeDoc" + if(options) { + this.setOptions(options) + if(options.env === Environment.EXTENSION) { + const vscode = import("vscode") + + this.vscodeError = vscode.window.showErrorMessage + this.vscodeWarn = vscode.window.showWarningMessage + this.vscodeInfo = vscode.window.showInformationMessage + } + } + } + + get name() { + return this.#name + } + + get debugLevel() { + return this.#debugLevel + } + + get options() { + return { + name: this.#name, + debugLevel: this.#debugLevel, + } + } + + setOptions(options) { + this.#name = options.name ?? this.#name + this.#debugLevel = options.debugLevel + } + + #compose(level, message, debugLevel = 0) { + const tag = Util.capitalize(level) + + if(level === "debug") + return `[${this.#name}] ${loggerColours[level][debugLevel]}${tag}${loggerColours.reset}: ${message}` + + return `[${this.#name}] ${loggerColours[level]}${tag}${loggerColours.reset}: ${message}` + } + + lastStackLine(error = new Error(), stepsRemoved = 3) { + const stack = ErrorStackParser.parse(error) + + return stack[stepsRemoved] + } + + extractFileFunction(level = 0) { + const frame = this.lastStackLine() + const { + functionName: func, + fileName: file, + lineNumber: line, + columnNumber: col, + } = frame + + const tempFile = new FileObject(file) + const {module, url} = tempFile + + let functionName = func ?? "anonymous" + + if(functionName.startsWith("#")) + functionName = `${module}.${functionName}` + + const methodName = /\[as \w+\]$/.test(functionName) + ? /\[as (\w+)\]/.exec(functionName)[1] + : null + + if(methodName) { + functionName = functionName.replace(/\[as \w+\]$/, "") + functionName = `${functionName}{${methodName}}` + } + + if(/^async /.test(functionName)) + functionName = functionName.replace(/^async /, "(async)") + + let result = functionName + + if(level >= 2) + result = `${result}:${line}:${col}` + + if(level >= 3) + result = `${url} ${result}` + + return result + } + + newDebug(tag) { + return function(message, level, ...arg) { + tag = this.extractFileFunction(this.#debugLevel) + this.debug(`[${tag}] ${message}`, level, ...arg) + }.bind(this) + } + + debug(message, level = 0, ...arg) { + if(level <= (this.debugLevel ?? 4)) + console.debug(this.#compose("debug", message, level), ...arg) + } + + warn(message, ...arg) { + console.warn(this.#compose("warn", message), ...arg) + this.vscodeWarn?.(JSON.stringify(message)) + } + + info(message, ...arg) { + console.info(this.#compose("info", message), ...arg) + this.vscodeInfo?.(JSON.stringify(message)) + } + + error(message, ...arg) { + console.error(this.#compose("error", message), ...arg) + this.vscodeError?.(JSON.stringify(message)) + } +} diff --git a/src/Schema.js b/src/Schema.js new file mode 100644 index 0000000..1cd7fb9 --- /dev/null +++ b/src/Schema.js @@ -0,0 +1,6 @@ +import {Data} from "@gesslar/toolkit" + +export default Data.deepFreezeObject({ + local: "dist/schema/bedoc.action.json", + url: "https://schema.gesslar.dev/bedoc/v1/bedoc-action.json", +}) diff --git a/src/cli.js b/src/cli.js index fb1c07b..914e745 100644 --- a/src/cli.js +++ b/src/cli.js @@ -1,24 +1,29 @@ #!/usr/bin/env node -import {Data, DirectoryObject, FileObject, Glog, Term} from "@gesslar/toolkit" +import {Data, DirectoryObject, FileObject, Glog, Sass, Tantrum, Term} from "@gesslar/toolkit" import {program} from "commander" -import console from "node:console" import process from "node:process" import url from "node:url" -import {ConfigurationParameters} from "./core/ConfigurationParameters.js" -import BeDoc, {Environment} from "./core/Core.js" +import BeDoc from "./BeDoc.js" +import {ConfigurationParameters} from "./ConfigurationParameters.js" +import Environment from "./Environment.js" +import Schema from "./Schema.js" +import {Schemer} from "@gesslar/negotiator" // Main entry point void (async() => { try { + const glog = new Glog() + .withName("BeDoc") + .withStackTrace() + .noDisplayName() + // Get package info const thisPath = new DirectoryObject(url.fileURLToPath(new url.URL("..", import.meta.url))) const pkgJsonFile = new FileObject("package.json", thisPath) const pkgJson = await pkgJsonFile.loadData() - Glog.setLogLevel(5).setLogPrefix("[BEDOC]") - // Setup program program .name(pkgJson.name) @@ -66,19 +71,22 @@ void (async() => { } // Create core instance with validated config - const prjPath = new DirectoryObject(process.cwd()) + const prjPath = new DirectoryObject() const prjPkJsonFile = new FileObject("package.json", prjPath) const prjPkjJson = await prjPkJsonFile.loadData() const pkjBedoc = prjPkjJson?.bedoc ?? {} + const validateBeDocSchema = await loadSchemaValidator(prjPath) const bedoc = await BeDoc .new({ options: { ...optionsWithSources, - basePath: {value: prjPath, source: "cli"}, + basePath: prjPath, project: pkjBedoc, }, - source: Environment.CLI + source: Environment.CLI, + glog, + validateBeDocSchema, }) if(!(bedoc instanceof BeDoc)) { @@ -88,29 +96,35 @@ void (async() => { } } - const filesToProcess = bedoc.options.input.map(f => f.path) - const result = await bedoc.processFiles(filesToProcess) + const result = await bedoc.processFiles() const errored = result.errored const warned = result.warned - if(warned.length > 0) - warned.forEach(w => bedoc.logger.warn(w.warning)) + if(warned?.length > 0) + warned.forEach(w => glog.warn(w.warning)) - if(errored.length > 0) - throw new AggregateError(errored.map(e => e.error), "Error processing files") + if(errored?.length > 0) { + const errors = errored.map(e => e.error) + Tantrum.new("Error processing files", errors).report(true) + } process.exit(0) - } catch (error) { - if(error instanceof Error) { - if(error instanceof AggregateError) { - error.errors.forEach(e => console.error(e)) - } else { - console.error(error.message, error.stack) - } - } else { - console.error("Error: %o", error) - } + } catch(error) { + Sass.new("Starting BeDoc", error).report(true) process.exit(1) } + + /** + * Load the BeDoc action schema and return a validator function. + * + * @returns {Promise} AJV validator function + */ + async function loadSchemaValidator(prjPath) { + const schemaFile = new FileObject(Schema.local, prjPath) + if(!(await schemaFile.exists)) + throw Sass.new(`Missing schema at ${schemaFile.path}`) + + return await Schemer.fromFile(schemaFile) + } })() diff --git a/tsconfig.types.json b/tsconfig.types.json new file mode 100644 index 0000000..dc52d99 --- /dev/null +++ b/tsconfig.types.json @@ -0,0 +1,42 @@ +{ + "compilerOptions": { + // Enable JavaScript support + "allowJs": true, + "checkJs": false, + "maxNodeModuleJsDepth": 0, + // Type declaration generation + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + // Output configuration + "outDir": "./dist/types", + "rootDir": "./src", + // Module system + "module": "ES2022", + "moduleResolution": "node", + "target": "ES2022", + // Strict type checking (helps generate better types) + "strict": false, + "noImplicitAny": false, + "strictNullChecks": false, + // Additional checks + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + // Emit settings + "stripInternal": false, + "removeComments": false, + "preserveConstEnums": true + }, + "include": [ + "src/**/*.js" + ], + "exclude": [ + "node_modules", + "tests", + "work", + "src/types", + "examples/" + ] +}