Skip to content

Commit d1c30cf

Browse files
committed
validate imports properly
1 parent 5afa64c commit d1c30cf

15 files changed

Lines changed: 138 additions & 57 deletions

File tree

packages/start/scripts/validate-imports.js

Lines changed: 106 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* Features:
1414
* - Scans all .ts and .tsx files in the src/ directory recursively
1515
* - Detects relative imports (starting with ./ or ../) without extensions
16+
* - Prevents .js/.jsx imports in TypeScript files (should use .ts/.tsx)
1617
* - Ignores commented code and CSS imports (handled by bundlers)
1718
* - Provides detailed error reporting with line numbers and suggestions
1819
* - Exits with appropriate status codes for CI/CD integration
@@ -24,13 +25,17 @@
2425
* 0 - All relative imports have valid extensions
2526
* 1 - Found relative imports without extensions
2627
*
27-
* Valid extensions: .ts, .tsx, .js, .jsx, .json
28+
* Valid extensions for TypeScript files: .ts, .tsx, .json
29+
* Invalid extensions for TypeScript files: .js, .jsx (use .ts/.tsx instead)
2830
*
2931
* @example
3032
* // ❌ Invalid - missing extension
3133
* import { config } from "./config/index";
3234
*
33-
* // ✅ Valid - has extension
35+
* // ❌ Invalid - .js extension in TypeScript file
36+
* import { config } from "./config/index.js";
37+
*
38+
* // ✅ Valid - has .ts extension
3439
* import { config } from "./config/index.ts";
3540
*
3641
* // ✅ Valid - external package import (no extension needed)
@@ -46,7 +51,8 @@ const __dirname = dirname(__filename);
4651

4752
// Configuration
4853
const SRC_DIR = join(__dirname, "..", "src");
49-
const VALID_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".json"];
54+
const VALID_EXTENSIONS = [".ts", ".tsx", ".json"];
55+
const INVALID_EXTENSIONS = [".js", ".jsx"];
5056
const FILE_EXTENSIONS = [".ts", ".tsx"];
5157

