File size: 5,081 Bytes
7357f85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import fs from "fs";
import path from "path";

export default {
	meta: {
		type: "suggestion",
		docs: {
			description: "Enforce file extensions in import statements",
		},
		fixable: "code",
		schema: [
			{
				type: "object",
				properties: {
					ignorePaths: {
						type: "array",
						items: { type: "string" },
					},
					includePaths: {
						type: "array",
						items: { type: "string" },
						description: "Path patterns to include (e.g., '$lib/')",
					},
					tsToJs: {
						type: "boolean",
						description: "Convert .ts files to .js when importing",
					},
					aliases: {
						type: "object",
						description: "Map of path aliases to their actual paths (e.g., {'$lib': 'src/lib'})",
					},
				},
				additionalProperties: false,
			},
		],
		messages: {
			missingExtension: "Import should include a file extension",
			noFileFound: "Import is missing extension and no matching file was found",
		},
	},
	create(context) {
		const options = context.options[0] || {};
		const ignorePaths = options.ignorePaths || [];
		const includePaths = options.includePaths || [];
		const tsToJs = options.tsToJs !== undefined ? options.tsToJs : true; // Default to true
		const aliases = options.aliases || {};

		// Get the project root directory
		const projectRoot = process.cwd();

		// Utility function to resolve file paths
		function resolveImportPath(importPath, currentFilePath) {
			// Handle relative paths
			if (importPath.startsWith("./") || importPath.startsWith("../")) {
				return path.resolve(path.dirname(currentFilePath), importPath);
			}

			// Handle aliased paths
			for (const [alias, aliasPath] of Object.entries(aliases)) {
				// Check if the import starts with this alias
				if (importPath === alias || importPath.startsWith(`${alias}/`)) {
					// Replace the alias with the actual path
					const relativePath = importPath === alias ? "" : importPath.slice(alias.length + 1); // +1 for the slash

					// Convert the aliasPath to an absolute path
					let absoluteAliasPath = aliasPath;
					if (!path.isAbsolute(absoluteAliasPath)) {
						absoluteAliasPath = path.resolve(projectRoot, aliasPath);
					}

					return path.join(absoluteAliasPath, relativePath);
				}
			}

			return null;
		}

		// Find the file extension by checking which file exists
		function findActualFile(basePath) {
			if (!basePath) return null;

			try {
				// Get the directory and base name
				const dir = path.dirname(basePath);
				const base = path.basename(basePath);

				// If the directory doesn't exist, return early
				if (!fs.existsSync(dir)) {
					return null;
				}

				// Read all files in the directory
				const files = fs.readdirSync(dir);

				// Look for files that match our base name plus any extension
				for (const file of files) {
					const fileParts = path.parse(file);

					// If we find a file that matches our base name
					if (fileParts.name === base) {
						// Handle TypeScript to JavaScript conversion
						if (tsToJs && fileParts.ext === ".ts") {
							return {
								actualPath: path.join(dir, file),
								importExt: ".js", // Import as .js even though it's a .ts file
							};
						}

						// Otherwise use the actual extension
						return {
							actualPath: path.join(dir, file),
							importExt: fileParts.ext,
						};
					}
				}
			} catch (error) {
				// If there's an error checking file existence, return null
				console.error("Error checking files:", error);
			}

			return null;
		}

		return {
			ImportDeclaration(node) {
				const source = node.source.value;

				// Check if it's a relative import or matches a manually specified include path
				const isRelativeImport = source.startsWith("./") || source.startsWith("../");
				const isAliasedPath = Object.keys(aliases).some(alias => source === alias || source.startsWith(`${alias}/`));
				const isIncludedPath = includePaths.some(pattern => source.startsWith(pattern));

				// Skip if it's not a relative import, aliased path, or included path
				if (!isRelativeImport && !isAliasedPath && !isIncludedPath) {
					return;
				}

				// Skip ignored paths
				if (ignorePaths.some(path => source.includes(path))) {
					return;
				}

				// Check if the import already has an extension
				const hasExtension = path.extname(source) !== "";
				if (!hasExtension) {
					// Get current file path to resolve the import
					const currentFilePath = context.getFilename();

					// Try to determine the correct file by checking what exists
					const resolvedPath = resolveImportPath(source, currentFilePath);
					const fileInfo = findActualFile(resolvedPath);

					context.report({
						node,
						messageId: fileInfo ? "missingExtension" : "noFileFound",
						fix(fixer) {
							// Only provide a fix if we found a file
							if (fileInfo) {
								// Replace the string literal with one that includes the extension
								return fixer.replaceText(node.source, `"${source}${fileInfo.importExt}"`);
							}

							// Otherwise, don't try to fix
							return null;
						},
					});
				}
			},
		};
	},
};