loadSteps and loadStepsFromDir
These functions import step files at runtime with jiti, so a step file can be TypeScript with no build step. They do not throw: both return an EitherAsync<ReadonlyArray<LoadError>, ReadonlyArray<Step<E, Ctx>>>, with the steps on the right, or on the left every file that could not be loaded.
loadSteps
Section titled “loadSteps”Loads the steps of a list of files. Relative paths are resolved against the working directory. The steps keep the order of the paths, then the order inside each file.
import { loadSteps } from "@ngwes/groucho-fp";
type Ctx = { input: string; validated?: boolean };type Err = { message: string };
// an Either: the steps, or the load errorsconst loaded = await loadSteps<Err, Ctx>(["./steps/validate.ts", "./steps/transform.ts"]);loadStepsFromDir
Section titled “loadStepsFromDir”Loads the step files of a directory (not its sub-directories), in alphabetical order of file name. Prefix the names (01-validate.ts, 02-transform.ts) when the order matters.
import { loadStepsFromDir } from "@ngwes/groucho-fp";
type Ctx = { input: string; validated?: boolean };type Err = { message: string };
const loaded = await loadStepsFromDir<Err, Ctx>("./steps");
// only the files whose name matches the patternconst tagged = await loadStepsFromDir<Err, Ctx>("./steps", { pattern: /\.step\.ts$/ });| Option | Default | Description |
|---|---|---|
pattern |
.ts, .js, .mjs and .cjs files, except .d.ts, .test.* and .spec.* files |
A regular expression tested against each file name. Only matching files are loaded. Its g and y flags are ignored. |
A steps folder holds only step files. Every file the pattern matches must export steps; a helper module or a type-only file in the folder is reported as an invalid-export error and the whole load is a left. Keep shared code outside the folder, or pass a pattern that only matches the step files.
Step file formats
Section titled “Step file formats”A step file exports its steps in one of three ways. If it has a steps export, that one is used; otherwise the default export.
1. Default export: one step
import { left, right } from "@ngwes/mini-fp";import type { Step } from "@ngwes/groucho-fp";
type Ctx = { input: string; validated?: boolean };type Err = { message: string };
const validate: Step<Err, Ctx> = (ctx) => ctx.input.length > 0 ? right({ ...ctx, validated: true }) : left({ message: "input is empty" });
export default validate;2. Default export: an array of steps
import { right } from "@ngwes/mini-fp";import type { Step } from "@ngwes/groucho-fp";
type Ctx = { input: string; trimmed?: string; upper?: string };type Err = { message: string };
const trim: Step<Err, Ctx> = (ctx) => right({ ...ctx, trimmed: ctx.input.trim() });const upper: Step<Err, Ctx> = (ctx) => right({ ...ctx, upper: (ctx.trimmed ?? "").toUpperCase() });
export default [trim, upper];3. Named export steps
import { right } from "@ngwes/mini-fp";import type { Step } from "@ngwes/groucho-fp";
type Ctx = { input: string; auditedAt?: Date };type Err = { message: string };
export const steps: ReadonlyArray<Step<Err, Ctx>> = [ (ctx) => right({ ...ctx, auditedAt: new Date() }),];An empty array is valid and adds no steps. Anything else, such as a module with neither export, an object, or an array holding something that is not a function, is an invalid-export error.
The DynamicStepModule<E, Ctx> type describes these three shapes. The loader only checks at runtime that the steps are functions: it cannot check their types.
Load errors
Section titled “Load errors”Each file that cannot be loaded gives one LoadError. Every file is attempted, so a left lists all the failures, in file order. path is always absolute.
tag |
Meaning | Extra field |
|---|---|---|
not-found |
The file, or the directory given to loadStepsFromDir, does not exist or cannot be read. |
cause |
import-failed |
The file exists but importing it failed: a syntax error, a module that throws when imported, a missing dependency. | cause |
invalid-export |
The file imported fine but has no usable steps or default export. |
detail |
Handle them with match, or turn them into your own error type with mapLeft:
import { loadStepsFromDir } from "@ngwes/groucho-fp";import type { LoadError, Step } from "@ngwes/groucho-fp";
type Ctx = { input: string };type Err = { message: string };
const describeLoadError = (error: LoadError): string => { switch (error.tag) { case "not-found": return `${error.path}: not found`; case "import-failed": return `${error.path}: could not be imported (${String(error.cause)})`; case "invalid-export": return `${error.path}: ${error.detail}`; }};
const steps: ReadonlyArray<Step<Err, Ctx>> = await loadStepsFromDir<Err, Ctx>("./steps").match( (loaded) => loaded, (errors) => { throw new Error(`cannot load the steps:\n${errors.map(describeLoadError).join("\n")}`); },);