Default Effect migration rules for detecting legacy patterns and enforcing architectural boundaries.
pnpm add -D @effect-migrate/preset-basicAdd the preset to your effect-migrate.config.ts:
import type { Config } from "@effect-migrate/core"
export default {
version: 1,
presets: ["@effect-migrate/preset-basic"],
paths: {
root: ".",
include: ["src/**/*.ts"]
}
} satisfies ConfigThe preset automatically loads all pattern and boundary rules, plus sensible config defaults.
Pattern rules detect legacy code patterns that should be migrated to Effect equivalents.
Detects: async function declarations and arrow functions
Why: async/await cannot be interrupted, retried, or composed with Effects. Effect.gen provides all async/await benefits plus interruption, retry, and structured concurrency.
Example violation:
// ❌ Detected
async function fetchUser(id: string) {
const response = await fetch(`/users/${id}`)
return response.json()
}Fix:
// ✅ Use Effect.gen
const fetchUser = (id: string) =>
Effect.gen(function* () {
const response = yield* Effect.tryPromise(() => fetch(`/users/${id}`))
const data = yield* Effect.tryPromise(() => response.json())
return data
})Detects: new Promise<T>(...) constructor calls
Why: Raw Promises lack resource safety and composability. Effect provides better abstractions for async operations.
Example violation:
// ❌ Detected
function delay(ms: number) {
return new Promise<void>((resolve) => setTimeout(resolve, ms))
}Fix:
// ✅ Use Effect.sleep
const delay = (ms: number) => Effect.sleep(Duration.millis(ms))
// ✅ Or Effect.async for custom async operations
const delay = (ms: number) =>
Effect.async<void>((resume) => {
const id = setTimeout(() => resume(Effect.void), ms)
return Effect.sync(() => clearTimeout(id))
})Detects: try { ... } catch blocks
Why: try/catch doesn't compose well with Effects and loses type information. Effect.catchAll and Effect.catchTag provide typed error handling.
Example violation:
// ❌ Detected
function parseJSON(text: string) {
try {
return JSON.parse(text)
} catch (error) {
return null
}
}Fix:
// ✅ Use Effect.try
const parseJSON = (text: string) =>
Effect.try({
try: () => JSON.parse(text),
catch: (error) => new ParseError({ message: String(error) })
})
// ✅ Or handle specific error types
const program = Effect.gen(function* () {
const data = yield* parseJSON(text).pipe(
Effect.catchTag("ParseError", () => Effect.succeed(null))
)
return data
})Detects: import { ... } from "effect" (barrel imports)
Why: Barrel imports hurt tree-shaking and increase bundle size. Import from specific modules for better optimization.
Example violation:
// ❌ Detected
import { Effect, Console, pipe } from "effect"Fix:
// ✅ Import from specific modules
import * as Effect from "effect/Effect"
import * as Console from "effect/Console"
import { pipe } from "effect/Function"Detects: from "fs/promises" or from "node:fs/promises" imports
Why: Direct Node.js filesystem imports couple code to Node.js. Use @effect/platform FileSystem for cross-platform compatibility and resource safety.
Example violation:
// ❌ Detected
import { readFile } from "fs/promises"
async function loadConfig(path: string) {
const content = await readFile(path, "utf-8")
return JSON.parse(content)
}Fix:
// ✅ Use @effect/platform FileSystem
import { FileSystem } from "@effect/platform"
import * as Effect from "effect/Effect"
const loadConfig = (path: string) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const content = yield* fs.readFileString(path)
return JSON.parse(content)
})Boundary rules enforce architectural constraints to maintain clean separation between Effect and platform-specific code.
Severity: Error
Enforces: Service layer (src/services/**/*.ts) cannot import Node.js built-ins (node:*)
Why: Services should be platform-agnostic for testability and reusability.
Example violation:
// ❌ src/services/ConfigService.ts
import { readFileSync } from "node:fs"Fix:
// ✅ Use @effect/platform abstraction
import { FileSystem } from "@effect/platform"Docs: https://effect.website/docs/guides/platform/overview
Severity: Error
Enforces: Core logic (src/core/**/*.ts) cannot import @effect/platform-node
Why: Core modules should depend on platform-agnostic abstractions only.
Example violation:
// ❌ src/core/parser.ts
import { FileSystem } from "@effect/platform-node"Fix:
// ✅ Import from platform-agnostic package
import { FileSystem } from "@effect/platform"Docs: https://effect.website/docs/guides/platform/platform-specific
Severity: Warning
Enforces: Source files (src/**/*.ts) should not import fs/promises, node:fs/promises, or node:fs
Why: Direct filesystem imports couple code to Node.js. Use @effect/platform FileSystem service.
Fix: Use FileSystem from @effect/platform (see pattern rule example above)
Docs: https://effect.website/docs/guides/platform/file-system
Severity: Warning
Enforces: Source files (src/**/*.ts) should not import path or node:path
Why: Direct path imports couple code to Node.js. Use @effect/platform Path service for cross-platform path handling.
Example violation:
// ❌ Detected
import path from "path"
const joined = path.join("src", "index.ts")Fix:
// ✅ Use @effect/platform Path
import { Path } from "@effect/platform"
import * as Effect from "effect/Effect"
const program = Effect.gen(function* () {
const path = yield* Path.Path
const joined = path.join("src", "index.ts")
return joined
})Docs: https://effect.website/docs/guides/platform/path
The preset provides sensible defaults that are merged with your config:
{
paths: {
exclude: ["node_modules/**", "dist/**", ".next/**", "coverage/**", ".git/**", "build/**"]
}
}You can override or extend these:
export default {
version: 1,
presets: ["@effect-migrate/preset-basic"],
paths: {
exclude: ["vendor/**"] // Extends preset excludes
}
} satisfies Config| Rule ID | Type | Severity | Detects |
|---|---|---|---|
no-async-await |
pattern | warning | async functions |
no-new-promise |
pattern | warning | new Promise() constructor |
no-try-catch |
pattern | warning | try/catch blocks |
no-barrel-import-effect |
pattern | warning | import from "effect" |
no-fs-promises |
pattern | warning | import from "fs/promises" |
no-node-in-services |
boundary | error | node:* in src/services/**/* |
no-platform-node-in-core |
boundary | error | @effect/platform-node in core |
no-fs-promises (boundary) |
boundary | warning | fs/promises in src/**/* |
no-node-path |
boundary | warning | node:path in src/**/* |
To disable specific preset rules, filter them out after loading:
import { presetBasic } from "@effect-migrate/preset-basic"
export default {
version: 1,
presets: [], // Don't use preset field
// Manually load and filter
patterns: presetBasic.rules
.filter((rule) => rule.kind === "pattern" && rule.id !== "no-barrel-import-effect")
.map((rule) => ({
id: rule.id
// ... convert to config format
}))
} satisfies ConfigOr use @effect-migrate-ignore comments in your code to suppress specific violations:
// @effect-migrate-ignore
async function legacyFunction() {
// This won't trigger no-async-await
}To create your own preset, export an object matching the Preset interface:
import type { Preset } from "@effect-migrate/core"
export const myPreset: Preset = {
rules: [
// Your custom rules
],
defaults: {
paths: {
exclude: ["vendor/**"]
}
}
}
export default myPresetMIT