@pyreon/lint 0.12.9 → 0.12.11

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/lib/index.js CHANGED
@@ -670,6 +670,136 @@ const noErrorWithoutPrefix = {
670
670
  }
671
671
  };
672
672
 
673
+ //#endregion
674
+ //#region src/rules/architecture/no-process-dev-gate.ts
675
+ /**
676
+ * `pyreon/no-process-dev-gate` — flag the broken `typeof process` dev-mode gate
677
+ * pattern that is dead code in real Vite browser bundles.
678
+ *
679
+ * The pattern this rule catches:
680
+ *
681
+ * ```ts
682
+ * const __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'
683
+ * ```
684
+ *
685
+ * This works in vitest (Node, `process` is defined) but is **silently dead
686
+ * code in real Vite browser bundles** because Vite does not polyfill
687
+ * `process` for the client. Every dev warning gated on this constant never
688
+ * fires for real users in dev mode.
689
+ *
690
+ * The fix is to use `import.meta.env.DEV`, which Vite/Rolldown literal-replace
691
+ * at build time:
692
+ *
693
+ * ```ts
694
+ * // No const needed — read directly at the use site so the bundler can fold:
695
+ * if (!import.meta.env?.DEV) return
696
+ * ```
697
+ *
698
+ * Vitest sets `import.meta.env.DEV === true` automatically (because it is
699
+ * Vite-based), so existing tests continue to pass.
700
+ *
701
+ * Reference implementation: `packages/fundamentals/flow/src/layout.ts:warnIgnoredOptions`.
702
+ *
703
+ * **Auto-fix**: replaces the assignment with `import.meta.env?.DEV === true`.
704
+ * Does NOT delete the const declaration — that has to happen by hand because
705
+ * the variable name and downstream usages may need updating in callers.
706
+ *
707
+ * **Server-package exception**: server-only files run in Node where `process`
708
+ * is always defined, so the pattern is correct there. The rule skips files
709
+ * matching `packages/zero/`, `packages/core/server/`, `packages/core/runtime-server/`,
710
+ * `packages/tools/vite-plugin/`, and any file containing `/server/` in its
711
+ * path. Add new server packages to `SERVER_PACKAGE_PATTERNS` below.
712
+ */
713
+ /**
714
+ * File-path patterns for server-only packages. Substring match against the
715
+ * file path passed to the rule. Patterns intentionally do NOT start with `/`
716
+ * so they match both absolute paths (`/Users/.../packages/zero/...`) and
717
+ * relative paths (`packages/zero/...`) — different lint runners pass paths
718
+ * differently.
719
+ */
720
+ const SERVER_PACKAGE_PATTERNS = [
721
+ "packages/zero/",
722
+ "packages/core/server/",
723
+ "packages/core/runtime-server/",
724
+ "packages/tools/vite-plugin/",
725
+ "packages/tools/cli/",
726
+ "packages/tools/lint/",
727
+ "packages/tools/mcp/",
728
+ "packages/tools/storybook/",
729
+ "packages/tools/typescript/",
730
+ "scripts/"
731
+ ];
732
+ function isServerOnlyFile(filePath) {
733
+ return SERVER_PACKAGE_PATTERNS.some((pat) => filePath.includes(pat));
734
+ }
735
+ const noProcessDevGate = {
736
+ meta: {
737
+ id: "pyreon/no-process-dev-gate",
738
+ category: "architecture",
739
+ description: "Forbid `typeof process !== \"undefined\" && process.env.NODE_ENV !== \"production\"` as a dev-mode gate. Use `import.meta.env.DEV` instead — `typeof process` is dead code in real Vite browser bundles because Vite does not polyfill `process` for the client.",
740
+ severity: "error",
741
+ fixable: true
742
+ },
743
+ create(context) {
744
+ const filePath = context.getFilePath();
745
+ if (filePath.includes("/tests/") || filePath.includes("/test/") || filePath.includes("/__tests__/") || filePath.includes(".test.") || filePath.includes(".spec.")) return {};
746
+ if (isServerOnlyFile(filePath)) return {};
747
+ /**
748
+ * Match the broken pattern at the AST level. We're looking for any
749
+ * `LogicalExpression` whose two sides are:
750
+ *
751
+ * 1. `typeof process !== 'undefined'` (a UnaryExpression on the LHS
752
+ * of a BinaryExpression with operator `!==`)
753
+ * 2. `process.env.NODE_ENV !== 'production'` (a MemberExpression on
754
+ * the LHS of a BinaryExpression with operator `!==`)
755
+ *
756
+ * The order can be either way (process check first or NODE_ENV check
757
+ * first), and the operator can be `&&` or `||` (we only flag `&&`
758
+ * because `||` doesn't make sense as a dev gate).
759
+ */
760
+ function isTypeofProcessCheck(node) {
761
+ if (node?.type !== "BinaryExpression") return false;
762
+ if (node.operator !== "!==" && node.operator !== "!=") return false;
763
+ const left = node.left;
764
+ const right = node.right;
765
+ if (left?.type !== "UnaryExpression" || left.operator !== "typeof") return false;
766
+ if (left.argument?.type !== "Identifier" || left.argument.name !== "process") return false;
767
+ if ((right?.type === "Literal" || right?.type === "StringLiteral") && right.value === "undefined") return true;
768
+ return false;
769
+ }
770
+ function isNodeEnvCheck(node) {
771
+ if (node?.type !== "BinaryExpression") return false;
772
+ if (node.operator !== "!==" && node.operator !== "!=") return false;
773
+ const left = node.left;
774
+ const right = node.right;
775
+ if (left?.type !== "MemberExpression") return false;
776
+ if (left.object?.type !== "MemberExpression") return false;
777
+ if (left.object.object?.type !== "Identifier" || left.object.object.name !== "process") return false;
778
+ if (left.object.property?.type !== "Identifier" || left.object.property.name !== "env") return false;
779
+ if (left.property?.type !== "Identifier" || left.property.name !== "NODE_ENV") return false;
780
+ if ((right?.type === "Literal" || right?.type === "StringLiteral") && right.value === "production") return true;
781
+ return false;
782
+ }
783
+ function isBrokenDevGate(node) {
784
+ if (node?.type !== "LogicalExpression") return false;
785
+ if (node.operator !== "&&") return false;
786
+ return isTypeofProcessCheck(node.left) && isNodeEnvCheck(node.right) || isNodeEnvCheck(node.left) && isTypeofProcessCheck(node.right);
787
+ }
788
+ return { LogicalExpression(node) {
789
+ if (!isBrokenDevGate(node)) return;
790
+ const span = getSpan(node);
791
+ context.report({
792
+ message: "`typeof process !== \"undefined\" && process.env.NODE_ENV !== \"production\"` is dead code in real Vite browser bundles — Vite does not polyfill `process`, so this guard is `false` and any wrapped dev warnings never fire for real users. Use `import.meta.env.DEV` instead, which Vite literal-replaces at build time and tree-shakes correctly in prod. Reference implementation: `packages/fundamentals/flow/src/layout.ts:warnIgnoredOptions`.",
793
+ span,
794
+ fix: {
795
+ span,
796
+ replacement: "import.meta.env?.DEV === true"
797
+ }
798
+ });
799
+ } };
800
+ }
801
+ };
802
+
673
803
  //#endregion
674
804
  //#region src/rules/form/no-submit-without-validation.ts
675
805
  const noSubmitWithoutValidation = {
@@ -2541,6 +2671,7 @@ const allRules = [
2541
2671
  noCrossLayerImport,
2542
2672
  devGuardWarnings,
2543
2673
  noErrorWithoutPrefix,
2674
+ noProcessDevGate,
2544
2675
  noStoreOutsideProvider,
2545
2676
  noMutateStoreState,
2546
2677
  noDuplicateStoreId,
@@ -2595,7 +2726,8 @@ function buildLib() {
2595
2726
  "pyreon/no-circular-import": "error",
2596
2727
  "pyreon/no-cross-layer-import": "error",
2597
2728
  "pyreon/dev-guard-warnings": "error",
2598
- "pyreon/no-error-without-prefix": "error"
2729
+ "pyreon/no-error-without-prefix": "error",
2730
+ "pyreon/no-process-dev-gate": "error"
2599
2731
  } };
2600
2732
  }
2601
2733
  const presetBuilders = {