Skip to content

Commit 600f3f2

Browse files
patricktreeclaude
andauthored
fix(file-extension-in-import): handle files with dots in basename (#506)
Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent 754a1a6 commit 600f3f2

File tree

7 files changed

+98
-7
lines changed

7 files changed

+98
-7
lines changed

lib/rules/file-extension-in-import.js

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@ const visitImport = require("../util/visit-import")
1313
/**
1414
* Get all file extensions of the files which have the same basename.
1515
* @param {string} filePath The path to the original file to check.
16+
* @param {string} [basename] Optional basename to use instead of deriving from filePath.
1617
* @returns {string[]} File extensions.
1718
*/
18-
function getExistingExtensions(filePath) {
19+
function getExistingExtensions(filePath, basename) {
1920
const directory = path.dirname(filePath)
20-
const extension = path.extname(filePath)
21-
const basename = path.basename(filePath, extension)
21+
if (basename == null) {
22+
const extension = path.extname(filePath)
23+
basename = path.basename(filePath, extension)
24+
}
2225

2326
try {
2427
return fs
@@ -115,17 +118,63 @@ module.exports = {
115118

116119
// Get extension.
117120
const currentExt = path.extname(name)
118-
const actualExt = path.extname(filePath)
121+
let actualExt = path.extname(filePath)
122+
let actualFilePath = filePath
123+
124+
// If the file doesn't exist (or is a directory), the resolver may have returned
125+
// a fallback path. In this case, path.extname may return a "fake" extension
126+
// (e.g., ".client" for "utils.client" when the actual file is "utils.client.ts").
127+
// We need to search for the actual file using the full basename from the import.
128+
const fileExists =
129+
fs.existsSync(filePath) && fs.statSync(filePath).isFile()
130+
if (actualExt !== "" && !fileExists) {
131+
// Use the full basename (e.g., "utils.client") since what path.extname
132+
// thinks is an extension (e.g., ".client") may not be a real file extension
133+
const importBasename = path.basename(name)
134+
const extensions = getExistingExtensions(
135+
filePath,
136+
importBasename
137+
)
138+
// Find the preferred extension based on tryExtensions order
139+
const preferred = tryExtensions.find(ext =>
140+
extensions.includes(ext)
141+
)
142+
const foundExt = preferred ?? extensions[0]
143+
if (foundExt) {
144+
actualExt = foundExt
145+
actualFilePath = path.join(
146+
path.dirname(filePath),
147+
`${importBasename}${actualExt}`
148+
)
149+
}
150+
}
151+
152+
let isDirectoryImport = false
153+
154+
// Check for directory imports. This handles both:
155+
// 1. Normal case: "./my-folder" -> "./my-folder/index.js"
156+
// 2. Dot-in-name case: "./my-things.client" -> "./my-things.client/index.js"
157+
// where path.extname incorrectly sees ".client" as an extension
158+
const isDirectory =
159+
fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()
160+
if (isDirectory) {
161+
const indexExt = getIndexExtension(filePath, tryExtensions)
162+
if (indexExt) {
163+
isDirectoryImport = true
164+
actualExt = indexExt
165+
actualFilePath = path.join(filePath, `index${indexExt}`)
166+
}
167+
}
168+
119169
const style = overrideStyle[actualExt] || defaultStyle
120170

121171
let expectedExt = convertTsExtensionToJs(
122172
context,
123-
filePath,
173+
actualFilePath,
124174
actualExt
125175
)
126-
let isDirectoryImport = false
127176

128-
if (currentExt === "" && actualExt === "") {
177+
if (currentExt === "" && actualExt === "" && !isDirectoryImport) {
129178
const indexExt = getIndexExtension(filePath, tryExtensions)
130179
if (indexExt) {
131180
isDirectoryImport = true
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// Fixture: directory with dot in name (my-things.client/index.js)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Fixture: directory with dot in name (my-things.client/index.ts)
2+
export const bar = "baz"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// Fixture file with dot in basename (util.client.js)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// Fixture file with dot in basename (util.client.ts)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Fixture file with dot in basename (utils.client.ts)
2+
export const foo = "bar"

tests/lib/rules/file-extension-in-import.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,41 @@ new RuleTester({
354354
errors: [{ messageId: "forbidExt", data: { ext: ".json" } }],
355355
},
356356

357+
// Files with dots in basename (e.g. utils.client.ts)
358+
// The specifier "utils.client" looks like it has an extension but doesn't
359+
// Rule should require the actual extension ".js" (mapped from .ts)
360+
{
361+
filename: fixture("test.ts"),
362+
code: "import './utils.client'",
363+
output: "import './utils.client.js'",
364+
errors: [{ messageId: "requireExt", data: { ext: ".js" } }],
365+
},
366+
{
367+
filename: fixture("test.ts"),
368+
code: "import './util.client'",
369+
output: "import './util.client.js'",
370+
errors: [{ messageId: "requireExt", data: { ext: ".js" } }],
371+
},
372+
{
373+
filename: fixture("test.js"),
374+
code: "import './util.client'",
375+
output: "import './util.client.js'",
376+
errors: [{ messageId: "requireExt", data: { ext: ".js" } }],
377+
},
378+
// Directories with dots in name (e.g. my-things.client/index.ts)
379+
{
380+
filename: fixture("test.ts"),
381+
code: "import './my-things.client'",
382+
output: "import './my-things.client/index.js'",
383+
errors: [{ messageId: "requireExt", data: { ext: ".js" } }],
384+
},
385+
{
386+
filename: fixture("test.js"),
387+
code: "import './my-things.client'",
388+
output: "import './my-things.client/index.js'",
389+
errors: [{ messageId: "requireExt", data: { ext: ".js" } }],
390+
},
391+
357392
// import()
358393
...(DynamicImportSupported
359394
? [

0 commit comments

Comments
 (0)