@dvukovic/style-guide 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/eslint/plugins/dvukovic.d.ts +2 -0
- package/dist/eslint/rules/nextjs-export-name/nextjs-export-name.d.ts +19 -0
- package/dist/eslint/rules/no-cross-module-imports/no-cross-module-imports.d.ts +30 -0
- package/dist/eslint/rules/no-instanceof-error/no-instanceof-error.d.ts +16 -0
- package/package.json +1 -1
- package/src/cli/generators/eslint.js +2 -0
- package/src/cli/generators/scripts.js +2 -0
- package/src/eslint/configs/next.js +2 -1
- package/src/eslint/plugins/dvukovic.js +27 -7
- package/src/eslint/rules/nextjs-export-name/nextjs-export-name.js +130 -0
- package/src/eslint/rules/no-cross-module-imports/no-cross-module-imports.js +110 -0
- package/src/eslint/rules/no-instanceof-error/no-instanceof-error.js +36 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export namespace nextjsExportName {
|
|
2
|
+
function create(context: any): {
|
|
3
|
+
ExportDefaultDeclaration?: undefined;
|
|
4
|
+
} | {
|
|
5
|
+
ExportDefaultDeclaration(node: any): void;
|
|
6
|
+
};
|
|
7
|
+
namespace meta {
|
|
8
|
+
namespace docs {
|
|
9
|
+
let description: string;
|
|
10
|
+
}
|
|
11
|
+
let fixable: string;
|
|
12
|
+
namespace messages {
|
|
13
|
+
let anonymous: string;
|
|
14
|
+
let mismatch: string;
|
|
15
|
+
}
|
|
16
|
+
let schema: never[];
|
|
17
|
+
let type: string;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export namespace noCrossModuleImports {
|
|
2
|
+
function create(context: any): {
|
|
3
|
+
CallExpression?: undefined;
|
|
4
|
+
ImportDeclaration?: undefined;
|
|
5
|
+
ImportExpression?: undefined;
|
|
6
|
+
} | {
|
|
7
|
+
CallExpression(node: any): void;
|
|
8
|
+
ImportDeclaration(node: any): void;
|
|
9
|
+
ImportExpression(node: any): void;
|
|
10
|
+
};
|
|
11
|
+
namespace meta {
|
|
12
|
+
namespace docs {
|
|
13
|
+
let description: string;
|
|
14
|
+
}
|
|
15
|
+
namespace messages {
|
|
16
|
+
let noCrossModuleImports: string;
|
|
17
|
+
}
|
|
18
|
+
let schema: {
|
|
19
|
+
additionalProperties: boolean;
|
|
20
|
+
properties: {
|
|
21
|
+
modulesDirectory: {
|
|
22
|
+
default: string;
|
|
23
|
+
type: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
type: string;
|
|
27
|
+
}[];
|
|
28
|
+
let type: string;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export namespace noInstanceofError {
|
|
2
|
+
function create(context: any): {
|
|
3
|
+
BinaryExpression(node: any): void;
|
|
4
|
+
};
|
|
5
|
+
namespace meta {
|
|
6
|
+
namespace docs {
|
|
7
|
+
let description: string;
|
|
8
|
+
}
|
|
9
|
+
let fixable: string;
|
|
10
|
+
namespace messages {
|
|
11
|
+
let forbidden: string;
|
|
12
|
+
}
|
|
13
|
+
let schema: never[];
|
|
14
|
+
let type: string;
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -26,11 +26,13 @@ export function generateScripts(tools, packageManager) {
|
|
|
26
26
|
if (tools.includes("cspell")) {
|
|
27
27
|
scripts["lint:cspell"] = "cspell --no-progress --no-summary --unique '**'"
|
|
28
28
|
lintParts.push(`${runner} lint:cspell`)
|
|
29
|
+
fixParts.push(`${runner} lint:cspell`)
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
if (tools.includes("knip")) {
|
|
32
33
|
scripts["lint:knip"] = "knip --cache"
|
|
33
34
|
lintParts.push(`${runner} lint:knip`)
|
|
35
|
+
fixParts.push(`${runner} lint:knip`)
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
if (lintParts.length > 0) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ALL_JS_TS_FILES } from "../file-patterns.js"
|
|
2
|
+
import { dvukovicNextjs } from "../plugins/dvukovic.js"
|
|
2
3
|
import { next as nextPlugin } from "../plugins/next.js"
|
|
3
4
|
|
|
4
|
-
export const nextConfig = [nextPlugin]
|
|
5
|
+
export const nextConfig = [nextPlugin, dvukovicNextjs]
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Next.js framework configuration
|
|
@@ -1,21 +1,41 @@
|
|
|
1
1
|
import { documentTodos } from "../rules/document-todos/document-todos.js"
|
|
2
|
+
import { nextjsExportName } from "../rules/nextjs-export-name/nextjs-export-name.js"
|
|
2
3
|
import { noCommentedOutCode } from "../rules/no-commented-out-code/no-commented-out-code.js"
|
|
4
|
+
import { noCrossModuleImports } from "../rules/no-cross-module-imports/no-cross-module-imports.js"
|
|
5
|
+
import { noInstanceofError } from "../rules/no-instanceof-error/no-instanceof-error.js"
|
|
3
6
|
import { noT } from "../rules/no-t/no-t.js"
|
|
4
7
|
|
|
8
|
+
const dvukovicPlugin = {
|
|
9
|
+
rules: {
|
|
10
|
+
"document-todos": documentTodos,
|
|
11
|
+
"nextjs-export-name": nextjsExportName,
|
|
12
|
+
"no-commented-out-code": noCommentedOutCode,
|
|
13
|
+
"no-cross-module-imports": noCrossModuleImports,
|
|
14
|
+
"no-instanceof-error": noInstanceofError,
|
|
15
|
+
"no-t": noT,
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
/** @type {import("@eslint/config-helpers").Config} */
|
|
6
20
|
export const dvukovic = {
|
|
7
21
|
plugins: {
|
|
8
|
-
dvukovic:
|
|
9
|
-
rules: {
|
|
10
|
-
"document-todos": documentTodos,
|
|
11
|
-
"no-commented-out-code": noCommentedOutCode,
|
|
12
|
-
"no-t": noT,
|
|
13
|
-
},
|
|
14
|
-
},
|
|
22
|
+
dvukovic: dvukovicPlugin,
|
|
15
23
|
},
|
|
16
24
|
rules: {
|
|
17
25
|
"dvukovic/document-todos": ["error", { url: "http" }],
|
|
18
26
|
"dvukovic/no-commented-out-code": "error",
|
|
27
|
+
"dvukovic/no-instanceof-error": "error",
|
|
19
28
|
"dvukovic/no-t": "error",
|
|
20
29
|
},
|
|
21
30
|
}
|
|
31
|
+
|
|
32
|
+
/** @type {import("@eslint/config-helpers").Config} */
|
|
33
|
+
export const dvukovicNextjs = {
|
|
34
|
+
plugins: {
|
|
35
|
+
dvukovic: dvukovicPlugin,
|
|
36
|
+
},
|
|
37
|
+
rules: {
|
|
38
|
+
"dvukovic/nextjs-export-name": "error",
|
|
39
|
+
"dvukovic/no-cross-module-imports": "error",
|
|
40
|
+
},
|
|
41
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
|
|
3
|
+
const SPECIAL_FILES = new Map([
|
|
4
|
+
["page", "Page"],
|
|
5
|
+
["layout", "Layout"],
|
|
6
|
+
["loading", "Loading"],
|
|
7
|
+
["error", "Error"],
|
|
8
|
+
["not-found", "NotFound"],
|
|
9
|
+
["template", "Template"],
|
|
10
|
+
["default", "Default"],
|
|
11
|
+
["global-error", "GlobalError"],
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
function getExpectedName(filename) {
|
|
15
|
+
const basename = path.basename(filename)
|
|
16
|
+
const nameWithoutExtension = basename.replace(/\.[^.]+$/, "")
|
|
17
|
+
|
|
18
|
+
return SPECIAL_FILES.get(nameWithoutExtension) ?? null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const nextjsExportName = {
|
|
22
|
+
create(context) {
|
|
23
|
+
const expectedName = getExpectedName(context.filename)
|
|
24
|
+
|
|
25
|
+
if (!expectedName) {
|
|
26
|
+
return {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
ExportDefaultDeclaration(node) {
|
|
31
|
+
const declaration = node.declaration
|
|
32
|
+
|
|
33
|
+
if (
|
|
34
|
+
declaration.type === "FunctionDeclaration" ||
|
|
35
|
+
declaration.type === "ClassDeclaration"
|
|
36
|
+
) {
|
|
37
|
+
if (!declaration.id) {
|
|
38
|
+
context.report({
|
|
39
|
+
data: { expectedName },
|
|
40
|
+
messageId: "anonymous",
|
|
41
|
+
node,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (declaration.id.name !== expectedName) {
|
|
48
|
+
context.report({
|
|
49
|
+
data: {
|
|
50
|
+
actualName: declaration.id.name,
|
|
51
|
+
expectedName,
|
|
52
|
+
},
|
|
53
|
+
fix(fixer) {
|
|
54
|
+
return fixer.replaceText(declaration.id, expectedName)
|
|
55
|
+
},
|
|
56
|
+
messageId: "mismatch",
|
|
57
|
+
node,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (declaration.type === "Identifier") {
|
|
65
|
+
const scope = context.sourceCode.getScope(node)
|
|
66
|
+
const variable = scope.set.get(declaration.name)
|
|
67
|
+
|
|
68
|
+
if (!variable) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const definition = variable.defs[0]
|
|
73
|
+
|
|
74
|
+
if (!definition) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (declaration.name !== expectedName) {
|
|
79
|
+
context.report({
|
|
80
|
+
data: {
|
|
81
|
+
actualName: declaration.name,
|
|
82
|
+
expectedName,
|
|
83
|
+
},
|
|
84
|
+
fix(fixer) {
|
|
85
|
+
const replaced = new Set()
|
|
86
|
+
const fixes = []
|
|
87
|
+
|
|
88
|
+
if (definition.name) {
|
|
89
|
+
fixes.push(fixer.replaceText(definition.name, expectedName))
|
|
90
|
+
replaced.add(definition.name.range[0])
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const unreplacedReferences = variable.references.filter(
|
|
94
|
+
(reference) => {
|
|
95
|
+
return !replaced.has(reference.identifier.range[0])
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
for (const reference of unreplacedReferences) {
|
|
100
|
+
fixes.push(
|
|
101
|
+
fixer.replaceText(reference.identifier, expectedName),
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return fixes
|
|
106
|
+
},
|
|
107
|
+
messageId: "mismatch",
|
|
108
|
+
node,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
meta: {
|
|
116
|
+
docs: {
|
|
117
|
+
description:
|
|
118
|
+
"Enforces that default exports in Next.js special files match the expected PascalCase name derived from the filename.",
|
|
119
|
+
},
|
|
120
|
+
fixable: "code",
|
|
121
|
+
messages: {
|
|
122
|
+
anonymous:
|
|
123
|
+
"Default export in this file must be a named function. Expected name: `{{ expectedName }}`.",
|
|
124
|
+
mismatch:
|
|
125
|
+
"Default export `{{ actualName }}` does not match the expected name `{{ expectedName }}` for this file.",
|
|
126
|
+
},
|
|
127
|
+
schema: [],
|
|
128
|
+
type: "problem",
|
|
129
|
+
},
|
|
130
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MODULES_DIRECTORY = "src/modules"
|
|
4
|
+
|
|
5
|
+
function getModuleName(filePath, modulesDirectory) {
|
|
6
|
+
const normalizedPath = filePath.replaceAll("\\", "/")
|
|
7
|
+
const normalizedModulesDirectory = modulesDirectory.replaceAll("\\", "/")
|
|
8
|
+
|
|
9
|
+
const modulesIndex = normalizedPath.indexOf(`${normalizedModulesDirectory}/`)
|
|
10
|
+
|
|
11
|
+
if (modulesIndex === -1) {
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const afterModules = normalizedPath.slice(modulesIndex + normalizedModulesDirectory.length + 1)
|
|
16
|
+
|
|
17
|
+
const moduleName = afterModules.split("/")[0]
|
|
18
|
+
|
|
19
|
+
return moduleName ?? null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const noCrossModuleImports = {
|
|
23
|
+
create(context) {
|
|
24
|
+
const modulesDirectory = context.options[0]?.modulesDirectory ?? DEFAULT_MODULES_DIRECTORY
|
|
25
|
+
|
|
26
|
+
const currentModule = getModuleName(context.filename, modulesDirectory)
|
|
27
|
+
|
|
28
|
+
if (!currentModule) {
|
|
29
|
+
return {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function checkImportSource(node, source) {
|
|
33
|
+
if (!source || typeof source !== "string") {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!source.startsWith(".")) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const resolvedPath = path.resolve(path.dirname(context.filename), source)
|
|
42
|
+
const targetModule = getModuleName(resolvedPath, modulesDirectory)
|
|
43
|
+
|
|
44
|
+
if (!targetModule) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (targetModule !== currentModule) {
|
|
49
|
+
context.report({
|
|
50
|
+
data: {
|
|
51
|
+
currentModule,
|
|
52
|
+
targetModule,
|
|
53
|
+
},
|
|
54
|
+
messageId: "noCrossModuleImports",
|
|
55
|
+
node,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
CallExpression(node) {
|
|
62
|
+
// eslint-disable-next-line baseline-js/use-baseline -- false positive: AST node property, not Function.arguments
|
|
63
|
+
if (node.callee.name !== "require" || node.arguments.length === 0) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const argument = node.arguments[0] // eslint-disable-line baseline-js/use-baseline -- false positive: AST node property, not Function.arguments
|
|
68
|
+
|
|
69
|
+
if (argument.type !== "Literal" || typeof argument.value !== "string") {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
checkImportSource(node, argument.value)
|
|
74
|
+
},
|
|
75
|
+
ImportDeclaration(node) {
|
|
76
|
+
checkImportSource(node, node.source.value)
|
|
77
|
+
},
|
|
78
|
+
ImportExpression(node) {
|
|
79
|
+
if (node.source.type !== "Literal" || typeof node.source.value !== "string") {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
checkImportSource(node, node.source.value)
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
meta: {
|
|
88
|
+
docs: {
|
|
89
|
+
description:
|
|
90
|
+
"Prevents importing from one module into another inside the modules directory to enforce module isolation.",
|
|
91
|
+
},
|
|
92
|
+
messages: {
|
|
93
|
+
noCrossModuleImports:
|
|
94
|
+
'Importing from module "{{targetModule}}" is not allowed inside module "{{currentModule}}".',
|
|
95
|
+
},
|
|
96
|
+
schema: [
|
|
97
|
+
{
|
|
98
|
+
additionalProperties: false,
|
|
99
|
+
properties: {
|
|
100
|
+
modulesDirectory: {
|
|
101
|
+
default: DEFAULT_MODULES_DIRECTORY,
|
|
102
|
+
type: "string",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
type: "object",
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
type: "problem",
|
|
109
|
+
},
|
|
110
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const noInstanceofError = {
|
|
2
|
+
create(context) {
|
|
3
|
+
return {
|
|
4
|
+
BinaryExpression(node) {
|
|
5
|
+
if (
|
|
6
|
+
node.operator === "instanceof" &&
|
|
7
|
+
node.right.type === "Identifier" &&
|
|
8
|
+
node.right.name === "Error"
|
|
9
|
+
) {
|
|
10
|
+
context.report({
|
|
11
|
+
fix(fixer) {
|
|
12
|
+
const leftText = context.sourceCode.getText(node.left)
|
|
13
|
+
|
|
14
|
+
return fixer.replaceText(node, `Error.isError(${leftText})`)
|
|
15
|
+
},
|
|
16
|
+
messageId: "forbidden",
|
|
17
|
+
node,
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
meta: {
|
|
24
|
+
docs: {
|
|
25
|
+
description:
|
|
26
|
+
"Forbids `instanceof Error` in favor of `Error.isError()` which works across realms.",
|
|
27
|
+
},
|
|
28
|
+
fixable: "code",
|
|
29
|
+
messages: {
|
|
30
|
+
forbidden:
|
|
31
|
+
"Use `Error.isError()` instead of `instanceof Error`. It works across realms where `instanceof` fails.",
|
|
32
|
+
},
|
|
33
|
+
schema: [],
|
|
34
|
+
type: "problem",
|
|
35
|
+
},
|
|
36
|
+
}
|