5258
/**
@@ -87,6 +93,13 @@ function hasValidExtension(path) {
8793
return VALID_EXTENSIONS.some(ext => path.endsWith(ext));
8894
}
8995

96+
/**
97+
* Check if an import path has an invalid extension for TypeScript files
98+
*/
99+
function hasInvalidExtension(path) {
100+
return INVALID_EXTENSIONS.some(ext => path.endsWith(ext));
101+
}
102+
90103
/**
91104
* Extract import/export statements from file content
92105
* Matches both import and export statements with from clauses
@@ -154,13 +167,24 @@ function validateFile(filePath) {
154167
const statements = extractImportExportStatements(content, filePath);
155168

156169
statements.forEach(statement => {
157-
if (isRelativeImport(statement.path) && !hasValidExtension(statement.path)) {
158-
errors.push({
159-
file: filePath,
160-
line: statement.line,
161-
importPath: statement.path,
162-
fullLine: statement.fullLine
163-
});
170+
if (isRelativeImport(statement.path)) {
171+
if (hasInvalidExtension(statement.path)) {
172+
errors.push({
173+
file: filePath,
174+
line: statement.line,
175+
importPath: statement.path,
176+
fullLine: statement.fullLine,
177+
type: "invalid-extension"
178+
});
179+
} else if (!hasValidExtension(statement.path)) {
180+
errors.push({
181+
file: filePath,
182+
line: statement.line,
183+
importPath: statement.path,
184+
fullLine: statement.fullLine,
185+
type: "missing-extension"
186+
});
187+
}
164188
}
165189
});
166190
} catch (error) {
@@ -203,26 +227,85 @@ function validateImports() {
203227
console.log("✅ All relative imports have valid extensions!");
204228
process.exit(0);
205229
} else {
206-
console.log(`❌ Found ${totalErrors} relative import(s) without extensions:\n`);
207-
208-
errorsByFile.forEach((errors, file) => {
209-
const relativePath = file.replace(process.cwd(), "").replace(/^\//, "");
210-
console.log(`📄 ${relativePath}:`);
230+
const missingExtensionErrors = [];
231+
const invalidExtensionErrors = [];
211232

233+
errorsByFile.forEach(errors => {
212234
errors.forEach(error => {
213-
console.log(` Line ${error.line}: ${error.importPath}`);
214-
console.log(` ${error.fullLine}`);
215-
console.log(
216-
` ${"".padStart(error.fullLine.indexOf(error.importPath), " ")}${"".padStart(error.importPath.length, "^")}`
217-
);
235+
if (error.type === "missing-extension") {
236+
missingExtensionErrors.push(error);
237+
} else if (error.type === "invalid-extension") {
238+
invalidExtensionErrors.push(error);
239+
}
218240
});
219-
console.log("");
220241
});
221242

243+
if (missingExtensionErrors.length > 0) {
244+
console.log(
245+
`❌ Found ${missingExtensionErrors.length} relative import(s) without extensions:\n`
246+
);
247+
248+
const missingByFile = new Map();
249+
missingExtensionErrors.forEach(error => {
250+
if (!missingByFile.has(error.file)) {
251+
missingByFile.set(error.file, []);
252+
}
253+
missingByFile.get(error.file).push(error);
254+
});
255+
256+
missingByFile.forEach((errors, file) => {
257+
const relativePath = file.replace(process.cwd(), "").replace(/^\//, "");
258+
console.log(`📄 ${relativePath}:`);
259+
260+
errors.forEach(error => {
261+
console.log(` Line ${error.line}: ${error.importPath}`);
262+
console.log(` ${error.fullLine}`);
263+
console.log(
264+
` ${"".padStart(error.fullLine.indexOf(error.importPath), " ")}${"".padStart(error.importPath.length, "^")}`
265+
);
266+
});
267+
console.log("");
268+
});
269+
}
270+
271+
if (invalidExtensionErrors.length > 0) {
272+
console.log(
273+
`❌ Found ${invalidExtensionErrors.length} relative import(s) with invalid extensions:\n`
274+
);
275+
276+
const invalidByFile = new Map();
277+
invalidExtensionErrors.forEach(error => {
278+
if (!invalidByFile.has(error.file)) {
279+
invalidByFile.set(error.file, []);
280+
}
281+
invalidByFile.get(error.file).push(error);
282+
});
283+
284+
invalidByFile.forEach((errors, file) => {
285+
const relativePath = file.replace(process.cwd(), "").replace(/^\//, "");
286+
console.log(`📄 ${relativePath}:`);
287+
288+
errors.forEach(error => {
289+
console.log(` Line ${error.line}: ${error.importPath}`);
290+
console.log(` ${error.fullLine}`);
291+
console.log(
292+
` ${"".padStart(error.fullLine.indexOf(error.importPath), " ")}${"".padStart(error.importPath.length, "^")}`
293+
);
294+
});
295+
console.log("");
296+
});
297+
}
298+
222299
console.log("💡 Tips:");
223-
console.log(' - Add file extensions to relative imports (e.g., "./file" → "./file.ts")');
300+
if (missingExtensionErrors.length > 0) {
301+
console.log(' - Add file extensions to relative imports (e.g., "./file" → "./file.ts")');
302+
}
303+
if (invalidExtensionErrors.length > 0) {
304+
console.log(" - Replace .js/.jsx with .ts/.tsx in TypeScript files");
305+
}
224306
console.log(" - This is required by Node.js ESM modules");
225-
console.log(" - Valid extensions: " + VALID_EXTENSIONS.join(", "));
307+
console.log(" - Valid extensions for TypeScript files: " + VALID_EXTENSIONS.join(", "));
308+
console.log(" - Invalid extensions for TypeScript files: " + INVALID_EXTENSIONS.join(", "));
226309

227310
process.exit(1);
228311
}

packages/start/src/client/StartClient.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @refresh skip
22
import App from "#start/app";
33
import type { JSX } from "solid-js";
4-
import { ErrorBoundary } from "../shared/ErrorBoundary.jsx";
4+
import { ErrorBoundary } from "../shared/ErrorBoundary.tsx";
55

66
function Dummy(props: { children: JSX.Element }) {
77
return props.children;
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
export { mount } from "./mount.js";
2-
export { StartClient, StartClientTanstack } from "./StartClient.jsx";
3-
1+
export { mount } from "./mount.ts";
2+
export { StartClient, StartClientTanstack } from "./StartClient.tsx";

packages/start/src/client/spa/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ export function mount(fn: () => JSX.Element, el: MountableElement) {
66
render(fn, el);
77
}
88

9-
export { StartClient } from "../StartClient.jsx";
9+
export { StartClient } from "../StartClient.tsx";

packages/start/src/router.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { getManifest } from "solid-start:get-manifest";
22
import { getRequestEvent, isServer } from "solid-js/web";
33

4-
import lazyRoute from "./server/lazyRoute.jsx";
5-
import { pageRoutes as routeConfigs } from "./server/routes.js";
6-
import type { PageEvent } from "./server/types.js";
4+
import lazyRoute from "./server/lazyRoute.tsx";
5+
import { pageRoutes as routeConfigs } from "./server/routes.ts";
6+
import type { PageEvent } from "./server/types.ts";
77

88
export function createRoutes() {
99
function createRoute(route: any) {

packages/start/src/server/StartServer.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import {
1010
useAssets
1111
} from "solid-js/web";
1212

13-
import { ErrorBoundary, TopErrorBoundary } from "../shared/ErrorBoundary.jsx";
14-
import { renderAsset } from "./renderAsset.jsx";
13+
import { ErrorBoundary, TopErrorBoundary } from "../shared/ErrorBoundary.tsx";
14+
import { renderAsset } from "./renderAsset.tsx";
1515
import type { Asset, DocumentComponentProps, PageEvent } from "./types.js";
16-
import { getSsrManifest } from "./manifest/ssr-manifest.js";
16+
import { getSsrManifest } from "./manifest/ssr-manifest.ts";
1717

1818
const docType = ssr("<!DOCTYPE html>");
1919

packages/start/src/server/handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { sharedConfig } from "solid-js";
1212
import { getRequestEvent, renderToStream, renderToString } from "solid-js/web";
1313
import { provideRequestEvent } from "solid-js/web/storage";
1414

15-
import { createRoutes } from "../router.jsx";
15+
import { createRoutes } from "../router.tsx";
1616
import { getFetchEvent } from "./fetchEvent.ts";
1717
import { getSsrManifest } from "./manifest/ssr-manifest.ts";
1818
import { matchAPIRoute } from "./routes.ts";

packages/start/src/server/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
export { getServerFunctionMeta } from "../shared/serverFunction.js";
2-
export { StartServer } from "./StartServer.jsx";
3-
export { createHandler } from "./handler.js";
1+
export { getServerFunctionMeta } from "../shared/serverFunction.ts";
2+
export { StartServer } from "./StartServer.tsx";
3+
export { createHandler } from "./handler.ts";
44

55
/**
66
* Checks if user has set a redirect status in the response.

packages/start/src/server/lazyRoute.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type Component, createComponent, type JSX, lazy, onCleanup } from "solid-js";
22

3-
import { type Asset, renderAsset } from "./renderAsset.jsx";
3+
import { type Asset, renderAsset } from "./renderAsset.tsx";
44

55
export default function lazyRoute<T extends Record<string, any>>(
66
component: { src: string; import(): Promise<Record<string, Component>> },

packages/start/src/server/manifest/prod-ssr-manifest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { clientViteManifest } from "solid-start:client-vite-manifest";
22
import { join } from "pathe";
33
import { CLIENT_BASE_PATH } from "../../constants.ts";
4-
import type { Asset } from "../renderAsset.jsx";
4+
import type { Asset } from "../renderAsset.tsx";
55

66
// Only reads from client manifest atm, might need server support for islands
77
export function getSsrProdManifest() {

0 commit comments

Comments
 (0)