oxlint-plugin-react-doctor 0.3.0 → 0.4.0-dev.a5c6993

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/index.d.ts CHANGED
@@ -1829,6 +1829,44 @@ declare const REACT_DOCTOR_RULES: readonly [{
1829
1829
  readonly recommendation?: string;
1830
1830
  readonly create: (context: RuleContext) => RuleVisitors;
1831
1831
  };
1832
+ }, {
1833
+ readonly key: "react-doctor/nextjs-error-boundary-missing-use-client";
1834
+ readonly id: "nextjs-error-boundary-missing-use-client";
1835
+ readonly source: "react-doctor";
1836
+ readonly originallyExternal: false;
1837
+ readonly rule: {
1838
+ readonly framework: "nextjs";
1839
+ readonly category: "Bugs";
1840
+ readonly id: string;
1841
+ readonly title?: string;
1842
+ readonly severity: RuleSeverity;
1843
+ readonly requires?: ReadonlyArray<string>;
1844
+ readonly disabledBy?: ReadonlyArray<string>;
1845
+ readonly tags?: ReadonlyArray<string>;
1846
+ readonly defaultEnabled?: boolean;
1847
+ readonly lifecycle?: "retired";
1848
+ readonly recommendation?: string;
1849
+ readonly create: (context: RuleContext) => RuleVisitors;
1850
+ };
1851
+ }, {
1852
+ readonly key: "react-doctor/nextjs-global-error-missing-html-body";
1853
+ readonly id: "nextjs-global-error-missing-html-body";
1854
+ readonly source: "react-doctor";
1855
+ readonly originallyExternal: false;
1856
+ readonly rule: {
1857
+ readonly framework: "nextjs";
1858
+ readonly category: "Bugs";
1859
+ readonly id: string;
1860
+ readonly title?: string;
1861
+ readonly severity: RuleSeverity;
1862
+ readonly requires?: ReadonlyArray<string>;
1863
+ readonly disabledBy?: ReadonlyArray<string>;
1864
+ readonly tags?: ReadonlyArray<string>;
1865
+ readonly defaultEnabled?: boolean;
1866
+ readonly lifecycle?: "retired";
1867
+ readonly recommendation?: string;
1868
+ readonly create: (context: RuleContext) => RuleVisitors;
1869
+ };
1832
1870
  }, {
1833
1871
  readonly key: "react-doctor/nextjs-image-missing-sizes";
1834
1872
  readonly id: "nextjs-image-missing-sizes";
@@ -1962,6 +2000,44 @@ declare const REACT_DOCTOR_RULES: readonly [{
1962
2000
  readonly recommendation?: string;
1963
2001
  readonly create: (context: RuleContext) => RuleVisitors;
1964
2002
  };
2003
+ }, {
2004
+ readonly key: "react-doctor/nextjs-no-default-export-in-route-handler";
2005
+ readonly id: "nextjs-no-default-export-in-route-handler";
2006
+ readonly source: "react-doctor";
2007
+ readonly originallyExternal: false;
2008
+ readonly rule: {
2009
+ readonly framework: "nextjs";
2010
+ readonly category: "Bugs";
2011
+ readonly id: string;
2012
+ readonly title?: string;
2013
+ readonly severity: RuleSeverity;
2014
+ readonly requires?: ReadonlyArray<string>;
2015
+ readonly disabledBy?: ReadonlyArray<string>;
2016
+ readonly tags?: ReadonlyArray<string>;
2017
+ readonly defaultEnabled?: boolean;
2018
+ readonly lifecycle?: "retired";
2019
+ readonly recommendation?: string;
2020
+ readonly create: (context: RuleContext) => RuleVisitors;
2021
+ };
2022
+ }, {
2023
+ readonly key: "react-doctor/nextjs-no-edge-og-runtime";
2024
+ readonly id: "nextjs-no-edge-og-runtime";
2025
+ readonly source: "react-doctor";
2026
+ readonly originallyExternal: false;
2027
+ readonly rule: {
2028
+ readonly framework: "nextjs";
2029
+ readonly category: "Bugs";
2030
+ readonly id: string;
2031
+ readonly title?: string;
2032
+ readonly severity: RuleSeverity;
2033
+ readonly requires?: ReadonlyArray<string>;
2034
+ readonly disabledBy?: ReadonlyArray<string>;
2035
+ readonly tags?: ReadonlyArray<string>;
2036
+ readonly defaultEnabled?: boolean;
2037
+ readonly lifecycle?: "retired";
2038
+ readonly recommendation?: string;
2039
+ readonly create: (context: RuleContext) => RuleVisitors;
2040
+ };
1965
2041
  }, {
1966
2042
  readonly key: "react-doctor/nextjs-no-font-link";
1967
2043
  readonly id: "nextjs-no-font-link";
@@ -1981,6 +2057,25 @@ declare const REACT_DOCTOR_RULES: readonly [{
1981
2057
  readonly recommendation?: string;
1982
2058
  readonly create: (context: RuleContext) => RuleVisitors;
1983
2059
  };
2060
+ }, {
2061
+ readonly key: "react-doctor/nextjs-no-google-analytics-script";
2062
+ readonly id: "nextjs-no-google-analytics-script";
2063
+ readonly source: "react-doctor";
2064
+ readonly originallyExternal: false;
2065
+ readonly rule: {
2066
+ readonly framework: "nextjs";
2067
+ readonly category: "Bugs";
2068
+ readonly id: string;
2069
+ readonly title?: string;
2070
+ readonly severity: RuleSeverity;
2071
+ readonly requires?: ReadonlyArray<string>;
2072
+ readonly disabledBy?: ReadonlyArray<string>;
2073
+ readonly tags?: ReadonlyArray<string>;
2074
+ readonly defaultEnabled?: boolean;
2075
+ readonly lifecycle?: "retired";
2076
+ readonly recommendation?: string;
2077
+ readonly create: (context: RuleContext) => RuleVisitors;
2078
+ };
1984
2079
  }, {
1985
2080
  readonly key: "react-doctor/nextjs-no-head-import";
1986
2081
  readonly id: "nextjs-no-head-import";
@@ -2076,6 +2171,25 @@ declare const REACT_DOCTOR_RULES: readonly [{
2076
2171
  readonly recommendation?: string;
2077
2172
  readonly create: (context: RuleContext) => RuleVisitors;
2078
2173
  };
2174
+ }, {
2175
+ readonly key: "react-doctor/nextjs-no-script-in-head";
2176
+ readonly id: "nextjs-no-script-in-head";
2177
+ readonly source: "react-doctor";
2178
+ readonly originallyExternal: false;
2179
+ readonly rule: {
2180
+ readonly framework: "nextjs";
2181
+ readonly category: "Bugs";
2182
+ readonly id: string;
2183
+ readonly title?: string;
2184
+ readonly severity: RuleSeverity;
2185
+ readonly requires?: ReadonlyArray<string>;
2186
+ readonly disabledBy?: ReadonlyArray<string>;
2187
+ readonly tags?: ReadonlyArray<string>;
2188
+ readonly defaultEnabled?: boolean;
2189
+ readonly lifecycle?: "retired";
2190
+ readonly recommendation?: string;
2191
+ readonly create: (context: RuleContext) => RuleVisitors;
2192
+ };
2079
2193
  }, {
2080
2194
  readonly key: "react-doctor/nextjs-no-side-effect-in-get-handler";
2081
2195
  readonly id: "nextjs-no-side-effect-in-get-handler";
@@ -2114,6 +2228,25 @@ declare const REACT_DOCTOR_RULES: readonly [{
2114
2228
  readonly recommendation?: string;
2115
2229
  readonly create: (context: RuleContext) => RuleVisitors;
2116
2230
  };
2231
+ }, {
2232
+ readonly key: "react-doctor/nextjs-no-vercel-og-import";
2233
+ readonly id: "nextjs-no-vercel-og-import";
2234
+ readonly source: "react-doctor";
2235
+ readonly originallyExternal: false;
2236
+ readonly rule: {
2237
+ readonly framework: "nextjs";
2238
+ readonly category: "Bugs";
2239
+ readonly id: string;
2240
+ readonly title?: string;
2241
+ readonly severity: RuleSeverity;
2242
+ readonly requires?: ReadonlyArray<string>;
2243
+ readonly disabledBy?: ReadonlyArray<string>;
2244
+ readonly tags?: ReadonlyArray<string>;
2245
+ readonly defaultEnabled?: boolean;
2246
+ readonly lifecycle?: "retired";
2247
+ readonly recommendation?: string;
2248
+ readonly create: (context: RuleContext) => RuleVisitors;
2249
+ };
2117
2250
  }, {
2118
2251
  readonly key: "react-doctor/no-access-key";
2119
2252
  readonly id: "no-access-key";
@@ -8127,6 +8260,44 @@ declare const RULES: readonly [{
8127
8260
  readonly recommendation?: string;
8128
8261
  readonly create: (context: RuleContext) => RuleVisitors;
8129
8262
  };
8263
+ }, {
8264
+ readonly key: "react-doctor/nextjs-error-boundary-missing-use-client";
8265
+ readonly id: "nextjs-error-boundary-missing-use-client";
8266
+ readonly source: "react-doctor";
8267
+ readonly originallyExternal: false;
8268
+ readonly rule: {
8269
+ readonly framework: "nextjs";
8270
+ readonly category: "Bugs";
8271
+ readonly id: string;
8272
+ readonly title?: string;
8273
+ readonly severity: RuleSeverity;
8274
+ readonly requires?: ReadonlyArray<string>;
8275
+ readonly disabledBy?: ReadonlyArray<string>;
8276
+ readonly tags?: ReadonlyArray<string>;
8277
+ readonly defaultEnabled?: boolean;
8278
+ readonly lifecycle?: "retired";
8279
+ readonly recommendation?: string;
8280
+ readonly create: (context: RuleContext) => RuleVisitors;
8281
+ };
8282
+ }, {
8283
+ readonly key: "react-doctor/nextjs-global-error-missing-html-body";
8284
+ readonly id: "nextjs-global-error-missing-html-body";
8285
+ readonly source: "react-doctor";
8286
+ readonly originallyExternal: false;
8287
+ readonly rule: {
8288
+ readonly framework: "nextjs";
8289
+ readonly category: "Bugs";
8290
+ readonly id: string;
8291
+ readonly title?: string;
8292
+ readonly severity: RuleSeverity;
8293
+ readonly requires?: ReadonlyArray<string>;
8294
+ readonly disabledBy?: ReadonlyArray<string>;
8295
+ readonly tags?: ReadonlyArray<string>;
8296
+ readonly defaultEnabled?: boolean;
8297
+ readonly lifecycle?: "retired";
8298
+ readonly recommendation?: string;
8299
+ readonly create: (context: RuleContext) => RuleVisitors;
8300
+ };
8130
8301
  }, {
8131
8302
  readonly key: "react-doctor/nextjs-image-missing-sizes";
8132
8303
  readonly id: "nextjs-image-missing-sizes";
@@ -8260,6 +8431,44 @@ declare const RULES: readonly [{
8260
8431
  readonly recommendation?: string;
8261
8432
  readonly create: (context: RuleContext) => RuleVisitors;
8262
8433
  };
8434
+ }, {
8435
+ readonly key: "react-doctor/nextjs-no-default-export-in-route-handler";
8436
+ readonly id: "nextjs-no-default-export-in-route-handler";
8437
+ readonly source: "react-doctor";
8438
+ readonly originallyExternal: false;
8439
+ readonly rule: {
8440
+ readonly framework: "nextjs";
8441
+ readonly category: "Bugs";
8442
+ readonly id: string;
8443
+ readonly title?: string;
8444
+ readonly severity: RuleSeverity;
8445
+ readonly requires?: ReadonlyArray<string>;
8446
+ readonly disabledBy?: ReadonlyArray<string>;
8447
+ readonly tags?: ReadonlyArray<string>;
8448
+ readonly defaultEnabled?: boolean;
8449
+ readonly lifecycle?: "retired";
8450
+ readonly recommendation?: string;
8451
+ readonly create: (context: RuleContext) => RuleVisitors;
8452
+ };
8453
+ }, {
8454
+ readonly key: "react-doctor/nextjs-no-edge-og-runtime";
8455
+ readonly id: "nextjs-no-edge-og-runtime";
8456
+ readonly source: "react-doctor";
8457
+ readonly originallyExternal: false;
8458
+ readonly rule: {
8459
+ readonly framework: "nextjs";
8460
+ readonly category: "Bugs";
8461
+ readonly id: string;
8462
+ readonly title?: string;
8463
+ readonly severity: RuleSeverity;
8464
+ readonly requires?: ReadonlyArray<string>;
8465
+ readonly disabledBy?: ReadonlyArray<string>;
8466
+ readonly tags?: ReadonlyArray<string>;
8467
+ readonly defaultEnabled?: boolean;
8468
+ readonly lifecycle?: "retired";
8469
+ readonly recommendation?: string;
8470
+ readonly create: (context: RuleContext) => RuleVisitors;
8471
+ };
8263
8472
  }, {
8264
8473
  readonly key: "react-doctor/nextjs-no-font-link";
8265
8474
  readonly id: "nextjs-no-font-link";
@@ -8279,6 +8488,25 @@ declare const RULES: readonly [{
8279
8488
  readonly recommendation?: string;
8280
8489
  readonly create: (context: RuleContext) => RuleVisitors;
8281
8490
  };
8491
+ }, {
8492
+ readonly key: "react-doctor/nextjs-no-google-analytics-script";
8493
+ readonly id: "nextjs-no-google-analytics-script";
8494
+ readonly source: "react-doctor";
8495
+ readonly originallyExternal: false;
8496
+ readonly rule: {
8497
+ readonly framework: "nextjs";
8498
+ readonly category: "Bugs";
8499
+ readonly id: string;
8500
+ readonly title?: string;
8501
+ readonly severity: RuleSeverity;
8502
+ readonly requires?: ReadonlyArray<string>;
8503
+ readonly disabledBy?: ReadonlyArray<string>;
8504
+ readonly tags?: ReadonlyArray<string>;
8505
+ readonly defaultEnabled?: boolean;
8506
+ readonly lifecycle?: "retired";
8507
+ readonly recommendation?: string;
8508
+ readonly create: (context: RuleContext) => RuleVisitors;
8509
+ };
8282
8510
  }, {
8283
8511
  readonly key: "react-doctor/nextjs-no-head-import";
8284
8512
  readonly id: "nextjs-no-head-import";
@@ -8374,6 +8602,25 @@ declare const RULES: readonly [{
8374
8602
  readonly recommendation?: string;
8375
8603
  readonly create: (context: RuleContext) => RuleVisitors;
8376
8604
  };
8605
+ }, {
8606
+ readonly key: "react-doctor/nextjs-no-script-in-head";
8607
+ readonly id: "nextjs-no-script-in-head";
8608
+ readonly source: "react-doctor";
8609
+ readonly originallyExternal: false;
8610
+ readonly rule: {
8611
+ readonly framework: "nextjs";
8612
+ readonly category: "Bugs";
8613
+ readonly id: string;
8614
+ readonly title?: string;
8615
+ readonly severity: RuleSeverity;
8616
+ readonly requires?: ReadonlyArray<string>;
8617
+ readonly disabledBy?: ReadonlyArray<string>;
8618
+ readonly tags?: ReadonlyArray<string>;
8619
+ readonly defaultEnabled?: boolean;
8620
+ readonly lifecycle?: "retired";
8621
+ readonly recommendation?: string;
8622
+ readonly create: (context: RuleContext) => RuleVisitors;
8623
+ };
8377
8624
  }, {
8378
8625
  readonly key: "react-doctor/nextjs-no-side-effect-in-get-handler";
8379
8626
  readonly id: "nextjs-no-side-effect-in-get-handler";
@@ -8412,6 +8659,25 @@ declare const RULES: readonly [{
8412
8659
  readonly recommendation?: string;
8413
8660
  readonly create: (context: RuleContext) => RuleVisitors;
8414
8661
  };
8662
+ }, {
8663
+ readonly key: "react-doctor/nextjs-no-vercel-og-import";
8664
+ readonly id: "nextjs-no-vercel-og-import";
8665
+ readonly source: "react-doctor";
8666
+ readonly originallyExternal: false;
8667
+ readonly rule: {
8668
+ readonly framework: "nextjs";
8669
+ readonly category: "Bugs";
8670
+ readonly id: string;
8671
+ readonly title?: string;
8672
+ readonly severity: RuleSeverity;
8673
+ readonly requires?: ReadonlyArray<string>;
8674
+ readonly disabledBy?: ReadonlyArray<string>;
8675
+ readonly tags?: ReadonlyArray<string>;
8676
+ readonly defaultEnabled?: boolean;
8677
+ readonly lifecycle?: "retired";
8678
+ readonly recommendation?: string;
8679
+ readonly create: (context: RuleContext) => RuleVisitors;
8680
+ };
8415
8681
  }, {
8416
8682
  readonly key: "react-doctor/no-access-key";
8417
8683
  readonly id: "no-access-key";
package/dist/index.js CHANGED
@@ -12511,6 +12511,103 @@ const nextjsAsyncClientComponent = defineRule({
12511
12511
  }
12512
12512
  });
12513
12513
  //#endregion
12514
+ //#region src/plugin/constants/nextjs.ts
12515
+ const PAGE_FILE_PATTERN = /\/page\.(tsx?|jsx?)$/;
12516
+ const PAGE_OR_LAYOUT_FILE_PATTERN = /\/(page|layout)\.(tsx?|jsx?)$/;
12517
+ const INTERNAL_PAGE_PATH_PATTERN = /\/(?:(?:\((?:dashboard|admin|settings|account|internal|manage|console|portal|auth|onboarding|app|ee|protected)\))|(?:dashboard|admin|settings|account|internal|manage|console|portal))\//i;
12518
+ const OG_ROUTE_PATTERN = /\/og\b/i;
12519
+ const PAGES_DIRECTORY_PATTERN = /\/pages\//;
12520
+ const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
12521
+ "redirect",
12522
+ "permanentRedirect",
12523
+ "notFound",
12524
+ "forbidden",
12525
+ "unauthorized"
12526
+ ]);
12527
+ const GOOGLE_FONTS_PATTERN = /fonts\.googleapis\.com/;
12528
+ const POLYFILL_SCRIPT_PATTERN = /polyfill\.io|polyfill\.min\.js|cdn\.polyfill/;
12529
+ const APP_DIRECTORY_PATTERN = /\/app\//;
12530
+ const ROUTE_HANDLER_FILE_PATTERN = /\/route\.(tsx?|jsx?)$/;
12531
+ const CRON_ROUTE_PATTERN = /\/(?:cron|jobs\/cron)(?:\/|$)/i;
12532
+ const MUTATING_ROUTE_SEGMENTS = new Set([
12533
+ "logout",
12534
+ "log-out",
12535
+ "signout",
12536
+ "sign-out",
12537
+ "unsubscribe",
12538
+ "delete",
12539
+ "remove",
12540
+ "revoke",
12541
+ "cancel",
12542
+ "deactivate"
12543
+ ]);
12544
+ const ERROR_BOUNDARY_FILE_PATTERN = /\/(error|global-error)\.(tsx?|jsx?)$/;
12545
+ const GLOBAL_ERROR_FILE_PATTERN = /\/global-error\.(tsx?|jsx?)$/;
12546
+ const ROUTE_HANDLER_HTTP_METHODS = new Set([
12547
+ "GET",
12548
+ "POST",
12549
+ "PUT",
12550
+ "PATCH",
12551
+ "DELETE",
12552
+ "OPTIONS",
12553
+ "HEAD"
12554
+ ]);
12555
+ const GOOGLE_ANALYTICS_SCRIPT_PATTERN = /google-analytics\.com|googletagmanager\.com\/gtag/;
12556
+ const OG_IMAGE_FILE_PATTERN = /\/(opengraph-image|twitter-image)\d*\.(tsx?|jsx?)$/;
12557
+ //#endregion
12558
+ //#region src/plugin/rules/nextjs/nextjs-error-boundary-missing-use-client.ts
12559
+ const nextjsErrorBoundaryMissingUseClient = defineRule({
12560
+ id: "nextjs-error-boundary-missing-use-client",
12561
+ title: "Error boundary missing 'use client'",
12562
+ tags: ["test-noise"],
12563
+ requires: ["nextjs"],
12564
+ severity: "error",
12565
+ recommendation: "Add `'use client'` at the top of this file. Error boundaries must be Client Components to catch and render fallback UI",
12566
+ create: (context) => ({ Program(programNode) {
12567
+ const filename = normalizeFilename$1(context.filename ?? "");
12568
+ if (!APP_DIRECTORY_PATTERN.test(filename)) return;
12569
+ if (!ERROR_BOUNDARY_FILE_PATTERN.test(filename)) return;
12570
+ if (hasDirective(programNode, "use client")) return;
12571
+ context.report({
12572
+ node: programNode,
12573
+ message: "This error boundary silently does nothing without 'use client'. Next.js requires error.tsx to be a Client Component."
12574
+ });
12575
+ } })
12576
+ });
12577
+ //#endregion
12578
+ //#region src/plugin/utils/file-contains-jsx-elements.ts
12579
+ const fileContainsJsxElements = (programNode, tagNames) => {
12580
+ const targetTagNames = new Set(tagNames);
12581
+ const foundTagNames = /* @__PURE__ */ new Set();
12582
+ walkAst(programNode, (child) => {
12583
+ if (foundTagNames.size === targetTagNames.size) return false;
12584
+ if (isNodeOfType(child, "JSXOpeningElement") && isNodeOfType(child.name, "JSXIdentifier") && targetTagNames.has(child.name.name)) foundTagNames.add(child.name.name);
12585
+ });
12586
+ return foundTagNames;
12587
+ };
12588
+ //#endregion
12589
+ //#region src/plugin/rules/nextjs/nextjs-global-error-missing-html-body.ts
12590
+ const REQUIRED_HTML_TAGS = ["html", "body"];
12591
+ const nextjsGlobalErrorMissingHtmlBody = defineRule({
12592
+ id: "nextjs-global-error-missing-html-body",
12593
+ title: "global-error.tsx missing <html>/<body>",
12594
+ tags: ["test-noise"],
12595
+ requires: ["nextjs"],
12596
+ severity: "error",
12597
+ recommendation: "Wrap your error UI in `<html><body>...</body></html>`. The root layout is unmounted when global-error renders",
12598
+ create: (context) => ({ Program(programNode) {
12599
+ const filename = normalizeFilename$1(context.filename ?? "");
12600
+ if (!APP_DIRECTORY_PATTERN.test(filename)) return;
12601
+ if (!GLOBAL_ERROR_FILE_PATTERN.test(filename)) return;
12602
+ const foundTags = fileContainsJsxElements(programNode, REQUIRED_HTML_TAGS);
12603
+ const missingTags = REQUIRED_HTML_TAGS.filter((tagName) => !foundTags.has(tagName)).map((tagName) => `<${tagName}>`);
12604
+ if (missingTags.length > 0) context.report({
12605
+ node: programNode,
12606
+ message: `global-error.tsx is missing ${missingTags.join(" and ")}. The root layout unmounts on error, so this page renders broken HTML.`
12607
+ });
12608
+ } })
12609
+ });
12610
+ //#endregion
12514
12611
  //#region src/plugin/utils/has-jsx-attribute.ts
12515
12612
  const hasJsxAttribute = (attributes, attributeName) => Boolean(findJsxAttribute(attributes, attributeName));
12516
12613
  //#endregion
@@ -12554,37 +12651,6 @@ const nextjsInlineScriptMissingId = defineRule({
12554
12651
  } })
12555
12652
  });
12556
12653
  //#endregion
12557
- //#region src/plugin/constants/nextjs.ts
12558
- const PAGE_FILE_PATTERN = /\/page\.(tsx?|jsx?)$/;
12559
- const PAGE_OR_LAYOUT_FILE_PATTERN = /\/(page|layout)\.(tsx?|jsx?)$/;
12560
- const INTERNAL_PAGE_PATH_PATTERN = /\/(?:(?:\((?:dashboard|admin|settings|account|internal|manage|console|portal|auth|onboarding|app|ee|protected)\))|(?:dashboard|admin|settings|account|internal|manage|console|portal))\//i;
12561
- const OG_ROUTE_PATTERN = /\/og\b/i;
12562
- const PAGES_DIRECTORY_PATTERN = /\/pages\//;
12563
- const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
12564
- "redirect",
12565
- "permanentRedirect",
12566
- "notFound",
12567
- "forbidden",
12568
- "unauthorized"
12569
- ]);
12570
- const GOOGLE_FONTS_PATTERN = /fonts\.googleapis\.com/;
12571
- const POLYFILL_SCRIPT_PATTERN = /polyfill\.io|polyfill\.min\.js|cdn\.polyfill/;
12572
- const APP_DIRECTORY_PATTERN = /\/app\//;
12573
- const ROUTE_HANDLER_FILE_PATTERN = /\/route\.(tsx?|jsx?)$/;
12574
- const CRON_ROUTE_PATTERN = /\/(?:cron|jobs\/cron)(?:\/|$)/i;
12575
- const MUTATING_ROUTE_SEGMENTS = new Set([
12576
- "logout",
12577
- "log-out",
12578
- "signout",
12579
- "sign-out",
12580
- "unsubscribe",
12581
- "delete",
12582
- "remove",
12583
- "revoke",
12584
- "cancel",
12585
- "deactivate"
12586
- ]);
12587
- //#endregion
12588
12654
  //#region src/plugin/rules/nextjs/nextjs-missing-metadata.ts
12589
12655
  const nextjsMissingMetadata = defineRule({
12590
12656
  id: "nextjs-missing-metadata",
@@ -12734,6 +12800,89 @@ const nextjsNoCssLink = defineRule({
12734
12800
  } })
12735
12801
  });
12736
12802
  //#endregion
12803
+ //#region src/plugin/rules/nextjs/nextjs-no-default-export-in-route-handler.ts
12804
+ const programHasNamedHttpMethodExport = (programNode) => {
12805
+ for (const statement of programNode.body ?? []) {
12806
+ if (!isNodeOfType(statement, "ExportNamedDeclaration")) continue;
12807
+ const declaration = statement.declaration;
12808
+ if (isNodeOfType(declaration, "FunctionDeclaration") && declaration.id?.name && ROUTE_HANDLER_HTTP_METHODS.has(declaration.id.name)) return true;
12809
+ if (isNodeOfType(declaration, "VariableDeclaration")) {
12810
+ for (const declarator of declaration.declarations ?? []) if (isNodeOfType(declarator.id, "Identifier") && ROUTE_HANDLER_HTTP_METHODS.has(declarator.id.name)) return true;
12811
+ }
12812
+ for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier, "ExportSpecifier") && isNodeOfType(specifier.exported, "Identifier") && ROUTE_HANDLER_HTTP_METHODS.has(specifier.exported.name)) return true;
12813
+ }
12814
+ return false;
12815
+ };
12816
+ const nextjsNoDefaultExportInRouteHandler = defineRule({
12817
+ id: "nextjs-no-default-export-in-route-handler",
12818
+ title: "Default export in route handler",
12819
+ tags: ["test-noise"],
12820
+ requires: ["nextjs"],
12821
+ severity: "error",
12822
+ recommendation: "Replace `export default` with named HTTP method exports: `export async function GET(request) { … }`",
12823
+ create: (context) => {
12824
+ let isAppRouteHandler = false;
12825
+ let programNode = null;
12826
+ return {
12827
+ Program(node) {
12828
+ const filename = normalizeFilename$1(context.filename ?? "");
12829
+ isAppRouteHandler = APP_DIRECTORY_PATTERN.test(filename) && ROUTE_HANDLER_FILE_PATTERN.test(filename);
12830
+ programNode = node;
12831
+ },
12832
+ ExportDefaultDeclaration(node) {
12833
+ if (!isAppRouteHandler || !programNode) return;
12834
+ if (programHasNamedHttpMethodExport(programNode)) return;
12835
+ context.report({
12836
+ node,
12837
+ message: "Default exports in route.ts are silently ignored. Next.js only recognizes named HTTP method exports (GET, POST, etc.)."
12838
+ });
12839
+ },
12840
+ ExportNamedDeclaration(node) {
12841
+ if (!isAppRouteHandler || !programNode) return;
12842
+ if (!(node.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ExportSpecifier") && isNodeOfType(specifier.exported, "Identifier") && specifier.exported.name === "default")) return;
12843
+ if (programHasNamedHttpMethodExport(programNode)) return;
12844
+ context.report({
12845
+ node,
12846
+ message: "Default exports in route.ts are silently ignored. Next.js only recognizes named HTTP method exports (GET, POST, etc.)."
12847
+ });
12848
+ }
12849
+ };
12850
+ }
12851
+ });
12852
+ //#endregion
12853
+ //#region src/plugin/rules/nextjs/nextjs-no-edge-og-runtime.ts
12854
+ const nextjsNoEdgeOgRuntime = defineRule({
12855
+ id: "nextjs-no-edge-og-runtime",
12856
+ title: "Edge runtime in OG image route",
12857
+ tags: ["test-noise"],
12858
+ requires: ["nextjs"],
12859
+ severity: "warn",
12860
+ recommendation: "Remove `export const runtime = 'edge'` from OG image files. The default Node.js runtime supports more fonts and APIs",
12861
+ create: (context) => {
12862
+ let isOgImageFile = false;
12863
+ return {
12864
+ Program() {
12865
+ const filename = normalizeFilename$1(context.filename ?? "");
12866
+ isOgImageFile = OG_IMAGE_FILE_PATTERN.test(filename);
12867
+ },
12868
+ ExportNamedDeclaration(node) {
12869
+ if (!isOgImageFile) return;
12870
+ const declaration = node.declaration;
12871
+ if (!isNodeOfType(declaration, "VariableDeclaration")) return;
12872
+ for (const declarator of declaration.declarations ?? []) {
12873
+ if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
12874
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
12875
+ if (declarator.id.name !== "runtime") continue;
12876
+ if ((isNodeOfType(declarator.init, "Literal") ? declarator.init.value : null) === "edge") context.report({
12877
+ node,
12878
+ message: "Edge runtime limits OG image generation. Node.js runtime supports more fonts, filesystem access, and larger response sizes."
12879
+ });
12880
+ }
12881
+ }
12882
+ };
12883
+ }
12884
+ });
12885
+ //#endregion
12737
12886
  //#region src/plugin/rules/nextjs/nextjs-no-font-link.ts
12738
12887
  const nextjsNoFontLink = defineRule({
12739
12888
  id: "nextjs-no-font-link",
@@ -12754,6 +12903,27 @@ const nextjsNoFontLink = defineRule({
12754
12903
  } })
12755
12904
  });
12756
12905
  //#endregion
12906
+ //#region src/plugin/rules/nextjs/nextjs-no-google-analytics-script.ts
12907
+ const nextjsNoGoogleAnalyticsScript = defineRule({
12908
+ id: "nextjs-no-google-analytics-script",
12909
+ title: "Manual Google Analytics script",
12910
+ tags: ["test-noise"],
12911
+ requires: ["nextjs"],
12912
+ severity: "warn",
12913
+ recommendation: "Use `import { GoogleAnalytics } from '@next/third-parties/google'` for automatic optimization & smaller bundles",
12914
+ create: (context) => ({ JSXOpeningElement(node) {
12915
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
12916
+ if (node.name.name !== "script" && node.name.name !== "Script") return;
12917
+ const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
12918
+ if (!srcAttribute?.value) return;
12919
+ const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
12920
+ if (typeof srcValue === "string" && GOOGLE_ANALYTICS_SCRIPT_PATTERN.test(srcValue)) context.report({
12921
+ node,
12922
+ message: "Manual Google Analytics script blocks rendering. Use @next/third-parties for optimal loading strategy."
12923
+ });
12924
+ } })
12925
+ });
12926
+ //#endregion
12757
12927
  //#region src/plugin/rules/nextjs/nextjs-no-head-import.ts
12758
12928
  const nextjsNoHeadImport = defineRule({
12759
12929
  id: "nextjs-no-head-import",
@@ -12865,6 +13035,32 @@ const nextjsNoRedirectInTryCatch = defineRule({
12865
13035
  }
12866
13036
  });
12867
13037
  //#endregion
13038
+ //#region src/plugin/rules/nextjs/nextjs-no-script-in-head.ts
13039
+ const nextjsNoScriptInHead = defineRule({
13040
+ id: "nextjs-no-script-in-head",
13041
+ title: "next/script inside next/head",
13042
+ tags: ["test-noise"],
13043
+ requires: ["nextjs"],
13044
+ severity: "error",
13045
+ recommendation: "Move `<Script>` outside of `<Head>`. next/script manages its own placement and ignores head context",
13046
+ create: (context) => {
13047
+ let insideHeadDepth = 0;
13048
+ return {
13049
+ JSXOpeningElement(node) {
13050
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
13051
+ if (node.name.name === "Head" && !node.selfClosing) insideHeadDepth++;
13052
+ if (node.name.name === "Script" && insideHeadDepth > 0) context.report({
13053
+ node,
13054
+ message: "next/script inside next/head is silently ignored. Move <Script> outside <Head> so it actually loads."
13055
+ });
13056
+ },
13057
+ JSXClosingElement(node) {
13058
+ if (isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "Head") insideHeadDepth = Math.max(0, insideHeadDepth - 1);
13059
+ }
13060
+ };
13061
+ }
13062
+ });
13063
+ //#endregion
12868
13064
  //#region src/plugin/utils/is-cookies-call.ts
12869
13065
  const isCookiesCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "cookies";
12870
13066
  //#endregion
@@ -13213,6 +13409,23 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
13213
13409
  }
13214
13410
  });
