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 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 constant static values.
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 with static values use SCREAMING_SNAKE_CASE naming convention. Static values include: string/number/boolean literals, RegExp, static template literals, `as const` assertions, and objects/arrays containing only literal values.
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 template = `hello world`;
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 TEMPLATE = `hello world`;
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 static globals use `SCREAMING_SNAKE_CASE` while local scopes and dynamic values keep `camelCase`.
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.0",
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 startsWithBooleanPrefix2 = (name) => BOOLEAN_PREFIXES2.some((prefix) => {
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 true;
453
+ return typeof init.value === "string" || typeof init.value === "number";
470
454
  }
471
- if (init.type === import_utils4.AST_NODE_TYPES.UnaryExpression && init.argument.type === import_utils4.AST_NODE_TYPES.Literal) {
472
- return true;
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 constant static values"
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 (isFunctionOrComponent(declarator.init)) {
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 isAsConstAssertion2 = (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";
2865
- var isStaticValue3 = (init) => {
2866
- if (isAsConstAssertion2(init)) {
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 && isStaticValue3(el));
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 && isStaticValue3(prop.value)
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 (!isStaticValue3(declarator.init)) {
2910
+ if (!isStaticValue2(declarator.init)) {
2944
2911
  context.report({
2945
2912
  node: declarator.id,
2946
2913
  messageId: "dynamicScreamingCase",