@effect-migrate/preset-basic
v0.3.2
Published
Basic preset for Effect migration patterns
Downloads
26
Readme
@effect-migrate/preset-basic
Default Effect migration rules for detecting legacy patterns and enforcing architectural boundaries.
Installation
pnpm add -D @effect-migrate/preset-basicUsage
Add 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.
What's Included
Pattern Rules (5 rules)
Pattern rules detect legacy code patterns that should be migrated to Effect equivalents.
no-async-await
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
})no-new-promise
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))
})no-try-catch
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
})no-barrel-import-effect
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"no-fs-promises
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 (4 rules)
Boundary rules enforce architectural constraints to maintain clean separation between Effect and platform-specific code.
no-node-in-services
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
no-platform-node-in-core
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
no-fs-promises (boundary)
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
no-node-path
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
Config Defaults
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 ConfigRule Summary
| 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/**/* |
Disabling Rules
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
}Creating Custom Presets
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 myPresetLicense
MIT