13215
13411
  //#endregion
13412
+ //#region src/plugin/rules/nextjs/nextjs-no-vercel-og-import.ts
13413
+ const nextjsNoVercelOgImport = defineRule({
13414
+ id: "nextjs-no-vercel-og-import",
13415
+ title: "@vercel/og import instead of next/og",
13416
+ tags: ["test-noise"],
13417
+ requires: ["nextjs"],
13418
+ severity: "warn",
13419
+ recommendation: "Use `import { ImageResponse } from \"next/og\"`. The `@vercel/og` package is built into Next.js and should not be imported directly",
13420
+ create: (context) => ({ ImportDeclaration(node) {
13421
+ if (node.source?.value !== "@vercel/og") return;
13422
+ context.report({
13423
+ node,
13424
+ message: "@vercel/og is bundled into Next.js. Import from \"next/og\" instead to avoid duplicate code and version mismatch."
13425
+ });
13426
+ } })
13427
+ });
13428
+ //#endregion
13216
13429
  //#region src/plugin/rules/a11y/no-access-key.ts
13217
13430
  const MESSAGE$30 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
13218
13431
  const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
@@ -32917,15 +33130,6 @@ const serverFetchWithoutRevalidate = defineRule({
32917
33130
  });
32918
33131
  //#endregion
