Creating Modules
Learn how to create your own DevFlow module from scratch: detection logic, file generation, the options schema and wizard integration.
Quick Start
Every module is a single TypeScript file that exports a DevFlowModule object.
Template
Create a new file in the appropriate category directory (e.g., src/modules/quality/my-tool.ts):
import type {
DevFlowModule,
ModuleAction,
ProjectContext,
} from "../../core/types.js";
import { fileExists } from "../../core/fs-utils.js";
const myToolModule: DevFlowModule = {
id: "my-tool",
name: "My Tool",
description: "Short description for the wizard checkbox",
category: "quality",
recommendedFor: ["react", "vue", "node"],
conflicts: [], // module IDs that conflict
dependsOn: [], // module IDs that must be installed first
async detect(ctx: ProjectContext) {
const hasConfig = fileExists(ctx.root, "my-tool.config.js");
return {
applicable: true,
alreadyConfigured: hasConfig,
reason: hasConfig ? "My Tool already configured" : undefined,
};
},
async plan(ctx: ProjectContext, opts) {
const actions: ModuleAction[] = [];
// 1. Install packages
actions.push({
type: "install",
dev: true,
packages: ["my-tool"],
});
// 2. Create config file
const config = generateConfig(ctx);
actions.push({
type: "create-file",
path: "my-tool.config.js",
content: config,
skipIfExists: true,
});
// 3. Add npm script
actions.push({
type: "add-script",
name: "my-tool",
command: "my-tool .",
});
return actions;
},
};
function generateConfig(ctx: ProjectContext): string {
// Use ctx.stack, ctx.hasTypeScript, ctx.pm to customize
return `export default {
// configuration here
};
`;
}
export default myToolModule;Register the Module
Add your module to src/modules/registry.ts:
import myToolModule from "./quality/my-tool.js";
export const ALL_MODULES: DevFlowModule[] = [
// ... existing modules
myToolModule,
];Add to Presets (Optional)
If your module should be included in presets, add its ID to the appropriate arrays in src/modules/presets.ts.
Writing Tests
Create a test file next to the module or in the test directory:
import { describe, it, expect } from "vitest";
import myToolModule from "../modules/quality/my-tool.js";
import { stubContext } from "../test/helpers.js";
describe("my-tool.detect()", () => {
it("returns alreadyConfigured=false without config", async () => {
const ctx = stubContext();
const result = await myToolModule.detect(ctx);
expect(result.alreadyConfigured).toBe(false);
});
});
describe("my-tool.plan()", () => {
it("installs my-tool package", async () => {
const ctx = stubContext();
const actions = await myToolModule.plan(ctx, {});
const install = actions.find((a) => a.type === "install") as any;
expect(install.packages).toContain("my-tool");
});
});Best Practices
- Keep
plan()pure — no file system access, no side effects - Use
fileExists()indetect()— check common config file locations - Set
skipIfExists: true— make your module idempotent - Respect the context — use
ctx.stack,ctx.pm,ctx.hasTypeScriptfor stack-aware configs - Declare conflicts — if your tool replaces another, add it to
conflicts[] - Declare dependencies — if your tool needs another module, add it to
dependsOn[]
Context Detection
How DevFlow detects your stack, package manager, TypeScript configuration and monorepo layout, and which signal wins on a conflict.
Contributing
How to contribute to DevFlow — set up your local dev environment, write or fix modules, run tests, and submit a pull request to the open-source repository.