This monorepo uses TypeScript paths to provide clean, stable import aliases. Instead of relying on brittle relative paths like ../../utils/create-rule, use the aliases defined in each package's tsconfig.json.
| Alias | Target | Purpose |
|---|---|---|
@ |
./src |
Import from the current package's source directory. |
@/* |
./src/* |
Import a sub-module from the current package's source directory. |
# |
../.. |
Import from the workspace root (shared utilities, test helpers, etc.). |
#/* |
../../* |
Import a sub-module from the workspace root. |
Use @ whenever you need to reference a module inside the current package's src/ directory.
This eliminates the need to count ../ segments and makes refactorings (moving files between directories) much safer.
// ❌ Avoid — breaks when the file is moved
import { createRule } from "../../utils/create-rule";
// ✅ Preferred — stable regardless of file depth
import { createRule } from "@/utils/create-rule";In every package under packages/* and plugins/*, @ is mapped to that package's own src/ folder:
{
"compilerOptions": {
"paths": {
"@": ["./src"],
"@/*": ["./src/*"]
}
}
}Use # when you need to reference a module at the monorepo root (e.g. shared test helpers in test/, build scripts, or workspace-wide types).
// ❌ Avoid — fragile and hard to read
import { ruleTester } from "../../../../../test";
// ✅ Preferred — always points to the workspace root
import { ruleTester } from "#/test";In each package, # is mapped two levels up (../..) to reach the workspace root:
{
"compilerOptions": {
"paths": {
"#": ["../.."],
"#/*": ["../../*"]
}
}
}The workspace root tsconfig.json also defines # for root-level files:
{
"compilerOptions": {
"paths": {
"#": ["."],
"#/*": ["./*"]
}
}
}Every plugin and package must declare both aliases in its local tsconfig.json:
{
"extends": ["@local/configs/tsconfig.base.json"],
"compilerOptions": {
"paths": {
"@": ["./src"],
"#": ["../.."],
"@/*": ["./src/*"],
"#/*": ["../../*"]
}
},
"include": ["src"]
}Vitest is configured to resolve these aliases via resolve.tsconfigPaths: true in vitest.config.ts, so tests run with the same mappings as the TypeScript compiler.
// plugins/eslint-plugin-react-x/src/rules/no-missing-key/no-missing-key.ts
import { createRule } from "@/utils/create-rule";
import { getSettings } from "@/utils/get-settings";// plugins/eslint-plugin-react-x/src/rules/no-missing-key/no-missing-key.spec.ts
import { ruleTester } from "#/test";
import { createRule } from "@/utils/create-rule";// packages/ast/src/compare.ts
import { isTypeExpression } from "@/check";We follow the convention where @ represents the current package scope (similar to many Vite / Next.js setups) and # represents the workspace scope. This keeps the mental model simple:
@/*= "inside this package"#/*= "inside the whole repo"
Cross-package imports should still use the real package name (e.g. @eslint-react/ast, @eslint-react/core). paths aliases are only for intra-package and intra-workspace references that would otherwise require deep relative paths.
No — these aliases are for source code only. The bundler (tsdown) is configured to resolve and inline them during the build. Consumers of the published packages never see @/ or #/ imports.