32919
33132
  //#region src/plugin/rules/server/server-hoist-static-io.ts
32920
- const ROUTE_HANDLER_HTTP_METHODS = new Set([
32921
- "GET",
32922
- "POST",
32923
- "PUT",
32924
- "PATCH",
32925
- "DELETE",
32926
- "OPTIONS",
32927
- "HEAD"
32928
- ]);
32929
33133
  const STATIC_IO_FUNCTIONS = new Set([
32930
33134
  "readFileSync",
32931
33135
  "readFile",
@@ -35440,6 +35644,28 @@ const reactDoctorRules = [
35440
35644
  category: "Bugs"
35441
35645
  }
35442
35646
  },
35647
+ {
35648
+ key: "react-doctor/nextjs-error-boundary-missing-use-client",
35649
+ id: "nextjs-error-boundary-missing-use-client",
35650
+ source: "react-doctor",
35651
+ originallyExternal: false,
35652
+ rule: {
35653
+ ...nextjsErrorBoundaryMissingUseClient,
35654
+ framework: "nextjs",
35655
+ category: "Bugs"
35656
+ }
35657
+ },
35658
+ {
35659
+ key: "react-doctor/nextjs-global-error-missing-html-body",
35660
+ id: "nextjs-global-error-missing-html-body",
35661
+ source: "react-doctor",
35662
+ originallyExternal: false,
35663
+ rule: {
35664
+ ...nextjsGlobalErrorMissingHtmlBody,
35665
+ framework: "nextjs",
35666
+ category: "Bugs"
35667
+ }
35668
+ },
35443
35669
  {
35444
35670
  key: "react-doctor/nextjs-image-missing-sizes",
35445
35671
  id: "nextjs-image-missing-sizes",
@@ -35517,6 +35743,28 @@ const reactDoctorRules = [
35517
35743
  category: "Bugs"
35518
35744
  }
35519
35745
  },
35746
+ {
35747
+ key: "react-doctor/nextjs-no-default-export-in-route-handler",
35748
+ id: "nextjs-no-default-export-in-route-handler",
35749
+ source: "react-doctor",
35750
+ originallyExternal: false,
35751
+ rule: {
35752
+ ...nextjsNoDefaultExportInRouteHandler,
35753
+ framework: "nextjs",
35754
+ category: "Bugs"
35755
+ }
35756
+ },
35757
+ {
35758
+ key: "react-doctor/nextjs-no-edge-og-runtime",
35759
+ id: "nextjs-no-edge-og-runtime",
35760
+ source: "react-doctor",
35761
+ originallyExternal: false,
35762
+ rule: {
35763
+ ...nextjsNoEdgeOgRuntime,
35764
+ framework: "nextjs",
35765
+ category: "Bugs"
35766
+ }
35767
+ },
35520
35768
  {
35521
35769
  key: "react-doctor/nextjs-no-font-link",
35522
35770
  id: "nextjs-no-font-link",
@@ -35528,6 +35776,17 @@ const reactDoctorRules = [
35528
35776
  category: "Bugs"
35529
35777
  }
35530
35778
  },
35779
+ {
35780
+ key: "react-doctor/nextjs-no-google-analytics-script",
35781
+ id: "nextjs-no-google-analytics-script",
35782
+ source: "react-doctor",
35783
+ originallyExternal: false,
35784
+ rule: {
35785
+ ...nextjsNoGoogleAnalyticsScript,
35786
+ framework: "nextjs",
35787
+ category: "Bugs"
35788
+ }
35789
+ },
35531
35790
  {
35532
35791
  key: "react-doctor/nextjs-no-head-import",
35533
35792
  id: "nextjs-no-head-import",
@@ -35583,6 +35842,17 @@ const reactDoctorRules = [
35583
35842
  category: "Bugs"
35584
35843
  }
35585
35844
  },
35845
+ {
35846
+ key: "react-doctor/nextjs-no-script-in-head",
35847
+ id: "nextjs-no-script-in-head",
35848
+ source: "react-doctor",
35849
+ originallyExternal: false,
35850
+ rule: {
35851
+ ...nextjsNoScriptInHead,
35852
+ framework: "nextjs",
35853
+ category: "Bugs"
35854
+ }
35855
+ },
35586
35856
  {
35587
35857
  key: "react-doctor/nextjs-no-side-effect-in-get-handler",
35588
35858
  id: "nextjs-no-side-effect-in-get-handler",
@@ -35605,6 +35875,17 @@ const reactDoctorRules = [
35605
35875
  category: "Bugs"
35606
35876
  }
35607
35877
  },
35878
+ {
35879
+ key: "react-doctor/nextjs-no-vercel-og-import",
35880
+ id: "nextjs-no-vercel-og-import",
35881
+ source: "react-doctor",
35882
+ originallyExternal: false,
35883
+ rule: {
35884
+ ...nextjsNoVercelOgImport,
35885
+ framework: "nextjs",
35886
+ category: "Bugs"
35887
+ }
35888
+ },
35608
35889
  {
35609
35890
  key: "react-doctor/no-access-key",
35610
35891
  id: "no-access-key",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.3.0",
3
+ "version": "0.4.0-dev.a5c6993",
4
4
  "description": "oxlint plugin for React Doctor: diagnose React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",