eslint-plugin-nextjs 0.0.1 → 0.1.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.
Files changed (157) hide show
  1. package/dist/index.cjs +1494 -0
  2. package/dist/index.cjs.map +1 -0
  3. package/dist/index.d.cts +43 -0
  4. package/dist/index.d.ts +42 -1
  5. package/dist/index.js +1455 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/rules/google-font-display.cjs +119 -0
  8. package/dist/rules/google-font-display.cjs.map +1 -0
  9. package/dist/rules/google-font-display.d.cts +8 -0
  10. package/dist/rules/google-font-display.d.ts +8 -0
  11. package/dist/rules/google-font-display.js +92 -0
  12. package/dist/rules/google-font-display.js.map +1 -0
  13. package/dist/rules/google-font-preconnect.cjs +109 -0
  14. package/dist/rules/google-font-preconnect.cjs.map +1 -0
  15. package/dist/rules/google-font-preconnect.d.cts +5 -0
  16. package/dist/rules/google-font-preconnect.d.ts +5 -0
  17. package/dist/rules/google-font-preconnect.js +82 -0
  18. package/dist/rules/google-font-preconnect.js.map +1 -0
  19. package/dist/rules/inline-script-id.cjs +94 -0
  20. package/dist/rules/inline-script-id.cjs.map +1 -0
  21. package/dist/rules/inline-script-id.d.cts +5 -0
  22. package/dist/rules/inline-script-id.d.ts +5 -0
  23. package/dist/rules/inline-script-id.js +67 -0
  24. package/dist/rules/inline-script-id.js.map +1 -0
  25. package/dist/rules/next-script-for-ga.cjs +129 -0
  26. package/dist/rules/next-script-for-ga.cjs.map +1 -0
  27. package/dist/rules/next-script-for-ga.d.cts +5 -0
  28. package/dist/rules/next-script-for-ga.d.ts +5 -0
  29. package/dist/rules/next-script-for-ga.js +102 -0
  30. package/dist/rules/next-script-for-ga.js.map +1 -0
  31. package/dist/rules/no-assign-module-variable.cjs +64 -0
  32. package/dist/rules/no-assign-module-variable.cjs.map +1 -0
  33. package/dist/rules/no-assign-module-variable.d.cts +5 -0
  34. package/dist/rules/no-assign-module-variable.d.ts +5 -0
  35. package/dist/rules/no-assign-module-variable.js +37 -0
  36. package/dist/rules/no-assign-module-variable.js.map +1 -0
  37. package/dist/rules/no-async-client-component.cjs +99 -0
  38. package/dist/rules/no-async-client-component.cjs.map +1 -0
  39. package/dist/rules/no-async-client-component.d.cts +5 -0
  40. package/dist/rules/no-async-client-component.d.ts +5 -0
  41. package/dist/rules/no-async-client-component.js +72 -0
  42. package/dist/rules/no-async-client-component.js.map +1 -0
  43. package/dist/rules/no-before-interactive-script-outside-document.cjs +94 -0
  44. package/dist/rules/no-before-interactive-script-outside-document.cjs.map +1 -0
  45. package/dist/rules/no-before-interactive-script-outside-document.d.cts +5 -0
  46. package/dist/rules/no-before-interactive-script-outside-document.d.ts +5 -0
  47. package/dist/rules/no-before-interactive-script-outside-document.js +59 -0
  48. package/dist/rules/no-before-interactive-script-outside-document.js.map +1 -0
  49. package/dist/rules/no-css-tags.cjs +70 -0
  50. package/dist/rules/no-css-tags.cjs.map +1 -0
  51. package/dist/rules/no-css-tags.d.cts +5 -0
  52. package/dist/rules/no-css-tags.d.ts +5 -0
  53. package/dist/rules/no-css-tags.js +43 -0
  54. package/dist/rules/no-css-tags.js.map +1 -0
  55. package/dist/rules/no-document-import-in-page.cjs +74 -0
  56. package/dist/rules/no-document-import-in-page.cjs.map +1 -0
  57. package/dist/rules/no-document-import-in-page.d.cts +5 -0
  58. package/dist/rules/no-document-import-in-page.d.ts +5 -0
  59. package/dist/rules/no-document-import-in-page.js +39 -0
  60. package/dist/rules/no-document-import-in-page.js.map +1 -0
  61. package/dist/rules/no-duplicate-head.cjs +87 -0
  62. package/dist/rules/no-duplicate-head.cjs.map +1 -0
  63. package/dist/rules/no-duplicate-head.d.cts +5 -0
  64. package/dist/rules/no-duplicate-head.d.ts +5 -0
  65. package/dist/rules/no-duplicate-head.js +60 -0
  66. package/dist/rules/no-duplicate-head.js.map +1 -0
  67. package/dist/rules/no-head-element.cjs +76 -0
  68. package/dist/rules/no-head-element.cjs.map +1 -0
  69. package/dist/rules/no-head-element.d.cts +5 -0
  70. package/dist/rules/no-head-element.d.ts +5 -0
  71. package/dist/rules/no-head-element.js +41 -0
  72. package/dist/rules/no-head-element.js.map +1 -0
  73. package/dist/rules/no-head-import-in-document.cjs +76 -0
  74. package/dist/rules/no-head-import-in-document.cjs.map +1 -0
  75. package/dist/rules/no-head-import-in-document.d.cts +5 -0
  76. package/dist/rules/no-head-import-in-document.d.ts +5 -0
  77. package/dist/rules/no-head-import-in-document.js +41 -0
  78. package/dist/rules/no-head-import-in-document.js.map +1 -0
  79. package/dist/rules/no-html-link-for-pages.cjs +302 -0
  80. package/dist/rules/no-html-link-for-pages.cjs.map +1 -0
  81. package/dist/rules/no-html-link-for-pages.d.cts +5 -0
  82. package/dist/rules/no-html-link-for-pages.d.ts +5 -0
  83. package/dist/rules/no-html-link-for-pages.js +267 -0
  84. package/dist/rules/no-html-link-for-pages.js.map +1 -0
  85. package/dist/rules/no-img-element.cjs +83 -0
  86. package/dist/rules/no-img-element.cjs.map +1 -0
  87. package/dist/rules/no-img-element.d.cts +5 -0
  88. package/dist/rules/no-img-element.d.ts +5 -0
  89. package/dist/rules/no-img-element.js +48 -0
  90. package/dist/rules/no-img-element.js.map +1 -0
  91. package/dist/rules/no-page-custom-font.cjs +184 -0
  92. package/dist/rules/no-page-custom-font.cjs.map +1 -0
  93. package/dist/rules/no-page-custom-font.d.cts +5 -0
  94. package/dist/rules/no-page-custom-font.d.ts +5 -0
  95. package/dist/rules/no-page-custom-font.js +159 -0
  96. package/dist/rules/no-page-custom-font.js.map +1 -0
  97. package/dist/rules/no-script-component-in-head.cjs +74 -0
  98. package/dist/rules/no-script-component-in-head.cjs.map +1 -0
  99. package/dist/rules/no-script-component-in-head.d.cts +5 -0
  100. package/dist/rules/no-script-component-in-head.d.ts +5 -0
  101. package/dist/rules/no-script-component-in-head.js +47 -0
  102. package/dist/rules/no-script-component-in-head.js.map +1 -0
  103. package/dist/rules/no-styled-jsx-in-document.cjs +78 -0
  104. package/dist/rules/no-styled-jsx-in-document.cjs.map +1 -0
  105. package/dist/rules/no-styled-jsx-in-document.d.cts +5 -0
  106. package/dist/rules/no-styled-jsx-in-document.d.ts +5 -0
  107. package/dist/rules/no-styled-jsx-in-document.js +43 -0
  108. package/dist/rules/no-styled-jsx-in-document.js.map +1 -0
  109. package/dist/rules/no-sync-scripts.cjs +64 -0
  110. package/dist/rules/no-sync-scripts.cjs.map +1 -0
  111. package/dist/rules/no-sync-scripts.d.cts +5 -0
  112. package/dist/rules/no-sync-scripts.d.ts +5 -0
  113. package/dist/rules/no-sync-scripts.js +37 -0
  114. package/dist/rules/no-sync-scripts.js.map +1 -0
  115. package/dist/rules/no-title-in-document-head.cjs +78 -0
  116. package/dist/rules/no-title-in-document-head.cjs.map +1 -0
  117. package/dist/rules/no-title-in-document-head.d.cts +5 -0
  118. package/dist/rules/no-title-in-document-head.d.ts +5 -0
  119. package/dist/rules/no-title-in-document-head.js +51 -0
  120. package/dist/rules/no-title-in-document-head.js.map +1 -0
  121. package/dist/rules/no-typos.cjs +133 -0
  122. package/dist/rules/no-typos.cjs.map +1 -0
  123. package/dist/rules/no-typos.d.cts +5 -0
  124. package/dist/rules/no-typos.d.ts +5 -0
  125. package/dist/rules/no-typos.js +98 -0
  126. package/dist/rules/no-typos.js.map +1 -0
  127. package/dist/rules/no-unwanted-polyfillio.cjs +164 -0
  128. package/dist/rules/no-unwanted-polyfillio.cjs.map +1 -0
  129. package/dist/rules/no-unwanted-polyfillio.d.cts +5 -0
  130. package/dist/rules/no-unwanted-polyfillio.d.ts +5 -0
  131. package/dist/rules/no-unwanted-polyfillio.js +137 -0
  132. package/dist/rules/no-unwanted-polyfillio.js.map +1 -0
  133. package/dist/utils/define-rule.cjs +31 -0
  134. package/dist/utils/define-rule.cjs.map +1 -0
  135. package/dist/utils/define-rule.d.cts +5 -0
  136. package/dist/utils/define-rule.d.ts +5 -0
  137. package/dist/utils/define-rule.js +6 -0
  138. package/dist/utils/define-rule.js.map +1 -0
  139. package/dist/utils/get-root-dirs.cjs +60 -0
  140. package/dist/utils/get-root-dirs.cjs.map +1 -0
  141. package/dist/utils/get-root-dirs.d.cts +8 -0
  142. package/dist/utils/get-root-dirs.d.ts +8 -0
  143. package/dist/utils/get-root-dirs.js +25 -0
  144. package/dist/utils/get-root-dirs.js.map +1 -0
  145. package/dist/utils/node-attributes.cjs +67 -0
  146. package/dist/utils/node-attributes.cjs.map +1 -0
  147. package/dist/utils/node-attributes.d.cts +15 -0
  148. package/dist/utils/node-attributes.d.ts +15 -0
  149. package/dist/utils/node-attributes.js +46 -0
  150. package/dist/utils/node-attributes.js.map +1 -0
  151. package/dist/utils/url.cjs +167 -0
  152. package/dist/utils/url.cjs.map +1 -0
  153. package/dist/utils/url.d.cts +35 -0
  154. package/dist/utils/url.d.ts +35 -0
  155. package/dist/utils/url.js +128 -0
  156. package/dist/utils/url.js.map +1 -0
  157. package/package.json +15 -2
