Skip to content

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.

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 errors
const loaded = await loadSteps<Err, Ctx>(["./steps/validate.ts", "./steps/transform.ts"]);

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 pattern
const 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.

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

steps/01-validate.ts
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

steps/02-transforms.ts
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

steps/03-audit.ts
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.

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")}`);
},
);