eslint-plugin-nextfriday 3.2.0 → 3.2.1
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/CHANGELOG.md +6 -0
- package/docs/rules/ENFORCE_CONSTANT_CASE.md +28 -17
- package/lib/index.cjs +14 -47
- package/lib/index.cjs.map +1 -1
- package/lib/index.js +14 -47
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# eslint-plugin-nextfriday
|
|
2
2
|
|
|
3
|
+
## 3.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#120](https://github.com/next-friday/eslint-plugin-nextfriday/pull/120) [`6f56995`](https://github.com/next-friday/eslint-plugin-nextfriday/commit/6f569958e538f23b699fa3ac1cb4743b87e4ab60) Thanks [@joetakara](https://github.com/joetakara)! - `enforce-constant-case` now only flags global `const` declarations bound to a magic number or magic text literal. Object literals, array literals, RegExp, template literals (static or dynamic), `as const` assertions, booleans, and any non-literal initializer are no longer checked. This unblocks Next.js App Router files where reserved exports like `metadata`, `viewport`, `dynamic`, `revalidate`, `runtime`, `fetchCache`, `dynamicParams`, `preferredRegion`, and `maxDuration` are framework-owned and must keep their exact names. The rule scope now matches the documented intent of the plugin's naming convention skill ("top-level constant primitive values") instead of every static-shaped initializer.
|
|
8
|
+
|
|
3
9
|
## 3.2.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
# enforce-constant-case
|
|
2
2
|
|
|
3
|
-
Enforce SCREAMING_SNAKE_CASE for global
|
|
3
|
+
Enforce SCREAMING_SNAKE_CASE for global magic-number and magic-text constants.
|
|
4
4
|
|
|
5
5
|
## Rule Details
|
|
6
6
|
|
|
7
|
-
This rule ensures that global-scope `const` declarations
|
|
7
|
+
This rule ensures that global-scope `const` declarations bound to a **magic number** or **magic text** literal use SCREAMING_SNAKE_CASE. The rule scope is intentionally narrow:
|
|
8
|
+
|
|
9
|
+
- A magic text constant is a string literal: `const API_URL = "https://api.example.com"`
|
|
10
|
+
- A magic number constant is a number literal (including a unary `-`/`+` over a numeric literal): `const PAGE_LIMIT = 10`, `const OFFSET = -1`
|
|
11
|
+
|
|
12
|
+
Anything else is **not** checked: booleans, RegExp, template literals (static or dynamic), arrays, objects, `as const` assertions, function calls, identifiers, member expressions, JSX. Use whatever name fits the value (`metadata`, `viewport`, `statusMap`, `phoneRegex`, `isEnabled`, etc.) — the rule will not flag it.
|
|
8
13
|
|
|
9
14
|
Only global scope (top-level of a file) is checked. Local scope constants inside functions are not checked by this rule.
|
|
10
15
|
|
|
11
16
|
**Config files are exempt.** Files matching `*.config.{ts,mjs,cjs,js}`, `*.rc.*`, `*.setup.*`, `*.spec.*`, `*.test.*`, `.eslintrc*`, `.babelrc*`, and `.prettierrc*` skip this rule entirely. This avoids conflicts with framework conventions that require specific identifier names — e.g. Next.js expects `nextConfig` (not `NEXT_CONFIG`) in `next.config.ts`, Vite expects `config`, Tailwind expects `config`, etc.
|
|
12
17
|
|
|
18
|
+
### Why magic numbers and magic text only?
|
|
19
|
+
|
|
20
|
+
Reserved framework export names commonly bind to objects (Next.js App Router exports `metadata`, `viewport`, `generateStaticParams`, `dynamic`, `revalidate`, `runtime`, `fetchCache`, `dynamicParams`, `preferredRegion`, `maxDuration`; React Server Components and others have similar patterns). Forcing SCREAMING_SNAKE_CASE on any static-shaped initializer would rename those exports and break framework integration. Restricting the rule to bare number and string literals keeps the convention where it adds value (avoiding magic constants scattered through code) without colliding with framework-owned names.
|
|
21
|
+
|
|
13
22
|
## Examples
|
|
14
23
|
|
|
15
24
|
### Incorrect
|
|
@@ -18,10 +27,8 @@ Only global scope (top-level of a file) is checked. Local scope constants inside
|
|
|
18
27
|
const defaultCover = "/images/default.jpg";
|
|
19
28
|
const pageLimit = 10;
|
|
20
29
|
const apiBaseUrl = "https://api.example.com";
|
|
21
|
-
const
|
|
22
|
-
const phoneRegex = /^[0-9]{10}$/;
|
|
30
|
+
const negativeOne = -1;
|
|
23
31
|
const default_theme = "dark";
|
|
24
|
-
export const categories = [{ id: "1" }] as const;
|
|
25
32
|
```
|
|
26
33
|
|
|
27
34
|
### Correct
|
|
@@ -30,35 +37,39 @@ export const categories = [{ id: "1" }] as const;
|
|
|
30
37
|
const DEFAULT_COVER = "/images/default.jpg";
|
|
31
38
|
const PAGE_LIMIT = 10;
|
|
32
39
|
const API_BASE_URL = "https://api.example.com";
|
|
33
|
-
const
|
|
34
|
-
const PHONE_REGEX = /^[0-9]{10}$/;
|
|
40
|
+
const NEGATIVE_ONE = -1;
|
|
35
41
|
const DEFAULT_THEME = "dark";
|
|
36
|
-
export const CATEGORIES = [{ id: "1" }] as const;
|
|
37
42
|
|
|
38
|
-
const SKELETON_ITEMS = [1, 2, 3, 4, 5];
|
|
39
|
-
const MAP_STYLE = { height: "320px", width: "100%" };
|
|
40
|
-
const STATUS_MAP = { ACTIVE: "active" } as const;
|
|
41
|
-
|
|
42
|
-
// Booleans with standard prefixes (is, has, should, etc.) are exempt
|
|
43
43
|
const isProduction = true;
|
|
44
44
|
const hasAccess = false;
|
|
45
|
+
const featureEnabled = true;
|
|
46
|
+
|
|
47
|
+
const phoneRegex = /^[0-9]{10}$/;
|
|
48
|
+
const template = `hello world`;
|
|
49
|
+
const skeletonItems = [1, 2, 3, 4, 5];
|
|
50
|
+
const mapStyle = { height: "320px", width: "100%" };
|
|
51
|
+
const statusMap = { ACTIVE: "active" } as const;
|
|
52
|
+
const categories = [{ id: "1" }] as const;
|
|
53
|
+
|
|
54
|
+
export const metadata: Metadata = { title: "404 - Page Not Found" };
|
|
55
|
+
export const viewport: Viewport = { themeColor: "#fff" };
|
|
56
|
+
export const generateStaticParams = async () => [];
|
|
45
57
|
|
|
46
|
-
// Template literals with expressions are dynamic, camelCase is fine
|
|
47
58
|
const pendingHref = `/branch/${branch.branchNumber}`;
|
|
48
59
|
|
|
49
|
-
// Functions and components are not checked
|
|
50
60
|
const handleClick = () => {};
|
|
51
61
|
const MyComponent = () => {};
|
|
52
62
|
|
|
53
|
-
// Local scope is not checked
|
|
54
63
|
function foo() {
|
|
55
64
|
const maxRetry = 3;
|
|
56
65
|
}
|
|
57
66
|
```
|
|
58
67
|
|
|
68
|
+
> Note: Next.js App Router has a few string-valued reserved exports — `dynamic = "force-dynamic"`, `runtime = "edge"`, `fetchCache = "default-cache"`, etc. — and one number-valued one (`revalidate = 60`, `maxDuration = 30`). These remain in scope for this rule because their initializers are bare literals. Disable `nextfriday/enforce-constant-case` for `app/**` and `pages/**` in your own flat config if you use those exports.
|
|
69
|
+
|
|
59
70
|
## Configuration
|
|
60
71
|
|
|
61
|
-
This rule has no options — only severity is configurable (`"error"`, `"warn"`, `"off"`). It pairs with [`no-misleading-constant-case`](./NO_MISLEADING_CONSTANT_CASE.md) so that
|
|
72
|
+
This rule has no options — only severity is configurable (`"error"`, `"warn"`, `"off"`). It pairs with [`no-misleading-constant-case`](./NO_MISLEADING_CONSTANT_CASE.md) so that magic-literal globals use `SCREAMING_SNAKE_CASE` while local scopes and dynamic values keep `camelCase`.
|
|
62
73
|
|
|
63
74
|
### Install
|
|
64
75
|
|
package/lib/index.cjs
CHANGED
|
@@ -40,7 +40,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
40
40
|
// package.json
|
|
41
41
|
var package_default = {
|
|
42
42
|
name: "eslint-plugin-nextfriday",
|
|
43
|
-
version: "3.2.
|
|
43
|
+
version: "3.2.1",
|
|
44
44
|
description: "A comprehensive ESLint plugin providing custom rules and configurations for Next Friday development workflows.",
|
|
45
45
|
keywords: [
|
|
46
46
|
"eslint",
|
|
@@ -447,40 +447,14 @@ var createRule3 = import_utils4.ESLintUtils.RuleCreator(
|
|
|
447
447
|
);
|
|
448
448
|
var SCREAMING_SNAKE_CASE_REGEX = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/;
|
|
449
449
|
var SNAKE_CASE_REGEX2 = /^[a-z]+_[a-z0-9_]*$/;
|
|
450
|
-
var BOOLEAN_PREFIXES2 = ["is", "has", "should", "can", "did", "will", "was", "are", "does", "had"];
|
|
451
450
|
var toScreamingSnakeCase = (str) => str.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").toUpperCase();
|
|
452
|
-
var
|
|
453
|
-
if (!name.startsWith(prefix)) {
|
|
454
|
-
return false;
|
|
455
|
-
}
|
|
456
|
-
if (name.length === prefix.length) {
|
|
457
|
-
return true;
|
|
458
|
-
}
|
|
459
|
-
const nextChar = name.charAt(prefix.length);
|
|
460
|
-
return nextChar === nextChar.toUpperCase() && nextChar !== nextChar.toLowerCase();
|
|
461
|
-
});
|
|
462
|
-
var isBooleanLiteral2 = (init) => init.type === import_utils4.AST_NODE_TYPES.Literal && typeof init.value === "boolean";
|
|
463
|
-
var isAsConstAssertion = (node) => node.type === import_utils4.AST_NODE_TYPES.TSAsExpression && node.typeAnnotation.type === import_utils4.AST_NODE_TYPES.TSTypeReference && node.typeAnnotation.typeName.type === import_utils4.AST_NODE_TYPES.Identifier && node.typeAnnotation.typeName.name === "const";
|
|
464
|
-
var isStaticValue2 = (init) => {
|
|
465
|
-
if (isAsConstAssertion(init)) {
|
|
466
|
-
return true;
|
|
467
|
-
}
|
|
451
|
+
var isMagicLiteral = (init) => {
|
|
468
452
|
if (init.type === import_utils4.AST_NODE_TYPES.Literal) {
|
|
469
|
-
return
|
|
453
|
+
return typeof init.value === "string" || typeof init.value === "number";
|
|
470
454
|
}
|
|
471
|
-
if (init.type === import_utils4.AST_NODE_TYPES.UnaryExpression
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
if (init.type === import_utils4.AST_NODE_TYPES.TemplateLiteral && init.expressions.length === 0) {
|
|
475
|
-
return true;
|
|
476
|
-
}
|
|
477
|
-
if (init.type === import_utils4.AST_NODE_TYPES.ArrayExpression) {
|
|
478
|
-
return init.elements.every((el) => el !== null && el.type !== import_utils4.AST_NODE_TYPES.SpreadElement && isStaticValue2(el));
|
|
479
|
-
}
|
|
480
|
-
if (init.type === import_utils4.AST_NODE_TYPES.ObjectExpression) {
|
|
481
|
-
return init.properties.every(
|
|
482
|
-
(prop) => prop.type === import_utils4.AST_NODE_TYPES.Property && isStaticValue2(prop.value)
|
|
483
|
-
);
|
|
455
|
+
if (init.type === import_utils4.AST_NODE_TYPES.UnaryExpression) {
|
|
456
|
+
const { argument, operator } = init;
|
|
457
|
+
return (operator === "-" || operator === "+") && argument.type === import_utils4.AST_NODE_TYPES.Literal && typeof argument.value === "number";
|
|
484
458
|
}
|
|
485
459
|
return false;
|
|
486
460
|
};
|
|
@@ -494,13 +468,12 @@ var isGlobalScope2 = (node) => {
|
|
|
494
468
|
}
|
|
495
469
|
return false;
|
|
496
470
|
};
|
|
497
|
-
var isFunctionOrComponent = (init) => init.type === import_utils4.AST_NODE_TYPES.ArrowFunctionExpression || init.type === import_utils4.AST_NODE_TYPES.FunctionExpression;
|
|
498
471
|
var enforceConstantCase = createRule3({
|
|
499
472
|
name: "enforce-constant-case",
|
|
500
473
|
meta: {
|
|
501
474
|
type: "suggestion",
|
|
502
475
|
docs: {
|
|
503
|
-
description: "Enforce SCREAMING_SNAKE_CASE for global
|
|
476
|
+
description: "Enforce SCREAMING_SNAKE_CASE for global magic-number and magic-text constants"
|
|
504
477
|
},
|
|
505
478
|
messages: {
|
|
506
479
|
useScreamingSnakeCase: "Constant '{{ name }}' should use SCREAMING_SNAKE_CASE. Rename to '{{ suggestion }}'.",
|
|
@@ -522,16 +495,10 @@ var enforceConstantCase = createRule3({
|
|
|
522
495
|
if (declarator.id.type !== import_utils4.AST_NODE_TYPES.Identifier || !declarator.init) {
|
|
523
496
|
return;
|
|
524
497
|
}
|
|
525
|
-
if (
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
if (!isStaticValue2(declarator.init)) {
|
|
498
|
+
if (!isMagicLiteral(declarator.init)) {
|
|
529
499
|
return;
|
|
530
500
|
}
|
|
531
501
|
const { name } = declarator.id;
|
|
532
|
-
if (isBooleanLiteral2(declarator.init) && startsWithBooleanPrefix2(name)) {
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
502
|
if (SNAKE_CASE_REGEX2.test(name)) {
|
|
536
503
|
context.report({
|
|
537
504
|
node: declarator.id,
|
|
@@ -2861,9 +2828,9 @@ var createRule38 = import_utils42.ESLintUtils.RuleCreator(
|
|
|
2861
2828
|
(name) => `https://github.com/next-friday/eslint-plugin-nextfriday/blob/main/docs/rules/${name.replaceAll("-", "_").toUpperCase()}.md`
|
|
2862
2829
|
);
|
|
2863
2830
|
var SCREAMING_SNAKE_CASE_REGEX3 = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/;
|
|
2864
|
-
var
|
|
2865
|
-
var
|
|
2866
|
-
if (
|
|
2831
|
+
var isAsConstAssertion = (node) => node.type === import_utils42.AST_NODE_TYPES.TSAsExpression && node.typeAnnotation.type === import_utils42.AST_NODE_TYPES.TSTypeReference && node.typeAnnotation.typeName.type === import_utils42.AST_NODE_TYPES.Identifier && node.typeAnnotation.typeName.name === "const";
|
|
2832
|
+
var isStaticValue2 = (init) => {
|
|
2833
|
+
if (isAsConstAssertion(init)) {
|
|
2867
2834
|
return true;
|
|
2868
2835
|
}
|
|
2869
2836
|
if (init.type === import_utils42.AST_NODE_TYPES.Literal) {
|
|
@@ -2876,11 +2843,11 @@ var isStaticValue3 = (init) => {
|
|
|
2876
2843
|
return true;
|
|
2877
2844
|
}
|
|
2878
2845
|
if (init.type === import_utils42.AST_NODE_TYPES.ArrayExpression) {
|
|
2879
|
-
return init.elements.every((el) => el !== null && el.type !== import_utils42.AST_NODE_TYPES.SpreadElement &&
|
|
2846
|
+
return init.elements.every((el) => el !== null && el.type !== import_utils42.AST_NODE_TYPES.SpreadElement && isStaticValue2(el));
|
|
2880
2847
|
}
|
|
2881
2848
|
if (init.type === import_utils42.AST_NODE_TYPES.ObjectExpression) {
|
|
2882
2849
|
return init.properties.every(
|
|
2883
|
-
(prop) => prop.type === import_utils42.AST_NODE_TYPES.Property &&
|
|
2850
|
+
(prop) => prop.type === import_utils42.AST_NODE_TYPES.Property && isStaticValue2(prop.value)
|
|
2884
2851
|
);
|
|
2885
2852
|
}
|
|
2886
2853
|
return false;
|
|
@@ -2940,7 +2907,7 @@ var noMisleadingConstantCase = createRule38({
|
|
|
2940
2907
|
if (!declarator.init) {
|
|
2941
2908
|
return;
|
|
2942
2909
|
}
|
|
2943
|
-
if (!
|
|
2910
|
+
if (!isStaticValue2(declarator.init)) {
|
|
2944
2911
|
context.report({
|
|
2945
2912
|
node: declarator.id,
|
|
2946
2913
|
messageId: "dynamicScreamingCase",
|