fix: forbid relative .js imports in TS sources#1953
Conversation
Now that the `node` test target skips the build step (ipfs#1945, ipfs#1950) and runs `.ts` source directly via `--experimental-strip-types`, any relative `.js` import inside a `.ts` file fails at test-time because Node resolves the path literally. The new rule catches this at lint time. The selector matches only relative paths (`./` or `../`) ending in `.js`; bare-module imports like `@noble/curves/ed25519.js` are unaffected. BREAKING CHANGE: TS sources may not import relative `.js` paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The legacy `.js` import was a workaround for tsc not rewriting import specifiers. With the new lint rule that intent is no longer needed and the fixture's purpose (mixing .ts and .js files in one project) is preserved by the surrounding files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
achingbrain
left a comment
There was a problem hiding this comment.
I was looking for something like this, but as configured it doesn't allow importing .js files from .ts files at all - it should be allowed if they exist.
|
I'll check if there is a way to do this, my thought was just to add an ignore comment in those cases but thats not the best. |
The AST-only no-restricted-syntax check rejected every relative `.js` import; that's wrong for mixed-language projects where the `.js` file genuinely exists. The new aegir/no-legacy-js-import rule resolves the import target on disk and only flags when the `.js` is missing and a `.ts` sibling exists. The fix is autofixable. Lives at src/eslint/no-legacy-js-import.js with RuleTester coverage.
|
Updated per your feedback — new Covers RuleTester coverage in Open question: scoped to |
|
think the title could be changed to just |
Drop the custom no-legacy-js-import rule and its tests in favour of n/file-extension-in-import, which is bundled transitively via neostandard and does the same filesystem-aware extension check with broader maturity. The typescriptExtensionMap is set via settings instead of options so the v17 rule (shipped with neostandard) picks it up.
|
Realized One quirk: the v17 of Extensionless relative imports ( |
|
This picks up |
|
i think we should catch extensionless imports. i can add that change. |
The TS-context default tryExtensions in eslint-plugin-n is ['.js', '.json', '.node', '.mjs', '.cjs'] — no '.ts' — so a relative extensionless import like './foo' couldn't be resolved when only './foo.ts' existed on disk, and n/file-extension-in-import skipped silently. Adding '.ts' (and keeping the rest of the defaults) lets the resolver find those siblings so the rule reports and the autofix inserts the correct extension.
n/file-extension-in-import's built-in fixer always inserts the expected
extension before the closing quote. For extensionless imports that's
correct (./foo → ./foo.ts), but for a .js-aliased import resolving
through the backward extensionMap to a .ts sibling, it produces
./foo.js.ts — an unreachable path.
Wrap the upstream rule via Object.create(context, { report }) so that
when the import already carries a JS/TS extension we swap n's appender
for a replaceTextRange that overwrites the existing extension. Other
cases (extensionless, dot-in-name) fall through to the upstream fix
unchanged.
Registered under a small aegir-n plugin to keep n/'s namespace clean;
the rule reference is pulled from neostandard's already-loaded plugin
map so no new dependencies are added.
|
Follow-up: extensionless detection + autofix fix for Extensionless: added Autofix: Behavior:
Doesn't cover |
Summary
Since the
nodetest target skips the build step (#1945, #1950) and runs.tssource directly via--experimental-strip-types, any relative.jsimport inside a.tsfile fails at test-time — Node resolves the path literally and there's no.jsfile on disk.This rule catches that at lint time.
Rule
The selector matches only relative paths (
./or../) ending in.js:from '@noble/curves/ed25519.js'from './foo.ts'.tsfrom '../foo.js'import './foo.js'(side-effect)Fixture
test/fixtures/js+ts/src/another.tswas importing./typed.js(legacy convention from when tsc didn't rewrite specifiers). Updated to./typed.ts. The fixture's purpose — exercising mixed-language linting — is preserved by the project still containingtyped.ts,another.ts, andsome.js.Breaking change
Any
.tssource with a relative.jsimport will fail lint after this lands. Per-line// eslint-disable-next-line no-restricted-syntaxis the escape hatch for deliberate cases.