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