You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
refactor: introduce an @ path alias to replace deep relative imports, enforced with eslint (#3210) - #3240
Adds a @/* root path alias, rewrites every out-of-subtree import to use it, and enforces the convention with ESLint.
Changes
tsconfig: adds "@/*": ["./*"] and drops baseUrl, so bare root imports like constants/routes no longer resolve. The unused images/* path is also removed.
Imports: 331 specifiers across 125 files now use @/… instead of ../.
@types → types: the directory is renamed so imports read @/types/network. A @types/* alias was rejected because it would shadow npm @types/ packages.
Lint rule (eslint.config.mjs): any .. segment in an import specifier is an error. One regex, PARENT_SEGMENT_PATTERN, covers:
static import, import type, export … from and export * (@typescript-eslint/no-restricted-imports)
dynamic import() and require(), with string or template-literal specifiers (no-restricted-syntax)
./ stays allowed for same-directory and descendant imports.
Decisions
Bare root imports: no lint rule. Without baseUrl they fail to compile, unless an installed npm package has the same name as a top-level folder (config, utils, …), in which case the package wins. No installed package does today. A rule that lists the top-level folders would need updating by hand, so we deliberately don't have one.
MDX: not linted. Every .mdx file is compiled at runtime by next-mdx-remoteserialize(), which can't resolve imports; components come in through the components prop. No .mdx file contains an import or export.
Prettier: prettier-plugin-organize-imports sorts @/ among scoped packages, and check-format passes.
Scripts: esrun honours tsconfig paths, so scripts/ uses the alias and is linted like everything else.
Verification
npm run lint (0 errors, 8 pre-existing warnings), npm run check-format and npx tsc --noEmit pass.
npm run build-dev:data-portal passes.
The rule was run against a sample file in .ts, .tsx, .js, .jsx, .mjs, .cjs and .styles.ts. It flags ../x, ../../x, ./a/../../x, .., export * from "../x", export { x } from "../x", import("../x"), import(`../x/${y}`), import(`${y}/../x`), require("../x") and require(`../x`). It allows ./x, @/x, ..foo, import(`./x/${y}`) and require(`./x/${y}`).
… enforced with eslint (#3210)
add the `@/*` root alias to tsconfig and the codemod that rewrites
out-of-subtree imports to use it. the rewrite itself lands in the next
commit so the script can be reviewed apart from its output.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AsKeyxfv4ZT3FdQaqtGBdV
… enforced with eslint (#3210)
rename `@types` to `types` so the alias reads as `@/types/...`, apply the
codemod (331 specifiers across 125 files), and add `no-restricted-imports`
so `../` and bare baseurl paths fail lint.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AsKeyxfv4ZT3FdQaqtGBdV
…ngs (#3210)
skip scoped packages so `@types/node` is never rewritten, normalise
`./../` traversal in the codemod, and cover `..`, `./..` and `import type`
in the lint rule via the typescript-eslint variant.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AsKeyxfv4ZT3FdQaqtGBdV
The bare-path deny list contains only directory/*, so a direct top-level barrel import such as from "components" is not restricted even though components/index.tsx is a valid baseUrl resolution. This leaves the new alias convention unenforced for exact top-level module names; include the exact names (or use one pattern covering both the name and its subpaths).
Codemod misses exact top-level barrel imports
scripts/codemod-path-alias.ts:83
The rest.length > 0 guard leaves a valid baseUrl import such as from "components" unchanged, even though it resolves to the root barrel at components/index.tsx. That means the codemod does not cover all baseUrl-relative imports; rewrite exact top-level directory names as well as their subpaths.
nested traversal such as `./components/../../../constants/routes` slipped
past patterns anchored to the start of the specifier.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AsKeyxfv4ZT3FdQaqtGBdV
Reject bare root imports for components and content
eslint.config.mjs:119
These patterns only cover descendants such as components/foo; they do not reject the valid bare root imports components and content, which resolve through components/index.tsx and content/index.tsx. A new import using either form would bypass the @/ convention, so add exact-root patterns as well as the wildcard patterns.
Rewrite bare root imports to the @/ alias
scripts/codemod-path-alias.ts:83
The rest.length > 0 guard leaves valid bare root imports such as components and content unchanged. Both directories have an index.tsx, so baseUrl: "." resolves these specifiers; the codemod therefore does not convert every bare baseUrl import to the required @/ form. Rewrite known top-level directory specifiers even when there is no slash (or distinguish packages from directories by checking their entrypoint).
…x file (#3210)
`components`, `content` and `pages` resolve bare through their index.tsx
under `baseUrl: "."`. the lint rule now lists them as anchored exact
patterns and the codemod rewrites them when an index file exists.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AsKeyxfv4ZT3FdQaqtGBdV
The reason will be displayed to describe this comment to others. Learn more.
Review of the @/ alias change. I found no correctness problems in the rewritten imports. The comments below cover gaps in the lint rule's coverage and some cleanups in the codemod. I couldn't run ESLint locally (wrong Node version), so the rule-coverage points come from reading typescript-eslint's documented behaviour, not from lint runs.
…the @ alias (#3210)
without baseurl every bare root path is a compile error, so the eslint
bare list goes. the images/* alias was dead. no-restricted-syntax now
catches `..` in dynamic import() and require(), and the rule covers js
files. the one-off codemod is removed; it stays in this pr's history.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AsKeyxfv4ZT3FdQaqtGBdV
I reviewed head d0f19986 and found no correctness bugs. tsc --noEmit passes, no old-style imports are left (../, bare-root, images/*, @types), and the new rule catches ../ in static and dynamic imports, export … from, export * and require().
Please fix
The PR description is out of date. It still describes scripts/codemod-path-alias.ts, npm run codemod:path-alias, a rule banning bare root imports and a two-commit layout. d0f19986 removed all of these.
Gaps in the lint rule
Template-literal imports get past it (eslint.config.mjs:123). The selectors only match a plain string, so import(`../constants/${name}`) and require(`../x`) go unflagged. I confirmed this with a test file. This one is cheap to close.
MDX isn't linted (eslint.config.mjs:101). .mdx content can contain imports, and import X from "../../components/Foo" in a description.mdx wouldn't be caught.
An npm package could silently take over a bare import (eslint.config.mjs:97). The comment says bare root imports fail to compile, but that only holds while no npm package shares a top-level folder name (config, utils, constants, common, types). Such a package would win silently, and the last commit removed the rule that would have caught this.
Items 2 and 3 may be fine to leave, as long as that's a deliberate choice.
Nits
The .. regex appears in two selectors and overlaps the glob patterns at line 111. It could be one constant next to RELATIVE_IMPORT_MESSAGE.
The new block repeats the files glob from the block above. A block with no files already applies to every linted file.
…lve parent imports (#3210)
share one parent-segment regex between the static and dynamic import checks,
drop the redundant files glob, and document why bare root imports are unguarded.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PR description: rewritten to match the current branch. It no longer mentions the codemod, npm run codemod:path-alias, the bare-import rule or the two-commit layout.
Gaps in the lint rule
Template-literal imports: fixed. Each static chunk of a template literal is now checked, so import(`../constants/${name}`) and require(`../x`) are flagged. While there I also closed three similar gaps: concatenated specifiers (import("../x/" + y)), require.resolve("../x") / require.context("../x"), and typeof import("../x") types.
MDX: left as is, on purpose. Every .mdx file is compiled at runtime by next-mdx-remoteserialize(), which can't resolve imports; components come in through the components prop. No .mdx file has an import or export today, and none sit under pages/, which is the only place @next/mdx would compile them.
npm package taking over a bare import: left as is, on purpose. I checked, and no installed package matches any of our top-level folder names today. A guard would mean keeping a list of top-level folders up to date by hand, which is why the earlier rule was dropped. The eslint comment now states this case and says it is deliberately unguarded.
Nits
The .. regex is now one constant, PARENT_SEGMENT_PATTERN, next to RELATIVE_IMPORT_MESSAGE. The static-import rule uses it through its regex option, replacing the overlapping globs, and so do the dynamic-import selectors.
Removed the repeated files glob. I confirmed the rule still applies to .ts, .tsx, .js, .jsx, .mjs, .cjs and .styles.ts.
I tested the rule on a sample file: every parent-import form above is flagged, while ./x, @/x, ..foo, import(`./x/${y}`) and typeof import("./x") are not. Lint (0 errors), prettier, tsc --noEmit and build-dev:data-portal pass.
The reason will be displayed to describe this comment to others. Learn more.
Approving with feedback. I checked all 357 rewritten specifiers across 129 files and each resolves to the same file as before. tsc --noEmit passes and eslint . gives 0 errors and 8 warnings, matching the description. scripts/ still resolves @/ via esrun's esbuild.
The inline comments are all minor and about the new lint rule in eslint.config.mjs: false positives on non-specifier .. strings, a comment that overstates concatenation coverage, and flat-config override behavior worth guarding against. None of them block merging.
replace the no-restricted-syntax selectors with a custom rule that joins the
static text of the specifier argument, so strings elsewhere in the call are
ignored and concatenated specifiers are caught. note that the block must stay
last for no-restricted-imports.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #3210
Adds a
@/*root path alias, rewrites every out-of-subtree import to use it, and enforces the convention with ESLint.Changes
tsconfig: adds
"@/*": ["./*"]and dropsbaseUrl, so bare root imports likeconstants/routesno longer resolve. The unusedimages/*path is also removed.Imports: 331 specifiers across 125 files now use
@/…instead of../.@types→types: the directory is renamed so imports read@/types/network. A@types/*alias was rejected because it would shadow npm@types/packages.Lint rule (
eslint.config.mjs): any..segment in an import specifier is an error. One regex,PARENT_SEGMENT_PATTERN, covers:import,import type,export … fromandexport *(@typescript-eslint/no-restricted-imports)import()andrequire(), with string or template-literal specifiers (no-restricted-syntax)./stays allowed for same-directory and descendant imports.Decisions
baseUrlthey fail to compile, unless an installed npm package has the same name as a top-level folder (config,utils, …), in which case the package wins. No installed package does today. A rule that lists the top-level folders would need updating by hand, so we deliberately don't have one..mdxfile is compiled at runtime bynext-mdx-remoteserialize(), which can't resolve imports; components come in through thecomponentsprop. No.mdxfile contains animportorexport.prettier-plugin-organize-importssorts@/among scoped packages, andcheck-formatpasses.paths, soscripts/uses the alias and is linted like everything else.Verification
npm run lint(0 errors, 8 pre-existing warnings),npm run check-formatandnpx tsc --noEmitpass.npm run build-dev:data-portalpasses..ts,.tsx,.js,.jsx,.mjs,.cjsand.styles.ts. It flags../x,../../x,./a/../../x,..,export * from "../x",export { x } from "../x",import("../x"),import(`../x/${y}`),import(`${y}/../x`),require("../x")andrequire(`../x`). It allows./x,@/x,..foo,import(`./x/${y}`)andrequire(`./x/${y}`).🤖 Generated with Claude Code