1import rule from "@effect/oxc/oxlint/rules/no-js-extension-imports"
2import type { Fix, Visitor } from "@oxlint/plugins"
3import { describe, expect, it } from "vitest"
4import { runRule } from "./utils.ts"
5
6describe("no-js-extension-imports", () => {
7 const createImportDeclaration = (source: string) => ({
8 type: "ImportDeclaration",
9 source: { value: source, range: [8, 8 + source.length + 2] as [number, number] },
10 range: [0, 50] as [number, number]
11 })
12
13 const createExportAllDeclaration = (source: string) => ({
14 type: "ExportAllDeclaration",
15 source: { value: source, range: [14, 14 + source.length + 2] as [number, number] },
16 range: [0, 50] as [number, number]
17 })
18
19 const createExportNamedDeclaration = (source: string) => ({
20 type: "ExportNamedDeclaration",
21 source: { value: source, range: [9, 9 + source.length + 2] as [number, number] },
22 range: [0, 50] as [number, number]
23 })
24
25 const applyFix = (visitor: keyof Visitor, node: unknown): Fix => {
26 const errors = runRule(rule, visitor, node)
27 expect(errors).toHaveLength(1)
28 return errors[0].fix!({ replaceTextRange: (range, text) => ({ range, text }) })
29 }
30
31 describe("fixes", () => {
32 const declarations = [
33 ["ImportDeclaration", "import "],
34 ["ExportAllDeclaration", "export * from "],
35 ["ExportNamedDeclaration", "export { value } from "]
36 ] as const
37
38 it.each(declarations)("escapes the replacement in %s", (visitor, prefix) => {
39 const value = "./say\"hello.js"
40 const source = `${prefix}'${value}'`
41 const node = { source: { value, range: [prefix.length, source.length] } }
42 const fix = applyFix(visitor, node)
43 expect(fix.range).toEqual([prefix.length, source.length])
44 expect(Function(`return ${fix.text}`)()).toBe("./say\"hello.ts")
45 })
46
47 it.each([
48 ["./back\\file.js", "./back\\file.ts"],
49 ["./a\nb.js", "./a\nb.ts"],
50 ["./plain.js", "./plain.ts"],
51 ["./plain.jsx", "./plain.tsx"],
52 ["./plain.mjs", "./plain.mts"],
53 ["./plain.cjs", "./plain.cts"],
54 ["./say'hello.js", "./say'hello.ts"]
55 ])("preserves the decoded specifier for %s", (value, expected) => {
56 const fix = applyFix("ImportDeclaration", createImportDeclaration(value))
57 expect(Function(`return ${fix.text}`)()).toBe(expected)
58 })
59 })
60
61 describe("ImportDeclaration", () => {
62 it("should report .js extension in relative imports", () => {
63 const node = createImportDeclaration("./foo.js")
64 const errors = runRule(rule, "ImportDeclaration", node)
65 expect(errors).toHaveLength(1)
66 expect(errors[0].message).toBe(`Use ".ts" extension instead of ".js" for relative imports`)
67 })
68
69 it("should report .jsx extension in relative imports", () => {
70 const node = createImportDeclaration("./component.jsx")
71 const errors = runRule(rule, "ImportDeclaration", node)
72 expect(errors).toHaveLength(1)
73 expect(errors[0].message).toBe(`Use ".tsx" extension instead of ".jsx" for relative imports`)
74 })
75
76 it("should report .mjs extension in relative imports", () => {
77 const node = createImportDeclaration("../utils.mjs")
78 const errors = runRule(rule, "ImportDeclaration", node)
79 expect(errors).toHaveLength(1)
80 expect(errors[0].message).toBe(`Use ".mts" extension instead of ".mjs" for relative imports`)
81 })
82
83 it("should report .cjs extension in relative imports", () => {
84 const node = createImportDeclaration("./config.cjs")
85 const errors = runRule(rule, "ImportDeclaration", node)
86 expect(errors).toHaveLength(1)
87 expect(errors[0].message).toBe(`Use ".cts" extension instead of ".cjs" for relative imports`)
88 })
89
90 it("should not report .ts extension in relative imports", () => {
91 const node = createImportDeclaration("./foo.ts")
92 const errors = runRule(rule, "ImportDeclaration", node)
93 expect(errors).toHaveLength(0)
94 })
95
96 it("should not report .tsx extension in relative imports", () => {
97 const node = createImportDeclaration("./component.tsx")
98 const errors = runRule(rule, "ImportDeclaration", node)
99 expect(errors).toHaveLength(0)
100 })
101
102 it("should not report for package imports with .js extension", () => {
103 const node = createImportDeclaration("some-package/utils.js")
104 const errors = runRule(rule, "ImportDeclaration", node)
105 expect(errors).toHaveLength(0)
106 })
107
108 it("should not report for bare package imports", () => {
109 const node = createImportDeclaration("effect")
110 const errors = runRule(rule, "ImportDeclaration", node)
111 expect(errors).toHaveLength(0)
112 })
113
114 it("should handle deeply nested relative imports", () => {
115 const node = createImportDeclaration("../../lib/utils.js")
116 const errors = runRule(rule, "ImportDeclaration", node)
117 expect(errors).toHaveLength(1)
118 expect(errors[0].message).toBe(`Use ".ts" extension instead of ".js" for relative imports`)
119 })
120 })
121
122 describe("ExportAllDeclaration", () => {
123 it("should report .js extension in relative re-exports", () => {
124 const node = createExportAllDeclaration("./foo.js")
125 const errors = runRule(rule, "ExportAllDeclaration", node)
126 expect(errors).toHaveLength(1)
127 expect(errors[0].message).toBe(`Use ".ts" extension instead of ".js" for relative imports`)
128 })
129
130 it("should not report .ts extension in relative re-exports", () => {
131 const node = createExportAllDeclaration("./foo.ts")
132 const errors = runRule(rule, "ExportAllDeclaration", node)
133 expect(errors).toHaveLength(0)
134 })
135 })
136
137 describe("ExportNamedDeclaration", () => {
138 it("should report .js extension in relative named exports", () => {
139 const node = createExportNamedDeclaration("./foo.js")
140 const errors = runRule(rule, "ExportNamedDeclaration", node)
141 expect(errors).toHaveLength(1)
142 expect(errors[0].message).toBe(`Use ".ts" extension instead of ".js" for relative imports`)
143 })
144
145 it("should not report .ts extension in relative named exports", () => {
146 const node = createExportNamedDeclaration("./foo.ts")
147 const errors = runRule(rule, "ExportNamedDeclaration", node)
148 expect(errors).toHaveLength(0)
149 })
150
151 it("should handle export without source", () => {
152 const node = { type: "ExportNamedDeclaration", source: null }
153 const errors = runRule(rule, "ExportNamedDeclaration", node)
154 expect(errors).toHaveLength(0)
155 })
156 })
157})