UNPKG

@plugjs/plug

Version:
205 lines 8.36 kB
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ import { sep } from 'node:path'; import { assert, assertPromises } from "./asserts.js"; import { getLogger } from "./logging.js"; import { getAbsoluteParent, getCurrentWorkingDirectory, resolveAbsolutePath } from "./paths.js"; /* ========================================================================== * * PLUG CONTEXT * * ========================================================================== */ /** * The {@link Context} class defines the context in which a {@link Plug} * is invoked. */ export class Context { buildFile; taskName; /** The directory of the file where the task was defined (convenience). */ buildDir; /** The {@link Logger} associated with this instance. */ log; constructor( /** The absolute file name where the task was defined. */ buildFile, /** The _name_ of the task associated with this {@link Context}. */ taskName) { this.buildFile = buildFile; this.taskName = taskName; this.buildDir = getAbsoluteParent(buildFile); this.log = getLogger(taskName); } /** Return a new {@link Context} with the specified task name */ withTaskName(taskName) { return new Context(this.buildFile, taskName); } /** * Resolve a (set of) path(s) in this {@link Context}. * * If the path (or first component thereof) starts with `@...`, then the * resolved path will be relative to the directory containing the build file * where the current task was defined, otherwise it will be relative to the * current working directory. */ resolve(path, ...paths) { // Paths starting with "@" are relative to the build file directory if (path && path.startsWith('@')) { // We can have paths like "@/../foo/bar" or "@../foo/bar"... both are ok const components = path.substring(1).split(sep).filter((s) => !!s); return resolveAbsolutePath(this.buildDir, ...components, ...paths); } // No path? Resolve to the CWD! if (!path) return getCurrentWorkingDirectory(); // For all the rest, normal resolution! return resolveAbsolutePath(getCurrentWorkingDirectory(), path, ...paths); } } /* ========================================================================== * * PIPES * * ========================================================================== */ /** * In pipe chains, we want to keep track of the _leaf_ promises (that * is, when a derived pipe is created calling `plug` we want to track only the * new, derived, promise). * * We key these _leaf_ promises by _context_ (with a WeakMap), and those will * be awaited at the end of the task. */ const contextPromises = new WeakMap(); /** * An internal class recording _hot_ (failure will fail the task) and _cold_ * (failure will be ignored) {@link Promise}s for a task's {@link Context}. */ export class ContextPromises { context; _cold = new Set(); _hot = new Set(); /* Private constructor */ constructor(context) { this.context = context; } /** Track a {@link Promise} _hot_ (failure will fail the task) */ hot(promise) { this._cold.delete(promise); this._hot.add(promise); } /** Track a {@link Promise} _cold_ (failure will be ignored) */ cold(promise) { this._hot.delete(promise); this._cold.add(promise); } /** * Await all tracked {@link Promise}s, triggering a build failure if any of * the _hot_ ones is rejected. */ static async wait(context) { const instance = contextPromises.get(context); if (!instance) return; await Promise.allSettled([...instance._cold]); await assertPromises([...instance._hot]); } /** Get a {@link ContextPromises} instance for the given {@link Context} */ static get(context) { let promises = contextPromises.get(context); if (!promises) { promises = new ContextPromises(context); contextPromises.set(context, promises); } return promises; } } /** The default implementation of the {@link Pipe} interface. */ export class PipeImpl { _context; _promise; [Symbol.toStringTag] = 'Pipe'; constructor(_context, _promise) { this._context = _context; this._promise = _promise; // New "Pipe", remember the promise! ContextPromises.get(_context).hot(_promise); } /* ------------------------------------------------------------------------ * * Promise implementation * * ------------------------------------------------------------------------ * * From a _types_ point of view, the `Pipe` implements a `Promise<Files>` * * (because only when plugging the correct `Plug` the correct value are * * returned). * * * * Whether to return (as a type) another `Pipe` or a `Promise<undefined>` * * is determined by the type of the `plug` parameter below. * * * * That said, in practice, a `Pipe` implements `Promise<Files | undefined>` * * because the result of the plug is _eventually_ computed asynchronously * * while `plug` returns immediately. * * * So, all those "as whatever" below are kind-of-legit... * * ------------------------------------------------------------------------ */ then(onfulfilled, onrejected) { // We are delegating the handling of this promise to the caller ContextPromises.get(this._context).cold(this._promise); return this._promise.then(onfulfilled, onrejected); } catch(onrejected) { // We are delegating the handling of this promise to the caller ContextPromises.get(this._context).cold(this._promise); return this._promise.catch(onrejected); } finally(onfinally) { // We are delegating the handling of this promise to the caller ContextPromises.get(this._context).cold(this._promise); return this._promise.finally(onfinally); } plug(arg) { const plug = typeof arg === 'function' ? { pipe: arg } : arg; // We are creating a new "leaf" Pipe, we can forget our promise ContextPromises.get(this._context).cold(this._promise); // Create and return the new Pipe return new PipeImpl(this._context, this._promise.then(async (result) => { assert(result, 'Unable to extend pipe'); const result2 = await plug.pipe(result, this._context); return result2 || undefined; })); } } /** * Install a {@link Plug} into our {@link Pipe} prototype. * * This allows our shorthand syntax for well-defined plugs such as: * * ``` * find('./src', '*.ts').write('./target') * // Nicer and easier than... * find('./src', '*.ts').plug(new Write('./target')) * ``` * * Use this alongside interface merging like: * * ``` * declare module '@plugjs/plug/pipe' { * export interface Pipe { * write(): Pipe * } * } * * install('write', class Write implements Plug { * constructorg(...args: PipeParams<'write'>) { * // here `args` is automatically inferred by whatever was declared above * } * * // ... the plug implementation lives here * }) * ``` */ export function install(name, ctor) { /* The function plugging the newly constructed plug in a pipe */ function plug(...args) { // eslint-disable-next-line new-cap return this.plug(new ctor(...args)); } /* Setup name so that stack traces look better */ Object.defineProperty(plug, 'name', { value: name }); /* Inject the create function in the Pipe's prototype */ void Object.defineProperty(PipeImpl.prototype, name, { value: plug }); } //# sourceMappingURL=pipe.js.map