package/dist/index.js CHANGED
@@ -1,3 +1,1457 @@
1
+ // src/utils/define-rule.ts
2
+ var defineRule = (rule) => rule;
3
+
4
+ // src/utils/node-attributes.ts
5
+ var NodeAttributes = class {
6
+ attributes;
7
+ constructor(ASTnode) {
8
+ this.attributes = {};
9
+ ASTnode.attributes.forEach((attribute) => {
10
+ if (!attribute.type || attribute.type !== "JSXAttribute") {
11
+ return;
12
+ }
13
+ if (attribute.value) {
14
+ const value = typeof attribute.value.value === "string" ? attribute.value.value : typeof attribute.value.expression?.value !== "undefined" ? attribute.value.expression.value : attribute.value.expression?.properties;
15
+ this.attributes[attribute.name.name] = {
16
+ hasValue: true,
17
+ value
18
+ };
19
+ } else {
20
+ this.attributes[attribute.name.name] = {
21
+ hasValue: false
22
+ };
23
+ }
24
+ });
25
+ }
26
+ has(attrName) {
27
+ return Boolean(this.attributes[attrName]);
28
+ }
29
+ hasAny() {
30
+ return Boolean(Object.keys(this.attributes).length);
31
+ }
32
+ hasValue(attrName) {
33
+ return Boolean(this.attributes[attrName]?.hasValue);
34
+ }
35
+ value(attrName) {
36
+ const attr = this.attributes[attrName];
37
+ if (!attr) {
38
+ return true;
39
+ }
40
+ if ("hasValue" in attr && attr.hasValue) {
41
+ return attr.value;
42
+ }
43
+ return void 0;
44
+ }
45
+ };
46
+
47
+ // src/rules/google-font-display.ts
48
+ var url = "https://nextjs.org/docs/messages/google-font-display";
49
+ var googleFontDisplay = defineRule({
50
+ create: (context) => ({
51
+ JSXOpeningElement: (node) => {
52
+ let message2;
53
+ if (node.name.name !== "link") {
54
+ return;
55
+ }
56
+ const attributes = new NodeAttributes(node);
57
+ if (!attributes.has("href") || !attributes.hasValue("href")) {
58
+ return;
59
+ }
60
+ const hrefValue = attributes.value("href");
61
+ const isGoogleFont = typeof hrefValue === "string" && hrefValue.startsWith("https://fonts.googleapis.com/css");
62
+ if (isGoogleFont) {
63
+ const params = new URLSearchParams(hrefValue.split("?", 2)[1]);
64
+ const displayValue = params.get("display");
65
+ if (!params.has("display")) {
66
+ message2 = "A font-display parameter is missing (adding `&display=optional` is recommended).";
67
+ } else if (displayValue === "auto" || displayValue === "block" || displayValue === "fallback") {
68
+ message2 = `${displayValue[0]?.toUpperCase() + displayValue.slice(1)} is not recommended.`;
69
+ }
70
+ }
71
+ if (message2) {
72
+ context.report({
73
+ message: `${message2} See: ${url}`,
74
+ node
75
+ });
76
+ }
77
+ }
78
+ }),
79
+ meta: {
80
+ docs: {
81
+ description: "Enforce font-display behavior with Google Fonts.",
82
+ recommended: true,
83
+ url
84
+ },
85
+ schema: [],
86
+ type: "problem"
87
+ }
88
+ });
89
+
90
+ // src/rules/google-font-preconnect.ts
91
+ var url2 = "https://nextjs.org/docs/messages/google-font-preconnect";
92
+ var googleFontPreconnect = defineRule({
93
+ create: (context) => ({
94
+ JSXOpeningElement: (node) => {
95
+ if (node.name.name !== "link") {
96
+ return;
97
+ }
98
+ const attributes = new NodeAttributes(node);
99
+ if (!attributes.has("href") || !attributes.hasValue("href")) {
100
+ return;
101
+ }
102
+ const hrefValue = attributes.value("href");
103
+ const preconnectMissing = !attributes.has("rel") || !attributes.hasValue("rel") || attributes.value("rel") !== "preconnect";
104
+ if (typeof hrefValue === "string" && hrefValue.startsWith("https://fonts.gstatic.com") && preconnectMissing) {
105
+ context.report({
106
+ message: `\`rel="preconnect"\` is missing from Google Font. See: ${url2}`,
107
+ node
108
+ });
109
+ }
110
+ }
111
+ }),
112
+ meta: {
113
+ docs: {
114
+ description: "Ensure `preconnect` is used with Google Fonts.",
115
+ recommended: true,
116
+ url: url2
117
+ },
118
+ schema: [],
119
+ type: "problem"
120
+ }
121
+ });
122
+
123
+ // src/rules/inline-script-id.ts
124
+ var url3 = "https://nextjs.org/docs/messages/inline-script-id";
125
+ var inlineScriptId = defineRule({
126
+ create: (context) => {
127
+ let nextScriptImportName = null;
128
+ return {
129
+ ImportDeclaration: (node) => {
130
+ if (node.source.value === "next/script") {
131
+ nextScriptImportName = node.specifiers[0].local.name;
132
+ }
133
+ },
134
+ JSXElement: (node) => {
135
+ if (nextScriptImportName === null) {
136
+ return;
137
+ }
138
+ if (node.openingElement?.name && node.openingElement.name.name !== nextScriptImportName) {
139
+ return;
140
+ }
141
+ const attributeNames = /* @__PURE__ */ new Set();
142
+ let hasNonCheckableSpreadAttribute = false;
143
+ node.openingElement.attributes.forEach((attribute) => {
144
+ if (hasNonCheckableSpreadAttribute) {
145
+ return;
146
+ }
147
+ if (attribute.type === "JSXAttribute") {
148
+ attributeNames.add(attribute.name.name);
149
+ } else if (attribute.type === "JSXSpreadAttribute") {
150
+ if (attribute.argument?.properties) {
151
+ attribute.argument.properties.forEach((property) => {
152
+ attributeNames.add(property.key.name);
153
+ });
154
+ } else {
155
+ hasNonCheckableSpreadAttribute = true;
156
+ }
157
+ }
158
+ });
159
+ if (hasNonCheckableSpreadAttribute) {
160
+ return;
161
+ }
162
+ if (node.children.length > 0 || attributeNames.has("dangerouslySetInnerHTML")) {
163
+ if (!attributeNames.has("id")) {
164
+ context.report({
165
+ message: `\`next/script\` components with inline content must specify an \`id\` attribute. See: ${url3}`,
166
+ node
167
+ });
168
+ }
169
+ }
170
+ }
171
+ };
172
+ },
173
+ meta: {
174
+ docs: {
175
+ description: "Enforce `id` attribute on `next/script` components with inline content.",
176
+ recommended: true,
177
+ url: url3
178
+ },
179
+ schema: [],
180
+ type: "problem"
181
+ }
182
+ });
183
+
184
+ // src/rules/next-script-for-ga.ts
185
+ var SUPPORTED_SRCS = [
186
+ "www.google-analytics.com/analytics.js",
187
+ "www.googletagmanager.com/gtag/js"
188
+ ];
189
+ var SUPPORTED_HTML_CONTENT_URLS = [
190
+ "www.google-analytics.com/analytics.js",
191
+ "www.googletagmanager.com/gtm.js"
192
+ ];
193
+ var description = "Prefer `next/script` component when using the inline script for Google Analytics.";
194
+ var url4 = "https://nextjs.org/docs/messages/next-script-for-ga";
195
+ var ERROR_MSG = `${description} See: ${url4}`;
196
+ var containsStr = (str, strList) => {
197
+ return strList.some((s) => str.includes(s));
198
+ };
199
+ var nextScriptForGa = defineRule({
200
+ create: (context) => ({
201
+ JSXOpeningElement: (node) => {
202
+ if (node.name.name !== "script") {
203
+ return;
204
+ }
205
+ if (node.attributes.length === 0) {
206
+ return;
207
+ }
208
+ const attributes = new NodeAttributes(node);
209
+ if (typeof attributes.value("src") === "string" && containsStr(attributes.value("src"), SUPPORTED_SRCS)) {
210
+ return context.report({
211
+ message: ERROR_MSG,
212
+ node
213
+ });
214
+ }
215
+ if (attributes.value("dangerouslySetInnerHTML") && attributes.value("dangerouslySetInnerHTML").length > 0) {
216
+ const htmlContent = attributes.value("dangerouslySetInnerHTML")[0].value.quasis?.[0].value.raw;
217
+ if (htmlContent && containsStr(htmlContent, SUPPORTED_HTML_CONTENT_URLS)) {
218
+ context.report({
219
+ message: ERROR_MSG,
220
+ node
221
+ });
222
+ }
223
+ }
224
+ }
225
+ }),
226
+ meta: {
227
+ docs: {
228
+ description,
229
+ recommended: true,
230
+ url: url4
231
+ },
232
+ schema: [],
233
+ type: "problem"
234
+ }
235
+ });
236
+
237
+ // src/rules/no-assign-module-variable.ts
238
+ var url5 = "https://nextjs.org/docs/messages/no-assign-module-variable";
239
+ var noAssignModuleVariable = defineRule({
240
+ create: (context) => ({
241
+ VariableDeclaration: (node) => {
242
+ const moduleVariableFound = node.declarations.some((declaration) => {
243
+ if ("name" in declaration.id) {
244
+ return declaration.id.name === "module";
245
+ }
246
+ return false;
247
+ });
248
+ if (!moduleVariableFound) {
249
+ return;
250
+ }
251
+ context.report({
252
+ message: `Do not assign to the variable \`module\`. See: ${url5}`,
253
+ node
254
+ });
255
+ }
256
+ }),
257
+ meta: {
258
+ docs: {
259
+ description: "Prevent assignment to the `module` variable.",
260
+ recommended: true,
261
+ url: url5
262
+ },
263
+ schema: [],
264
+ type: "problem"
265
+ }
266
+ });
267
+
268
+ // src/rules/no-async-client-component.ts
269
+ var url6 = "https://nextjs.org/docs/messages/no-async-client-component";
270
+ var description2 = "Prevent client components from being async functions.";
271
+ var message = `${description2} See: ${url6}`;
272
+ var isCapitalized = (str) => /[A-Z]/.test(str[0] ?? "");
273
+ var noAsyncClientComponent = defineRule({
274
+ create: (context) => ({
275
+ Program: (node) => {
276
+ let isClientComponent = false;
277
+ for (const block of node.body) {
278
+ if (block.type === "ExpressionStatement" && block.expression.type === "Literal" && block.expression.value === "use client") {
279
+ isClientComponent = true;
280
+ }
281
+ if (block.type === "ExportDefaultDeclaration" && isClientComponent) {
282
+ if (block.declaration?.type === "FunctionDeclaration" && block.declaration.async && isCapitalized(block.declaration.id.name)) {
283
+ context.report({
284
+ message,
285
+ node: block
286
+ });
287
+ }
288
+ if (block.declaration.type === "Identifier" && isCapitalized(block.declaration.name)) {
289
+ const targetName = block.declaration.name;
290
+ const functionDeclaration = node.body.find((localBlock) => {
291
+ if (localBlock.type === "FunctionDeclaration" && localBlock.id.name === targetName) {
292
+ return true;
293
+ }
294
+ if (localBlock.type === "VariableDeclaration" && localBlock.declarations.find(
295
+ (declaration) => declaration.id?.type === "Identifier" && declaration.id.name === targetName
296
+ )) {
297
+ return true;
298
+ }
299
+ return false;
300
+ });
301
+ if (functionDeclaration?.type === "FunctionDeclaration" && functionDeclaration.async) {
302
+ context.report({
303
+ message,
304
+ node: functionDeclaration
305
+ });
306
+ }
307
+ if (functionDeclaration?.type === "VariableDeclaration") {
308
+ const varDeclarator = functionDeclaration.declarations.find(
309
+ (declaration) => declaration.id?.type === "Identifier" && declaration.id.name === targetName
310
+ );
311
+ if (varDeclarator?.init?.type === "ArrowFunctionExpression" && varDeclarator.init.async) {
312
+ context.report({
313
+ message,
314
+ node: functionDeclaration
315
+ });
316
+ }
317
+ }
318
+ }
319
+ }
320
+ }
321
+ }
322
+ }),
323
+ meta: {
324
+ docs: {
325
+ description: description2,
326
+ recommended: true,
327
+ url: url6
328
+ },
329
+ schema: [],
330
+ type: "problem"
331
+ }
332
+ });
333
+
334
+ // src/rules/no-before-interactive-script-outside-document.ts
335
+ import * as path from "node:path";
336
+ var url7 = "https://nextjs.org/docs/messages/no-before-interactive-script-outside-document";
337
+ var convertToCorrectSeparator = (str) => str.replaceAll(/[/\\]/g, path.sep);
338
+ var noBeforeInteractiveScriptOutsideDocument = defineRule({
339
+ create: (context) => {
340
+ let scriptImportName = null;
341
+ return {
342
+ 'ImportDeclaration[source.value="next/script"] > ImportDefaultSpecifier'(node) {
343
+ scriptImportName = node.local.name;
344
+ },
345
+ JSXOpeningElement: (node) => {
346
+ const pathname = convertToCorrectSeparator(context.filename);
347
+ const isInAppDir = pathname.includes(`${path.sep}app${path.sep}`);
348
+ if (isInAppDir) {
349
+ return;
350
+ }
351
+ if (!scriptImportName) {
352
+ return;
353
+ }
354
+ if (node.name && node.name.name !== scriptImportName) {
355
+ return;
356
+ }
357
+ const strategy = node.attributes.find(
358
+ (child) => child.name && child.name.name === "strategy"
359
+ );
360
+ if (!strategy?.value || strategy.value.value !== "beforeInteractive") {
361
+ return;
362
+ }
363
+ const document = context.filename.split("pages", 2)[1];
364
+ if (document && path.parse(document).name.startsWith("_document")) {
365
+ return;
366
+ }
367
+ context.report({
368
+ message: `\`next/script\`'s \`beforeInteractive\` strategy should not be used outside of \`pages/_document.js\`. See: ${url7}`,
369
+ node
370
+ });
371
+ }
372
+ };
373
+ },
374
+ meta: {
375
+ docs: {
376
+ description: "Prevent usage of `next/script`'s `beforeInteractive` strategy outside of `pages/_document.js`.",
377
+ recommended: true,
378
+ url: url7
379
+ },
380
+ schema: [],
381
+ type: "problem"
382
+ }
383
+ });
384
+
385
+ // src/rules/no-css-tags.ts
386
+ var url8 = "https://nextjs.org/docs/messages/no-css-tags";
387
+ var noCssTags = defineRule({
388
+ create: (context) => ({
389
+ JSXOpeningElement: (node) => {
390
+ if (node.name.name !== "link") {
391
+ return;
392
+ }
393
+ if (node.attributes.length === 0) {
394
+ return;
395
+ }
396
+ const attributes = node.attributes.filter(
397
+ (attr) => attr.type === "JSXAttribute"
398
+ );
399
+ if (attributes.find(
400
+ (attr) => attr.name.name === "rel" && attr.value.value === "stylesheet"
401
+ ) && attributes.find(
402
+ (attr) => attr.name.name === "href" && attr.value.type === "Literal" && !/^https?/.test(attr.value.value)
403
+ )) {
404
+ context.report({
405
+ message: `Do not include stylesheets manually. See: ${url8}`,
406
+ node
407
+ });
408
+ }
409
+ }
410
+ }),
411
+ meta: {
412
+ docs: {
413
+ description: "Prevent manual stylesheet tags.",
414
+ recommended: true,
415
+ url: url8
416
+ },
417
+ schema: [],
418
+ type: "problem"
419
+ }
420
+ });
421
+
422
+ // src/rules/no-document-import-in-page.ts
423
+ import * as path2 from "node:path";
424
+ var url9 = "https://nextjs.org/docs/messages/no-document-import-in-page";
425
+ var noDocumentImportInPage = defineRule({
426
+ create: (context) => ({
427
+ ImportDeclaration: (node) => {
428
+ if (node.source.value !== "next/document") {
429
+ return;
430
+ }
431
+ const paths = context.filename.split("pages");
432
+ const page = paths[paths.length - 1];
433
+ if (!page || page.startsWith(`${path2.sep}_document`) || page.startsWith(`${path2.posix.sep}_document`)) {
434
+ return;
435
+ }
436
+ context.report({
437
+ message: `\`<Document />\` from \`next/document\` should not be imported outside of \`pages/_document.js\`. See: ${url9}`,
438
+ node
439
+ });
440
+ }
441
+ }),
442
+ meta: {
443
+ docs: {
444
+ description: "Prevent importing `next/document` outside of `pages/_document.js`.",
445
+ recommended: true,
446
+ url: url9
447
+ },
448
+ schema: [],
449
+ type: "problem"
450
+ }
451
+ });
452
+
453
+ // src/rules/no-duplicate-head.ts
454
+ var url10 = "https://nextjs.org/docs/messages/no-duplicate-head";
455
+ var noDuplicateHead = defineRule({
456
+ create: (context) => {
457
+ const { sourceCode } = context;
458
+ let documentImportName = null;
459
+ return {
460
+ ImportDeclaration: (node) => {
461
+ if (node.source.value === "next/document") {
462
+ const documentImport = node.specifiers.find(
463
+ ({ type }) => type === "ImportDefaultSpecifier"
464
+ );
465
+ if (documentImport?.local) {
466
+ documentImportName = documentImport.local.name;
467
+ }
468
+ }
469
+ },
470
+ ReturnStatement: (node) => {
471
+ const ancestors = sourceCode.getAncestors(node);
472
+ const documentClass = ancestors.find(
473
+ (ancestorNode) => ancestorNode.type === "ClassDeclaration" && ancestorNode.superClass && "name" in ancestorNode.superClass && ancestorNode.superClass.name === documentImportName
474
+ );
475
+ if (!documentClass) {
476
+ return;
477
+ }
478
+ if (node.argument && "children" in node.argument && // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
479
+ node.argument.children) {
480
+ const headComponents = node.argument.children.filter(
481
+ (childrenNode) => childrenNode.openingElement?.name && childrenNode.openingElement.name.name === "Head"
482
+ );
483
+ if (headComponents.length > 1) {
484
+ for (let i = 1; i < headComponents.length; i++) {
485
+ context.report({
486
+ message: `Do not include multiple instances of \`<Head/>\`. See: ${url10}`,
487
+ // @ts-expect-error initial override, TODO: fix
488
+ node: headComponents[i]
489
+ });
490
+ }
491
+ }
492
+ }
493
+ }
494
+ };
495
+ },
496
+ meta: {
497
+ docs: {
498
+ description: "Prevent duplicate usage of `<Head>` in `pages/_document.js`.",
499
+ recommended: true,
500
+ url: url10
501
+ },
502
+ schema: [],
503
+ type: "problem"
504
+ }
505
+ });
506
+
507
+ // src/rules/no-head-element.ts
508
+ import path3 from "node:path";
509
+ var url11 = "https://nextjs.org/docs/messages/no-head-element";
510
+ var noHeadElement = defineRule({
511
+ create: (context) => ({
512
+ JSXOpeningElement: (node) => {
513
+ const paths = context.filename;
514
+ const isInAppDir = () => (
515
+ // TODO: fix
516
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
517
+ paths.includes(`app${path3.sep}`) || paths.includes(`app${path3.posix.sep}`)
518
+ );
519
+ if (node.name.name !== "head" || isInAppDir()) {
520
+ return;
521
+ }
522
+ context.report({
523
+ message: `Do not use \`<head>\` element. Use \`<Head />\` from \`next/head\` instead. See: ${url11}`,
524
+ node
525
+ });
526
+ }
527
+ }),
528
+ meta: {
529
+ docs: {
530
+ category: "HTML",
531
+ description: "Prevent usage of `<head>` element.",
532
+ recommended: true,
533
+ url: url11
534
+ },
535
+ schema: [],
536
+ type: "problem"
537
+ }
538
+ });
539
+
540
+ // src/rules/no-head-import-in-document.ts
541
+ import * as path4 from "node:path";
542
+ var url12 = "https://nextjs.org/docs/messages/no-head-import-in-document";
543
+ var noHeadImportInDocument = defineRule({
544
+ create: (context) => ({
545
+ ImportDeclaration: (node) => {
546
+ if (node.source.value !== "next/head") {
547
+ return;
548
+ }
549
+ const document = context.filename.split("pages", 2)[1];
550
+ if (!document) {
551
+ return;
552
+ }
553
+ const { dir, name: name2 } = path4.parse(document);
554
+ if (name2.startsWith("_document") || dir === "/_document" && name2 === "index") {
555
+ context.report({
556
+ message: `\`next/head\` should not be imported in \`pages${document}\`. Use \`<Head />\` from \`next/document\` instead. See: ${url12}`,
557
+ node
558
+ });
559
+ }
560
+ }
561
+ }),
562
+ meta: {
563
+ docs: {
564
+ description: "Prevent usage of `next/head` in `pages/_document.js`.",
565
+ recommended: true,
566
+ url: url12
567
+ },
568
+ schema: [],
569
+ type: "problem"
570
+ }
571
+ });
572
+
573
+ // src/rules/no-html-link-for-pages.ts
574
+ import * as fs2 from "node:fs";
575
+ import * as path6 from "node:path";
576
+
577
+ // src/utils/get-root-dirs.ts
578
+ import fastGlob from "fast-glob";
579
+ var processRootDir = (rootDir) => {
580
+ return fastGlob.globSync(rootDir.replaceAll("\\", "/"), {
581
+ onlyDirectories: true
582
+ });
583
+ };
584
+ var getRootDirs = (context) => {
585
+ let rootDirs = [context.cwd];
586
+ const nextSettings = (
587
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
588
+ context.settings.next || {}
589
+ );
590
+ const rootDir = nextSettings.rootDir;
591
+ if (typeof rootDir === "string") {
592
+ rootDirs = processRootDir(rootDir);
593
+ } else if (Array.isArray(rootDir)) {
594
+ rootDirs = rootDir.map((dir) => typeof dir === "string" ? processRootDir(dir) : []).flat();
595
+ }
596
+ return rootDirs;
597
+ };
598
+
599
+ // src/utils/url.ts
600
+ import * as fs from "node:fs";
601
+ import * as path5 from "node:path";
602
+ var fsReadDirSyncCache = {};
603
+ var parseUrlForPages = (urlprefix, directory) => {
604
+ fsReadDirSyncCache[directory] ??= fs.readdirSync(directory, {
605
+ withFileTypes: true
606
+ });
607
+ const res = [];
608
+ fsReadDirSyncCache[directory].forEach((dirent) => {
609
+ if (/(?<temp2>\.(?<temp1>j|t)sx?)$/.test(dirent.name)) {
610
+ if (/^index(?<temp2>\.(?<temp1>j|t)sx?)$/.test(dirent.name)) {
611
+ res.push(
612
+ `${urlprefix}${dirent.name.replace(/^index(?<temp2>\.(?<temp1>j|t)sx?)$/, "")}`
613
+ );
614
+ }
615
+ res.push(
616
+ `${urlprefix}${dirent.name.replace(/(?<temp2>\.(?<temp1>j|t)sx?)$/, "")}`
617
+ );
618
+ } else {
619
+ const dirPath = path5.join(directory, dirent.name);
620
+ if (dirent.isDirectory() && !dirent.isSymbolicLink()) {
621
+ res.push(...parseUrlForPages(`${urlprefix + dirent.name}/`, dirPath));
622
+ }
623
+ }
624
+ });
625
+ return res;
626
+ };
627
+ var parseUrlForAppDir = (urlprefix, directory) => {
628
+ fsReadDirSyncCache[directory] ??= fs.readdirSync(directory, {
629
+ withFileTypes: true
630
+ });
631
+ const res = [];
632
+ fsReadDirSyncCache[directory].forEach((dirent) => {
633
+ if (/(?<temp2>\.(?<temp1>j|t)sx?)$/.test(dirent.name)) {
634
+ if (/^page(?<temp2>\.(?<temp1>j|t)sx?)$/.test(dirent.name)) {
635
+ res.push(
636
+ `${urlprefix}${dirent.name.replace(/^page(?<temp2>\.(?<temp1>j|t)sx?)$/, "")}`
637
+ );
638
+ } else if (!/^layout(?<temp2>\.(?<temp1>j|t)sx?)$/.test(dirent.name)) {
639
+ res.push(
640
+ `${urlprefix}${dirent.name.replace(/(?<temp2>\.(?<temp1>j|t)sx?)$/, "")}`
641
+ );
642
+ }
643
+ } else {
644
+ const dirPath = path5.join(directory, dirent.name);
645
+ if (dirent.isDirectory(dirPath) && !dirent.isSymbolicLink()) {
646
+ res.push(...parseUrlForPages(`${urlprefix + dirent.name}/`, dirPath));
647
+ }
648
+ }
649
+ });
650
+ return res;
651
+ };
652
+ var normalizeURL = (url21) => {
653
+ if (!url21) {
654
+ return;
655
+ }
656
+ url21 = url21.split("?", 1)[0];
657
+ url21 = url21.split("#", 1)[0];
658
+ url21 = url21.replace(/(?<temp1>\/index\.html)$/, "/");
659
+ if (url21 === "") {
660
+ return url21;
661
+ }
662
+ url21 = url21.endsWith("/") ? url21 : `${url21}/`;
663
+ return url21;
664
+ };
665
+ var normalizeAppPath = (route) => ensureLeadingSlash(
666
+ route.split("/").reduce((pathname, segment, index, segments) => {
667
+ if (!segment) {
668
+ return pathname;
669
+ }
670
+ if (isGroupSegment(segment)) {
671
+ return pathname;
672
+ }
673
+ if (segment.startsWith("@")) {
674
+ return pathname;
675
+ }
676
+ if ((segment === "page" || segment === "route") && index === segments.length - 1) {
677
+ return pathname;
678
+ }
679
+ return `${pathname}/${segment}`;
680
+ }, "")
681
+ );
682
+ var getUrlFromPagesDirectories = (urlPrefix, directories) => Array.from(
683
+ // De-duplicate similar pages across multiple directories.
684
+ new Set(
685
+ directories.flatMap((directory) => parseUrlForPages(urlPrefix, directory)).map(
686
+ // Since the URLs are normalized we add `^` and `$` to the RegExp to make sure they match exactly.
687
+ (url21) => `^${normalizeURL(url21)}$`
688
+ )
689
+ )
690
+ ).map((urlReg) => {
691
+ urlReg = urlReg.replaceAll(/\[.*]/g, "((?!.+?\\..+?).*?)");
692
+ return new RegExp(urlReg);
693
+ });
694
+ var getUrlFromAppDirectory = (urlPrefix, directories) => Array.from(
695
+ // De-duplicate similar pages across multiple directories.
696
+ new Set(
697
+ directories.map((directory) => parseUrlForAppDir(urlPrefix, directory)).flat().map(
698
+ // Since the URLs are normalized we add `^` and `$` to the RegExp to make sure they match exactly.
699
+ (url21) => `^${normalizeAppPath(url21)}$`
700
+ )
701
+ )
702
+ ).map((urlReg) => {
703
+ urlReg = urlReg.replaceAll(/\[.*]/g, "((?!.+?\\..+?).*?)");
704
+ return new RegExp(urlReg);
705
+ });
706
+ var execOnce = (fn) => {
707
+ let used = false;
708
+ let result;
709
+ return (...args) => {
710
+ if (!used) {
711
+ used = true;
712
+ result = fn(...args);
713
+ }
714
+ return result;
715
+ };
716
+ };
717
+ var ensureLeadingSlash = (route) => route.startsWith("/") ? route : `/${route}`;
718
+ var isGroupSegment = (segment) => segment.startsWith("(") && segment.endsWith(")");
719
+
720
+ // src/rules/no-html-link-for-pages.ts
721
+ var pagesDirWarning = execOnce((pagesDirs) => {
722
+ console.warn(
723
+ `Pages directory cannot be found at ${pagesDirs.join(" or ")}. If using a custom path, please configure with the \`no-html-link-for-pages\` rule in your eslint config file.`
724
+ );
725
+ });
726
+ var fsExistsSyncCache = {};
727
+ var memoize = (fn) => {
728
+ const cache = {};
729
+ return (...args) => {
730
+ const key = JSON.stringify(args);
731
+ cache[key] ??= fn(...args);
732
+ return cache[key];
733
+ };
734
+ };
735
+ var cachedGetUrlFromPagesDirectories = memoize(getUrlFromPagesDirectories);
736
+ var cachedGetUrlFromAppDirectory = memoize(getUrlFromAppDirectory);
737
+ var url13 = "https://nextjs.org/docs/messages/no-html-link-for-pages";
738
+ var noHtmlLinkForPages = defineRule({
739
+ /**
740
+ * Creates an ESLint rule listener.
741
+ */
742
+ create: (context) => {
743
+ const ruleOptions = context.options;
744
+ const [customPagesDirectory] = ruleOptions;
745
+ const rootDirs = getRootDirs(context);
746
+ const pagesDirs = (customPagesDirectory ? [customPagesDirectory] : rootDirs.map((dir) => [
747
+ path6.join(dir, "pages"),
748
+ path6.join(dir, "src", "pages")
749
+ ])).flat();
750
+ const foundPagesDirs = pagesDirs.filter((dir) => {
751
+ fsExistsSyncCache[dir] ??= fs2.existsSync(dir);
752
+ return fsExistsSyncCache[dir];
753
+ });
754
+ const appDirs = rootDirs.map((dir) => [path6.join(dir, "app"), path6.join(dir, "src", "app")]).flat();
755
+ const foundAppDirs = appDirs.filter((dir) => {
756
+ fsExistsSyncCache[dir] ??= fs2.existsSync(dir);
757
+ return fsExistsSyncCache[dir];
758
+ });
759
+ if (foundPagesDirs.length === 0 && foundAppDirs.length === 0) {
760
+ pagesDirWarning(pagesDirs);
761
+ return {};
762
+ }
763
+ const pageUrls = cachedGetUrlFromPagesDirectories("/", foundPagesDirs);
764
+ const appDirUrls = cachedGetUrlFromAppDirectory("/", foundAppDirs);
765
+ const allUrlRegex = [...pageUrls, ...appDirUrls];
766
+ return {
767
+ JSXOpeningElement: (node) => {
768
+ if (node.name.name !== "a") {
769
+ return;
770
+ }
771
+ if (node.attributes.length === 0) {
772
+ return;
773
+ }
774
+ const target = node.attributes.find(
775
+ (attr) => attr.type === "JSXAttribute" && attr.name.name === "target"
776
+ );
777
+ if (target && target.value.value === "_blank") {
778
+ return;
779
+ }
780
+ const href = node.attributes.find(
781
+ (attr) => attr.type === "JSXAttribute" && attr.name.name === "href"
782
+ );
783
+ if (!href || href.value && href.value.type !== "Literal") {
784
+ return;
785
+ }
786
+ const hasDownloadAttr = node.attributes.find(
787
+ (attr) => attr.type === "JSXAttribute" && attr.name.name === "download"
788
+ );
789
+ if (hasDownloadAttr) {
790
+ return;
791
+ }
792
+ const hrefPath = normalizeURL(href.value.value);
793
+ if (/^(?<temp1>https?:\/\/|\/\/)/.test(hrefPath)) {
794
+ return;
795
+ }
796
+ allUrlRegex.forEach((foundUrl) => {
797
+ if (hrefPath && foundUrl.test(normalizeURL(hrefPath))) {
798
+ context.report({
799
+ message: `Do not use an \`<a>\` element to navigate to \`${hrefPath}\`. Use \`<Link />\` from \`next/link\` instead. See: ${url13}`,
800
+ node
801
+ });
802
+ }
803
+ });
804
+ }
805
+ };
806
+ },
807
+ meta: {
808
+ docs: {
809
+ category: "HTML",
810
+ description: "Prevent usage of `<a>` elements to navigate to internal Next.js pages.",
811
+ recommended: true,
812
+ url: url13
813
+ },
814
+ schema: [
815
+ {
816
+ oneOf: [
817
+ {
818
+ type: "string"
819
+ },
820
+ {
821
+ items: {
822
+ type: "string"
823
+ },
824
+ type: "array",
825
+ uniqueItems: true
826
+ }
827
+ ]
828
+ }
829
+ ],
830
+ type: "problem"
831
+ }
832
+ });
833
+
834
+ // src/rules/no-img-element.ts
835
+ import path7 from "node:path";
836
+ var url14 = "https://nextjs.org/docs/messages/no-img-element";
837
+ var noImgElement = defineRule({
838
+ create: (context) => {
839
+ const relativePath = context.filename.replace(path7.sep, "/").replace(context.cwd, "").replace(/^\//, "");
840
+ const isAppDir = /^(?<temp1>src\/)?app\//.test(relativePath);
841
+ return {
842
+ JSXOpeningElement: (node) => {
843
+ if (node.name.name !== "img") {
844
+ return;
845
+ }
846
+ if (node.attributes.length === 0) {
847
+ return;
848
+ }
849
+ if (node.parent?.parent?.openingElement?.name?.name === "picture") {
850
+ return;
851
+ }
852
+ if (isAppDir && /\/opengraph-image|twitter-image|icon\.\w+$/.test(relativePath)) {
853
+ return;
854
+ }
855
+ context.report({
856
+ message: `Using \`<img>\` could result in slower LCP and higher bandwidth. Consider using \`<Image />\` from \`next/image\` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: ${url14}`,
857
+ node
858
+ });
859
+ }
860
+ };
861
+ },
862
+ meta: {
863
+ docs: {
864
+ category: "HTML",
865
+ description: "Prevent usage of `<img>` element due to slower LCP and higher bandwidth.",
866
+ recommended: true,
867
+ url: url14
868
+ },
869
+ schema: [],
870
+ type: "problem"
871
+ }
872
+ });
873
+
874
+ // src/rules/no-page-custom-font.ts
875
+ import { posix as posix2, sep as sep3 } from "node:path";
876
+ var url15 = "https://nextjs.org/docs/messages/no-page-custom-font";
877
+ var isIdentifierMatch = (id1, id2) => id1 === null && id2 === null || id1 && id2 && id1.name === id2.name;
878
+ var noPageCustomFont = defineRule({
879
+ create: (context) => {
880
+ const { sourceCode } = context;
881
+ const paths = context.filename.split("pages");
882
+ const page = paths[paths.length - 1];
883
+ if (!page) {
884
+ return {};
885
+ }
886
+ const isDocument = (
887
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
888
+ page.startsWith(`${sep3}_document`) || page.startsWith(`${posix2.sep}_document`)
889
+ );
890
+ let documentImportName;
891
+ let localDefaultExportId;
892
+ let exportDeclarationType;
893
+ return {
894
+ ExportDefaultDeclaration: (node) => {
895
+ exportDeclarationType = node.declaration.type;
896
+ if (node.declaration.type === "FunctionDeclaration") {
897
+ localDefaultExportId = node.declaration.id;
898
+ return;
899
+ }
900
+ if (node.declaration.type === "ClassDeclaration" && node.declaration.superClass && "name" in node.declaration.superClass && node.declaration.superClass.name === documentImportName) {
901
+ localDefaultExportId = node.declaration.id;
902
+ }
903
+ },
904
+ ImportDeclaration: (node) => {
905
+ if (node.source.value === "next/document") {
906
+ const documentImport = node.specifiers.find(
907
+ ({ type }) => type === "ImportDefaultSpecifier"
908
+ );
909
+ if (documentImport?.local) {
910
+ documentImportName = documentImport.local.name;
911
+ }
912
+ }
913
+ },
914
+ JSXOpeningElement: (node) => {
915
+ if (node.name.name !== "link") {
916
+ return;
917
+ }
918
+ const ancestors = sourceCode.getAncestors(node);
919
+ if (!localDefaultExportId) {
920
+ const program = ancestors.find(
921
+ (ancestor) => ancestor.type === "Program"
922
+ );
923
+ for (let i = 0; i <= program.tokens.length - 1; i++) {
924
+ if (localDefaultExportId) {
925
+ break;
926
+ }
927
+ const token = program.tokens[i];
928
+ if (token?.type === "Keyword" && token.value === "export") {
929
+ const nextToken = program.tokens[i + 1];
930
+ if (nextToken && nextToken.type === "Keyword" && nextToken.value === "default") {
931
+ const maybeIdentifier = program.tokens[i + 2];
932
+ if (maybeIdentifier && maybeIdentifier.type === "Identifier") {
933
+ localDefaultExportId = { name: maybeIdentifier.value };
934
+ }
935
+ }
936
+ }
937
+ }
938
+ }
939
+ const parentComponent = ancestors.find((ancestor) => {
940
+ if (exportDeclarationType === "ClassDeclaration") {
941
+ return ancestor.type === exportDeclarationType && "superClass" in ancestor && ancestor.superClass && "name" in ancestor.superClass && ancestor.superClass.name === documentImportName;
942
+ }
943
+ if ("id" in ancestor) {
944
+ if (exportDeclarationType === "FunctionDeclaration") {
945
+ return ancestor.type === exportDeclarationType && isIdentifierMatch(ancestor.id, localDefaultExportId);
946
+ }
947
+ return isIdentifierMatch(ancestor.id, localDefaultExportId);
948
+ }
949
+ return false;
950
+ });
951
+ if (isDocument && parentComponent) {
952
+ return;
953
+ }
954
+ const attributes = new NodeAttributes(node);
955
+ if (!attributes.has("href") || !attributes.hasValue("href")) {
956
+ return;
957
+ }
958
+ const hrefValue = attributes.value("href");
959
+ const isGoogleFont = typeof hrefValue === "string" && hrefValue.startsWith("https://fonts.googleapis.com/css");
960
+ if (isGoogleFont) {
961
+ const end = `This is discouraged. See: ${url15}`;
962
+ const message2 = isDocument ? `Using \`<link />\` outside of \`<Head>\` will disable automatic font optimization. ${end}` : `Custom fonts not added in \`pages/_document.js\` will only load for a single page. ${end}`;
963
+ context.report({
964
+ message: message2,
965
+ node
966
+ });
967
+ }
968
+ }
969
+ };
970
+ },
971
+ meta: {
972
+ docs: {
973
+ description: "Prevent page-only custom fonts.",
974
+ recommended: true,
975
+ url: url15
976
+ },
977
+ schema: [],
978
+ type: "problem"
979
+ }
980
+ });
981
+
982
+ // src/rules/no-script-component-in-head.ts
983
+ var url16 = "https://nextjs.org/docs/messages/no-script-component-in-head";
984
+ var noScriptComponentInHead = defineRule({
985
+ create: (context) => {
986
+ let isNextHead = null;
987
+ return {
988
+ ImportDeclaration: (node) => {
989
+ if (node.source.value === "next/head") {
990
+ isNextHead = node.source.value;
991
+ }
992
+ },
993
+ JSXElement: (node) => {
994
+ if (!isNextHead) {
995
+ return;
996
+ }
997
+ if (node.openingElement?.name && node.openingElement.name.name !== "Head") {
998
+ return;
999
+ }
1000
+ const scriptTag = node.children.find(
1001
+ (child) => child.openingElement?.name && child.openingElement.name.name === "Script"
1002
+ );
1003
+ if (scriptTag) {
1004
+ context.report({
1005
+ message: `\`next/script\` should not be used in \`next/head\` component. Move \`<Script />\` outside of \`<Head>\` instead. See: ${url16}`,
1006
+ node
1007
+ });
1008
+ }
1009
+ }
1010
+ };
1011
+ },
1012
+ meta: {
1013
+ docs: {
1014
+ description: "Prevent usage of `next/script` in `next/head` component.",
1015
+ recommended: true,
1016
+ url: url16
1017
+ },
1018
+ schema: [],
1019
+ type: "problem"
1020
+ }
1021
+ });
1022
+
1023
+ // src/rules/no-styled-jsx-in-document.ts
1024
+ import * as path8 from "node:path";
1025
+ var url17 = "https://nextjs.org/docs/messages/no-styled-jsx-in-document";
1026
+ var noStyledJsxInDocument = defineRule({
1027
+ create: (context) => ({
1028
+ JSXOpeningElement: (node) => {
1029
+ const document = context.filename.split("pages", 2)[1];
1030
+ if (!document) {
1031
+ return;
1032
+ }
1033
+ const { dir, name: name2 } = path8.parse(document);
1034
+ if (!(name2.startsWith("_document") || dir === "/_document" && name2 === "index")) {
1035
+ return;
1036
+ }
1037
+ if (node.name.name === "style" && node.attributes.find(
1038
+ (attr) => attr.type === "JSXAttribute" && attr.name.name === "jsx"
1039
+ )) {
1040
+ context.report({
1041
+ message: `\`styled-jsx\` should not be used in \`pages/_document.js\`. See: ${url17}`,
1042
+ node
1043
+ });
1044
+ }
1045
+ }
1046
+ }),
1047
+ meta: {
1048
+ docs: {
1049
+ description: "Prevent usage of `styled-jsx` in `pages/_document.js`.",
1050
+ recommended: true,
1051
+ url: url17
1052
+ },
1053
+ schema: [],
1054
+ type: "problem"
1055
+ }
1056
+ });
1057
+
1058
+ // src/rules/no-sync-scripts.ts
1059
+ var url18 = "https://nextjs.org/docs/messages/no-sync-scripts";
1060
+ var noSyncScripts = defineRule({
1061
+ create: (context) => ({
1062
+ JSXOpeningElement: (node) => {
1063
+ if (node.name.name !== "script") {
1064
+ return;
1065
+ }
1066
+ if (node.attributes.length === 0) {
1067
+ return;
1068
+ }
1069
+ const attributeNames = node.attributes.filter((attr) => attr.type === "JSXAttribute").map((attr) => attr.name.name);
1070
+ if (attributeNames.includes("src") && !attributeNames.includes("async") && !attributeNames.includes("defer")) {
1071
+ context.report({
1072
+ message: `Synchronous scripts should not be used. See: ${url18}`,
1073
+ node
1074
+ });
1075
+ }
1076
+ }
1077
+ }),
1078
+ meta: {
1079
+ docs: {
1080
+ description: "Prevent synchronous scripts.",
1081
+ recommended: true,
1082
+ url: url18
1083
+ },
1084
+ schema: [],
1085
+ type: "problem"
1086
+ }
1087
+ });
1088
+
1089
+ // src/rules/no-title-in-document-head.ts
1090
+ var url19 = "https://nextjs.org/docs/messages/no-title-in-document-head";
1091
+ var noTitleInDocumentHead = defineRule({
1092
+ create: (context) => {
1093
+ let headFromNextDocument = false;
1094
+ return {
1095
+ ImportDeclaration: (node) => {
1096
+ if (node.source.value === "next/document") {
1097
+ if (node.specifiers.some(
1098
+ ({ local }) => local.name === "Head"
1099
+ )) {
1100
+ headFromNextDocument = true;
1101
+ }
1102
+ }
1103
+ },
1104
+ JSXElement: (node) => {
1105
+ if (!headFromNextDocument) {
1106
+ return;
1107
+ }
1108
+ if (node.openingElement?.name && node.openingElement.name.name !== "Head") {
1109
+ return;
1110
+ }
1111
+ const titleTag = node.children.find(
1112
+ (child) => child.openingElement?.name && child.openingElement.name.type === "JSXIdentifier" && child.openingElement.name.name === "title"
1113
+ );
1114
+ if (titleTag) {
1115
+ context.report({
1116
+ message: `Do not use \`<title>\` element with \`<Head />\` component from \`next/document\`. Titles should defined at the page-level using \`<Head />\` from \`next/head\` instead. See: ${url19}`,
1117
+ node: titleTag
1118
+ });
1119
+ }
1120
+ }
1121
+ };
1122
+ },
1123
+ meta: {
1124
+ docs: {
1125
+ description: "Prevent usage of `<title>` with `Head` component from `next/document`.",
1126
+ recommended: true,
1127
+ url: url19
1128
+ },
1129
+ schema: [],
1130
+ type: "problem"
1131
+ }
1132
+ });
1133
+
1134
+ // src/rules/no-typos.ts
1135
+ import * as path9 from "node:path";
1136
+ var NEXT_EXPORT_FUNCTIONS = [
1137
+ "getStaticProps",
1138
+ "getStaticPaths",
1139
+ "getServerSideProps"
1140
+ ];
1141
+ var THRESHOLD = 1;
1142
+ var minDistance = (a, b) => {
1143
+ const m = a.length;
1144
+ const n = b.length;
1145
+ if (m < n) {
1146
+ return minDistance(b, a);
1147
+ }
1148
+ if (n === 0) {
1149
+ return m;
1150
+ }
1151
+ let previousRow = Array.from({ length: n + 1 }, (_, i) => i);
1152
+ for (let i = 0; i < m; i++) {
1153
+ const s1 = a[i];
1154
+ const currentRow = [i + 1];
1155
+ for (let j = 0; j < n; j++) {
1156
+ const s2 = b[j];
1157
+ const insertions = previousRow[j + 1] + 1;
1158
+ const deletions = currentRow[j] + 1;
1159
+ const substitutions = previousRow[j] + Number(s1 !== s2);
1160
+ currentRow.push(Math.min(insertions, deletions, substitutions));
1161
+ }
1162
+ previousRow = currentRow;
1163
+ }
1164
+ return previousRow[previousRow.length - 1];
1165
+ };
1166
+ var noTypos = defineRule({
1167
+ create: (context) => {
1168
+ const checkTypos = (node, name2) => {
1169
+ if (NEXT_EXPORT_FUNCTIONS.includes(name2)) {
1170
+ return;
1171
+ }
1172
+ const potentialTypos = NEXT_EXPORT_FUNCTIONS.map((o) => ({
1173
+ distance: minDistance(o, name2) ?? Infinity,
1174
+ option: o
1175
+ })).filter(({ distance }) => distance <= THRESHOLD && distance > 0).sort((a, b) => a.distance - b.distance);
1176
+ if (potentialTypos.length) {
1177
+ context.report({
1178
+ message: `${name2} may be a typo. Did you mean ${potentialTypos[0]?.option}?`,
1179
+ node
1180
+ });
1181
+ }
1182
+ };
1183
+ return {
1184
+ ExportNamedDeclaration: (node) => {
1185
+ const page = context.filename.split("pages", 2)[1];
1186
+ if (!page || path9.parse(page).dir.startsWith("/api")) {
1187
+ return;
1188
+ }
1189
+ const decl = node.declaration;
1190
+ if (!decl) {
1191
+ return;
1192
+ }
1193
+ switch (decl.type) {
1194
+ case "FunctionDeclaration": {
1195
+ checkTypos(node, decl.id.name);
1196
+ break;
1197
+ }
1198
+ case "VariableDeclaration": {
1199
+ decl.declarations.forEach((d) => {
1200
+ if (d.id.type !== "Identifier") {
1201
+ return;
1202
+ }
1203
+ checkTypos(node, d.id.name);
1204
+ });
1205
+ break;
1206
+ }
1207
+ default: {
1208
+ break;
1209
+ }
1210
+ }
1211
+ }
1212
+ };
1213
+ },
1214
+ meta: {
1215
+ docs: {
1216
+ description: "Prevent common typos in Next.js data fetching functions.",
1217
+ recommended: true
1218
+ },
1219
+ schema: [],
1220
+ type: "problem"
1221
+ }
1222
+ });
1223
+
1224
+ // src/rules/no-unwanted-polyfillio.ts
1225
+ var NEXT_POLYFILLED_FEATURES = [
1226
+ "Array.prototype.@@iterator",
1227
+ "Array.prototype.at",
1228
+ "Array.prototype.copyWithin",
1229
+ "Array.prototype.fill",
1230
+ "Array.prototype.find",
1231
+ "Array.prototype.findIndex",
1232
+ "Array.prototype.flatMap",
1233
+ "Array.prototype.flat",
1234
+ "Array.from",
1235
+ "Array.prototype.includes",
1236
+ "Array.of",
1237
+ "Function.prototype.name",
1238
+ "fetch",
1239
+ "Map",
1240
+ "Number.EPSILON",
1241
+ "Number.Epsilon",
1242
+ "Number.isFinite",
1243
+ "Number.isNaN",
1244
+ "Number.isInteger",
1245
+ "Number.isSafeInteger",
1246
+ "Number.MAX_SAFE_INTEGER",
1247
+ "Number.MIN_SAFE_INTEGER",
1248
+ "Number.parseFloat",
1249
+ "Number.parseInt",
1250
+ "Object.assign",
1251
+ "Object.entries",
1252
+ "Object.fromEntries",
1253
+ "Object.getOwnPropertyDescriptor",
1254
+ "Object.getOwnPropertyDescriptors",
1255
+ "Object.hasOwn",
1256
+ "Object.is",
1257
+ "Object.keys",
1258
+ "Object.values",
1259
+ "Reflect",
1260
+ "Set",
1261
+ "Symbol",
1262
+ "Symbol.asyncIterator",
1263
+ "String.prototype.codePointAt",
1264
+ "String.prototype.endsWith",
1265
+ "String.fromCodePoint",
1266
+ "String.prototype.includes",
1267
+ "String.prototype.@@iterator",
1268
+ "String.prototype.padEnd",
1269
+ "String.prototype.padStart",
1270
+ "String.prototype.repeat",
1271
+ "String.raw",
1272
+ "String.prototype.startsWith",
1273
+ "String.prototype.trimEnd",
1274
+ "String.prototype.trimStart",
1275
+ "URL",
1276
+ "URL.prototype.toJSON",
1277
+ "URLSearchParams",
1278
+ "WeakMap",
1279
+ "WeakSet",
1280
+ "Promise",
1281
+ "Promise.prototype.finally",
1282
+ "es2015",
1283
+ // Should be covered by babel-preset-env instead.
1284
+ "es2016",
1285
+ // contains polyfilled 'Array.prototype.includes', 'String.prototype.padEnd' and 'String.prototype.padStart'
1286
+ "es2017",
1287
+ // contains polyfilled 'Object.entries', 'Object.getOwnPropertyDescriptors', 'Object.values', 'String.prototype.padEnd' and 'String.prototype.padStart'
1288
+ "es2018",
1289
+ // contains polyfilled 'Promise.prototype.finally' and ''Symbol.asyncIterator'
1290
+ "es2019",
1291
+ // Contains polyfilled 'Object.fromEntries' and polyfilled 'Array.prototype.flat', 'Array.prototype.flatMap', 'String.prototype.trimEnd' and 'String.prototype.trimStart'
1292
+ "es5",
1293
+ // Should be covered by babel-preset-env instead.
1294
+ "es6",
1295
+ // Should be covered by babel-preset-env instead.
1296
+ "es7"
1297
+ // contains polyfilled 'Array.prototype.includes', 'String.prototype.padEnd' and 'String.prototype.padStart'
1298
+ ];
1299
+ var url20 = "https://nextjs.org/docs/messages/no-unwanted-polyfillio";
1300
+ var noUnwantedPolyfillio = defineRule({
1301
+ create: (context) => {
1302
+ let scriptImport = null;
1303
+ return {
1304
+ ImportDeclaration: (node) => {
1305
+ if (node.source && node.source.value === "next/script") {
1306
+ scriptImport = node.specifiers[0].local.name;
1307
+ }
1308
+ },
1309
+ JSXOpeningElement: (node) => {
1310
+ if (node.name && node.name.name !== "script" && node.name.name !== scriptImport) {
1311
+ return;
1312
+ }
1313
+ if (node.attributes.length === 0) {
1314
+ return;
1315
+ }
1316
+ const srcNode = node.attributes.find(
1317
+ (attr) => attr.type === "JSXAttribute" && attr.name.name === "src"
1318
+ );
1319
+ if (!srcNode || srcNode.value.type !== "Literal") {
1320
+ return;
1321
+ }
1322
+ const src = srcNode.value.value;
1323
+ if (src.startsWith("https://cdn.polyfill.io/v2/") || src.startsWith("https://polyfill.io/v3/") || // https://community.fastly.com/t/new-options-for-polyfill-io-users/2540
1324
+ src.startsWith("https://polyfill-fastly.net/") || src.startsWith("https://polyfill-fastly.io/") || // https://blog.cloudflare.com/polyfill-io-now-available-on-cdnjs-reduce-your-supply-chain-risk
1325
+ src.startsWith("https://cdnjs.cloudflare.com/polyfill/")) {
1326
+ const featureQueryString = new URL(src).searchParams.get("features");
1327
+ const featuresRequested = (featureQueryString ?? "").split(",");
1328
+ const unwantedFeatures = featuresRequested.filter(
1329
+ (feature) => NEXT_POLYFILLED_FEATURES.includes(feature)
1330
+ );
1331
+ if (unwantedFeatures.length > 0) {
1332
+ context.report({
1333
+ message: `No duplicate polyfills from Polyfill.io are allowed. ${unwantedFeatures.join(
1334
+ ", "
1335
+ )} ${unwantedFeatures.length > 1 ? "are" : "is"} already shipped with Next.js. See: ${url20}`,
1336
+ node
1337
+ });
1338
+ }
1339
+ }
1340
+ }
1341
+ };
1342
+ },
1343
+ meta: {
1344
+ docs: {
1345
+ category: "HTML",
1346
+ description: "Prevent duplicate polyfills from Polyfill.io.",
1347
+ recommended: true,
1348
+ url: url20
1349
+ },
1350
+ schema: [],
1351
+ type: "problem"
1352
+ }
1353
+ });
1354
+
1
1355
  // src/index.ts
2
- console.info("eslint-plugin-nextjs");
1356
+ var name = "nextjs";
1357
+ var plugin = {
1358
+ name,
1359
+ rules: {
1360
+ [`${name}/google-font-display`]: googleFontDisplay,
1361
+ [`${name}/google-font-preconnect`]: googleFontPreconnect,
1362
+ [`${name}/inline-script-id`]: inlineScriptId,
1363
+ [`${name}/next-script-for-ga`]: nextScriptForGa,
1364
+ [`${name}/no-assign-module-variable`]: noAssignModuleVariable,
1365
+ [`${name}/no-async-client-component`]: noAsyncClientComponent,
1366
+ [`${name}/no-before-interactive-script-outside-document`]: noBeforeInteractiveScriptOutsideDocument,
1367
+ [`${name}/no-css-tags`]: noCssTags,
1368
+ [`${name}/no-document-import-in-page`]: noDocumentImportInPage,
1369
+ [`${name}/no-duplicate-head`]: noDuplicateHead,
1370
+ [`${name}/no-head-element`]: noHeadElement,
1371
+ [`${name}/no-head-import-in-document`]: noHeadImportInDocument,
1372
+ [`${name}/no-html-link-for-pages`]: noHtmlLinkForPages,
1373
+ [`${name}/no-img-element`]: noImgElement,
1374
+ [`${name}/no-page-custom-font`]: noPageCustomFont,
1375
+ [`${name}/no-script-component-in-head`]: noScriptComponentInHead,
1376
+ [`${name}/no-styled-jsx-in-document`]: noStyledJsxInDocument,
1377
+ [`${name}/no-sync-scripts`]: noSyncScripts,
1378
+ [`${name}/no-title-in-document-head`]: noTitleInDocumentHead,
1379
+ [`${name}/no-typos`]: noTypos,
1380
+ [`${name}/no-unwanted-polyfillio`]: noUnwantedPolyfillio
1381
+ }
1382
+ };
1383
+ var recommendedRules = {
1384
+ // warnings
1385
+ "nextjs/google-font-display": "warn",
1386
+ "nextjs/google-font-preconnect": "warn",
1387
+ // errors
1388
+ "nextjs/inline-script-id": "error",
1389
+ "nextjs/next-script-for-ga": "warn",
1390
+ "nextjs/no-assign-module-variable": "error",
1391
+ "nextjs/no-async-client-component": "warn",
1392
+ "nextjs/no-before-interactive-script-outside-document": "warn",
1393
+ "nextjs/no-css-tags": "warn",
1394
+ "nextjs/no-document-import-in-page": "error",
1395
+ "nextjs/no-duplicate-head": "error",
1396
+ "nextjs/no-head-element": "warn",
1397
+ "nextjs/no-head-import-in-document": "error",
1398
+ "nextjs/no-html-link-for-pages": "warn",
1399
+ "nextjs/no-img-element": "warn",
1400
+ "nextjs/no-page-custom-font": "warn",
1401
+ "nextjs/no-script-component-in-head": "error",
1402
+ "nextjs/no-styled-jsx-in-document": "warn",
1403
+ "nextjs/no-sync-scripts": "warn",
1404
+ "nextjs/no-title-in-document-head": "warn",
1405
+ "nextjs/no-typos": "warn",
1406
+ "nextjs/no-unwanted-polyfillio": "warn"
1407
+ };
1408
+ var coreWebVitalsRules = {
1409
+ ...recommendedRules,
1410
+ "nextjs/no-html-link-for-pages": "error",
1411
+ "nextjs/no-sync-scripts": "error"
1412
+ };
1413
+ var createRuleConfig = (pluginName, rules2, isFlat = false) => {
1414
+ return {
1415
+ plugins: isFlat ? { [pluginName]: plugin } : [pluginName],
1416
+ rules: rules2
1417
+ };
1418
+ };
1419
+ var recommendedFlatConfig = createRuleConfig(name, recommendedRules, true);
1420
+ var recommendedLegacyConfig = createRuleConfig(name, recommendedRules, false);
1421
+ var coreWebVitalsFlatConfig = createRuleConfig(
1422
+ name,
1423
+ coreWebVitalsRules,
1424
+ true
1425
+ );
1426
+ var coreWebVitalsLegacyConfig = createRuleConfig(
1427
+ name,
1428
+ coreWebVitalsRules,
1429
+ false
1430
+ );
1431
+ var index_default = {
1432
+ ...plugin,
1433
+ configs: {
1434
+ /**
1435
+ * Legacy config (ESLint < v9) with Core Web Vitals rules (recommended with some warnings upgrade to errors)
1436
+ */
1437
+ "core-web-vitals": coreWebVitalsLegacyConfig,
1438
+ /**
1439
+ * Flat config (ESLint v9+) with Core Web Vitals rules (recommended with some warnings upgrade to errors)
1440
+ */
1441
+ "core-web-vitals/flat": coreWebVitalsFlatConfig,
1442
+ /**
1443
+ * Legacy config (ESLint < v9) with recommended rules
1444
+ */
1445
+ recommended: recommendedLegacyConfig,
1446
+ /**
1447
+ * Flat config (ESLint v9+) with recommended rules
1448
+ */
1449
+ "recommended/flat": recommendedFlatConfig
1450
+ }
1451
+ };
1452
+ var rules = plugin.rules;
1453
+ export {
1454
+ index_default as default,
1455
+ rules
1456
+ };
3
1457
  //# sourceMappingURL=index.js.map