@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.
@@ -1 +1 @@
1
- {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/utils/source.ts","../../../src/cache.ts","../../../src/config/ignore.ts","../../../src/config/loader.ts","../../../src/config/presets.ts","../../../src/lint.ts","../../../src/reporter.ts","../../../src/lsp/index.ts","../../../src/rules/index.ts","../../../src/runner.ts","../../../src/utils/imports.ts","../../../src/watcher.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/utils/source.ts","../../../src/cache.ts","../../../src/config/ignore.ts","../../../src/config/loader.ts","../../../src/config/presets.ts","../../../src/lint.ts","../../../src/reporter.ts","../../../src/lsp/index.ts","../../../src/rules/index.ts","../../../src/runner.ts","../../../src/utils/imports.ts","../../../src/watcher.ts"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/lint",
3
- "version": "0.12.9",
3
+ "version": "0.12.11",
4
4
  "description": "Pyreon-specific linter — 56 rules for signals, JSX, SSR, performance, router, and architecture",
5
5
  "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/lint#readme",
6
6
  "bugs": {
@@ -30,6 +30,9 @@ function buildApp(): LintConfig {
30
30
  'pyreon/no-error-without-prefix': 'off',
31
31
  'pyreon/no-circular-import': 'off',
32
32
  'pyreon/no-cross-layer-import': 'off',
33
+ // `no-process-dev-gate` stays ON in `app` preset because the bug
34
+ // hits user-facing browser code regardless of whether it's a lib
35
+ // or an app.
33
36
  },
34
37
  }
35
38
  }
@@ -44,6 +47,7 @@ function buildLib(): LintConfig {
44
47
  'pyreon/no-cross-layer-import': 'error',
45
48
  'pyreon/dev-guard-warnings': 'error',
46
49
  'pyreon/no-error-without-prefix': 'error',
50
+ 'pyreon/no-process-dev-gate': 'error',
47
51
  },
48
52
  }
49
53
  }
@@ -0,0 +1,183 @@
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan } from '../../utils/ast'
3
+
4
+ /**
5
+ * `pyreon/no-process-dev-gate` — flag the broken `typeof process` dev-mode gate
6
+ * pattern that is dead code in real Vite browser bundles.
7
+ *
8
+ * The pattern this rule catches:
9
+ *
10
+ * ```ts
11
+ * const __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'
12
+ * ```
13
+ *
14
+ * This works in vitest (Node, `process` is defined) but is **silently dead
15
+ * code in real Vite browser bundles** because Vite does not polyfill
16
+ * `process` for the client. Every dev warning gated on this constant never
17
+ * fires for real users in dev mode.
18
+ *
19
+ * The fix is to use `import.meta.env.DEV`, which Vite/Rolldown literal-replace
20
+ * at build time:
21
+ *
22
+ * ```ts
23
+ * // No const needed — read directly at the use site so the bundler can fold:
24
+ * if (!import.meta.env?.DEV) return
25
+ * ```
26
+ *
27
+ * Vitest sets `import.meta.env.DEV === true` automatically (because it is
28
+ * Vite-based), so existing tests continue to pass.
29
+ *
30
+ * Reference implementation: `packages/fundamentals/flow/src/layout.ts:warnIgnoredOptions`.
31
+ *
32
+ * **Auto-fix**: replaces the assignment with `import.meta.env?.DEV === true`.
33
+ * Does NOT delete the const declaration — that has to happen by hand because
34
+ * the variable name and downstream usages may need updating in callers.
35
+ *
36
+ * **Server-package exception**: server-only files run in Node where `process`
37
+ * is always defined, so the pattern is correct there. The rule skips files
38
+ * matching `packages/zero/`, `packages/core/server/`, `packages/core/runtime-server/`,
39
+ * `packages/tools/vite-plugin/`, and any file containing `/server/` in its
40
+ * path. Add new server packages to `SERVER_PACKAGE_PATTERNS` below.
41
+ */
42
+ /**
43
+ * File-path patterns for server-only packages. Substring match against the
44
+ * file path passed to the rule. Patterns intentionally do NOT start with `/`
45
+ * so they match both absolute paths (`/Users/.../packages/zero/...`) and
46
+ * relative paths (`packages/zero/...`) — different lint runners pass paths
47
+ * differently.
48
+ */
49
+ const SERVER_PACKAGE_PATTERNS = [
50
+ 'packages/zero/',
51
+ 'packages/core/server/',
52
+ 'packages/core/runtime-server/',
53
+ 'packages/tools/vite-plugin/',
54
+ 'packages/tools/cli/',
55
+ 'packages/tools/lint/',
56
+ 'packages/tools/mcp/',
57
+ 'packages/tools/storybook/',
58
+ 'packages/tools/typescript/',
59
+ 'scripts/',
60
+ ]
61
+
62
+ function isServerOnlyFile(filePath: string): boolean {
63
+ return SERVER_PACKAGE_PATTERNS.some((pat) => filePath.includes(pat))
64
+ }
65
+
66
+ export const noProcessDevGate: Rule = {
67
+ meta: {
68
+ id: 'pyreon/no-process-dev-gate',
69
+ category: 'architecture',
70
+ description:
71
+ '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.',
72
+ severity: 'error',
73
+ fixable: true,
74
+ },
75
+ create(context) {
76
+ const filePath = context.getFilePath()
77
+
78
+ // Skip test files — vitest has `process`, the gate works there, and
79
+ // tests are not shipped to users.
80
+ if (
81
+ filePath.includes('/tests/') ||
82
+ filePath.includes('/test/') ||
83
+ filePath.includes('/__tests__/') ||
84
+ filePath.includes('.test.') ||
85
+ filePath.includes('.spec.')
86
+ ) {
87
+ return {}
88
+ }
89
+
90
+ // Skip server-only packages — they run in Node where `process` is
91
+ // always defined, so the pattern is correct there.
92
+ if (isServerOnlyFile(filePath)) {
93
+ return {}
94
+ }
95
+
96
+ /**
97
+ * Match the broken pattern at the AST level. We're looking for any
98
+ * `LogicalExpression` whose two sides are:
99
+ *
100
+ * 1. `typeof process !== 'undefined'` (a UnaryExpression on the LHS
101
+ * of a BinaryExpression with operator `!==`)
102
+ * 2. `process.env.NODE_ENV !== 'production'` (a MemberExpression on
103
+ * the LHS of a BinaryExpression with operator `!==`)
104
+ *
105
+ * The order can be either way (process check first or NODE_ENV check
106
+ * first), and the operator can be `&&` or `||` (we only flag `&&`
107
+ * because `||` doesn't make sense as a dev gate).
108
+ */
109
+ function isTypeofProcessCheck(node: any): boolean {
110
+ // typeof process !== 'undefined'
111
+ if (node?.type !== 'BinaryExpression') return false
112
+ if (node.operator !== '!==' && node.operator !== '!=') return false
113
+ const left = node.left
114
+ const right = node.right
115
+ if (left?.type !== 'UnaryExpression' || left.operator !== 'typeof') return false
116
+ if (left.argument?.type !== 'Identifier' || left.argument.name !== 'process') return false
117
+ if (
118
+ (right?.type === 'Literal' || right?.type === 'StringLiteral') &&
119
+ right.value === 'undefined'
120
+ ) {
121
+ return true
122
+ }
123
+ return false
124
+ }
125
+
126
+ function isNodeEnvCheck(node: any): boolean {
127
+ // process.env.NODE_ENV !== 'production'
128
+ if (node?.type !== 'BinaryExpression') return false
129
+ if (node.operator !== '!==' && node.operator !== '!=') return false
130
+ const left = node.left
131
+ const right = node.right
132
+ if (left?.type !== 'MemberExpression') return false
133
+ if (left.object?.type !== 'MemberExpression') return false
134
+ if (left.object.object?.type !== 'Identifier' || left.object.object.name !== 'process') {
135
+ return false
136
+ }
137
+ if (left.object.property?.type !== 'Identifier' || left.object.property.name !== 'env') {
138
+ return false
139
+ }
140
+ if (left.property?.type !== 'Identifier' || left.property.name !== 'NODE_ENV') return false
141
+ if (
142
+ (right?.type === 'Literal' || right?.type === 'StringLiteral') &&
143
+ right.value === 'production'
144
+ ) {
145
+ return true
146
+ }
147
+ return false
148
+ }
149
+
150
+ function isBrokenDevGate(node: any): boolean {
151
+ if (node?.type !== 'LogicalExpression') return false
152
+ if (node.operator !== '&&') return false
153
+ // Order can be (typeof process) && (NODE_ENV) OR vice versa
154
+ return (
155
+ (isTypeofProcessCheck(node.left) && isNodeEnvCheck(node.right)) ||
156
+ (isNodeEnvCheck(node.left) && isTypeofProcessCheck(node.right))
157
+ )
158
+ }
159
+
160
+ const callbacks: VisitorCallbacks = {
161
+ LogicalExpression(node: any) {
162
+ if (!isBrokenDevGate(node)) return
163
+
164
+ const span = getSpan(node)
165
+ // Auto-fix: replace the entire `typeof process ... && process.env.NODE_ENV ...`
166
+ // expression with `import.meta.env?.DEV === true`. We use optional
167
+ // chaining + strict equality so the expression is `false` (not
168
+ // `undefined`) when `import.meta.env` is missing — preserving the
169
+ // boolean shape callers expect.
170
+ const replacement = 'import.meta.env?.DEV === true'
171
+
172
+ context.report({
173
+ message:
174
+ '`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`.',
175
+ span,
176
+ fix: { span, replacement },
177
+ })
178
+ },
179
+ }
180
+
181
+ return callbacks
182
+ },
183
+ }
@@ -9,6 +9,7 @@ import { noCircularImport } from './architecture/no-circular-import'
9
9
  import { noCrossLayerImport } from './architecture/no-cross-layer-import'
10
10
  import { noDeepImport } from './architecture/no-deep-import'
11
11
  import { noErrorWithoutPrefix } from './architecture/no-error-without-prefix'
12
+ import { noProcessDevGate } from './architecture/no-process-dev-gate'
12
13
  import { noSubmitWithoutValidation } from './form/no-submit-without-validation'
13
14
  // Form
14
15
  import { noUnregisteredField } from './form/no-unregistered-field'
@@ -107,12 +108,13 @@ export const allRules: Rule[] = [
107
108
  noWindowInSsr,
108
109
  noMismatchRisk,
109
110
  preferRequestContext,
110
- // Architecture (5)
111
+ // Architecture (6)
111
112
  noCircularImport,
112
113
  noDeepImport,
113
114
  noCrossLayerImport,
114
115
  devGuardWarnings,
115
116
  noErrorWithoutPrefix,
117
+ noProcessDevGate,
116
118
  // Store (3)
117
119
  noStoreOutsideProvider,
118
120
  noMutateStoreState,
@@ -183,6 +185,7 @@ export {
183
185
  noNestedEffect,
184
186
  noOnChange,
185
187
  noPeekInTracked,
188
+ noProcessDevGate,
186
189
  noPropsDestructure,
187
190
  // Hooks
188
191
  noRawAddEventListener,
@@ -36,8 +36,8 @@ function lintWith(ruleId: string, source: string, filePath?: string) {
36
36
  // ── Rule Metadata ───────────────────────────────────────────────────────────
37
37
 
38
38
  describe('Rule metadata', () => {
39
- it('should have 56 rules', () => {
40
- expect(allRules.length).toBe(57)
39
+ it('should have 58 rules', () => {
40
+ expect(allRules.length).toBe(58)
41
41
  })
42
42
 
43
43
  it('should have unique rule IDs', () => {
@@ -82,7 +82,7 @@ describe('Rule metadata', () => {
82
82
  expect(counts.lifecycle).toBe(4)
83
83
  expect(counts.performance).toBe(4)
84
84
  expect(counts.ssr).toBe(3)
85
- expect(counts.architecture).toBe(5)
85
+ expect(counts.architecture).toBe(6)
86
86
  expect(counts.store).toBe(3)
87
87
  expect(counts.form).toBe(3)
88
88
  expect(counts.styling).toBe(4)
@@ -616,6 +616,266 @@ describe('Architecture rules', () => {
616
616
  expect(diags.length).toBe(0)
617
617
  })
618
618
 
619
+ // ── pyreon/no-process-dev-gate ────────────────────────────────────────────
620
+ // The recurring browser-dead-code bug we fixed in PR #200. Tests cover:
621
+ // - the canonical broken pattern (typeof process first, NODE_ENV second)
622
+ // - the reversed pattern (NODE_ENV first, typeof process second)
623
+ // - assignment context (const __DEV__ = ...) and inline use
624
+ // - the auto-fix output is the import.meta.env.DEV form
625
+ // - server packages are exempt (the pattern is correct in Node)
626
+ // - test files are exempt
627
+ // - the correct pattern (import.meta.env.DEV) does NOT trigger the rule
628
+
629
+ it('pyreon/no-process-dev-gate: flags the canonical broken __DEV__ assignment', () => {
630
+ const source = `const __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'`
631
+ const result = lintFile(
632
+ 'packages/core/runtime-dom/src/transition.ts',
633
+ source,
634
+ allRules,
635
+ defaultConfig(),
636
+ )
637
+ const diags = findByRule(result, 'pyreon/no-process-dev-gate')
638
+ expect(diags.length).toBe(1)
639
+ expect(diags[0]?.fix).toBeDefined()
640
+ expect(diags[0]?.fix?.replacement).toBe('import.meta.env?.DEV === true')
641
+ })
642
+
643
+ it('pyreon/no-process-dev-gate: flags the reversed pattern (NODE_ENV first)', () => {
644
+ const source = `const __DEV__ = process.env.NODE_ENV !== 'production' && typeof process !== 'undefined'`
645
+ const result = lintFile(
646
+ 'packages/core/runtime-dom/src/transition.ts',
647
+ source,
648
+ allRules,
649
+ defaultConfig(),
650
+ )
651
+ const diags = findByRule(result, 'pyreon/no-process-dev-gate')
652
+ expect(diags.length).toBe(1)
653
+ })
654
+
655
+ it('pyreon/no-process-dev-gate: flags inline use, not just assignment', () => {
656
+ // The pattern should be caught wherever it appears, not just in
657
+ // const declarations.
658
+ const source = `if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') { console.warn('hi') }`
659
+ const result = lintFile(
660
+ 'packages/core/runtime-dom/src/transition.ts',
661
+ source,
662
+ allRules,
663
+ defaultConfig(),
664
+ )
665
+ const diags = findByRule(result, 'pyreon/no-process-dev-gate')
666
+ expect(diags.length).toBe(1)
667
+ })
668
+
669
+ it('pyreon/no-process-dev-gate: clean for the correct import.meta.env.DEV pattern', () => {
670
+ const source = `if (!import.meta.env?.DEV) return`
671
+ const result = lintFile(
672
+ 'packages/core/runtime-dom/src/transition.ts',
673
+ source,
674
+ allRules,
675
+ defaultConfig(),
676
+ )
677
+ const diags = findByRule(result, 'pyreon/no-process-dev-gate')
678
+ expect(diags.length).toBe(0)
679
+ })
680
+
681
+ it('pyreon/no-process-dev-gate: exempts server-only packages (Node always has process)', () => {
682
+ const source = `const __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'`
683
+ // packages/core/server/ is the SSR adapter — runs in Node, the pattern is correct there.
684
+ const result = lintFile(
685
+ 'packages/core/server/src/handler.ts',
686
+ source,
687
+ allRules,
688
+ defaultConfig(),
689
+ )
690
+ const diags = findByRule(result, 'pyreon/no-process-dev-gate')
691
+ expect(diags.length).toBe(0)
692
+ })
693
+
694
+ it('pyreon/no-process-dev-gate: exempts runtime-server, zero, vite-plugin', () => {
695
+ const source = `const __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'`
696
+ for (const path of [
697
+ 'packages/core/runtime-server/src/index.ts',
698
+ 'packages/zero/zero/src/logger.ts',
699
+ 'packages/tools/vite-plugin/src/index.ts',
700
+ ]) {
701
+ const result = lintFile(path, source, allRules, defaultConfig())
702
+ const diags = findByRule(result, 'pyreon/no-process-dev-gate')
703
+ expect(diags.length, `expected ${path} to be exempt`).toBe(0)
704
+ }
705
+ })
706
+
707
+ it('pyreon/no-process-dev-gate: exempts test files', () => {
708
+ const source = `const __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'`
709
+ const result = lintFile(
710
+ 'packages/core/runtime-dom/src/tests/transition.test.ts',
711
+ source,
712
+ allRules,
713
+ defaultConfig(),
714
+ )
715
+ const diags = findByRule(result, 'pyreon/no-process-dev-gate')
716
+ expect(diags.length).toBe(0)
717
+ })
718
+
719
+ it('pyreon/no-process-dev-gate: does NOT flag isolated typeof process check', () => {
720
+ // A bare `typeof process !== 'undefined'` (e.g., for SSR detection) is
721
+ // fine — it's the COMBINATION with NODE_ENV check that flags as a dev
722
+ // gate. This protects against false positives on legitimate isomorphic code.
723
+ const source = `if (typeof process !== 'undefined') { console.log('node') }`
724
+ const result = lintFile(
725
+ 'packages/core/runtime-dom/src/transition.ts',
726
+ source,
727
+ allRules,
728
+ defaultConfig(),
729
+ )
730
+ const diags = findByRule(result, 'pyreon/no-process-dev-gate')
731
+ expect(diags.length).toBe(0)
732
+ })
733
+
734
+ it('pyreon/no-process-dev-gate: does NOT flag isolated NODE_ENV check', () => {
735
+ // Bare NODE_ENV check (without the typeof process guard) is also fine
736
+ // — it's not the dead-in-browser pattern.
737
+ const source = `if (process.env.NODE_ENV !== 'production') { /* ... */ }`
738
+ const result = lintFile(
739
+ 'packages/core/runtime-dom/src/transition.ts',
740
+ source,
741
+ allRules,
742
+ defaultConfig(),
743
+ )
744
+ const diags = findByRule(result, 'pyreon/no-process-dev-gate')
745
+ expect(diags.length).toBe(0)
746
+ })
747
+
748
+ it('pyreon/no-process-dev-gate: codebase-wide check — all browser packages are clean', async () => {
749
+ // Meta-test: scan every browser-package source file and assert NONE
750
+ // of them contain the broken pattern. If a future PR introduces a
751
+ // new file with `typeof process !== 'undefined' && process.env...`
752
+ // anywhere in the browser packages, this test fails immediately
753
+ // (in addition to the per-file lint rule firing on `bun run lint`).
754
+ //
755
+ // The lint rule by itself is the primary defence — this meta-test
756
+ // is the safety net for cases where someone might disable the rule
757
+ // for a single file or commit. The two layers together make the
758
+ // bug class impossible to reintroduce silently.
759
+ const fs = await import('node:fs')
760
+ const path = await import('node:path')
761
+
762
+ // Resolve the workspace root by walking up from this test file.
763
+ // This test file lives at `packages/tools/lint/src/tests/runner.test.ts`,
764
+ // so 5 levels up gets us to the workspace root.
765
+ const workspaceRoot = path.resolve(import.meta.dirname, '../../../../..')
766
+
767
+ // The list of source roots to scan. Mirrors the lint rule's
768
+ // SERVER_PACKAGE_PATTERNS exemption — these are the BROWSER-running
769
+ // packages that must be clean.
770
+ const browserPackageRoots = [
771
+ 'packages/core/core/src',
772
+ 'packages/core/runtime-dom/src',
773
+ 'packages/core/router/src',
774
+ 'packages/core/head/src',
775
+ 'packages/fundamentals/flow/src',
776
+ 'packages/fundamentals/code/src',
777
+ 'packages/fundamentals/charts/src',
778
+ 'packages/fundamentals/document/src',
779
+ 'packages/fundamentals/form/src',
780
+ 'packages/fundamentals/hooks/src',
781
+ 'packages/fundamentals/store/src',
782
+ 'packages/fundamentals/state-tree/src',
783
+ 'packages/ui-system/styler/src',
784
+ 'packages/ui-system/unistyle/src',
785
+ 'packages/ui-system/elements/src',
786
+ 'packages/ui-system/rocketstyle/src',
787
+ 'packages/ui-system/coolgrid/src',
788
+ 'packages/ui-system/kinetic/src',
789
+ 'packages/ui-system/document-primitives/src',
790
+ 'packages/ui-system/connector-document/src',
791
+ 'packages/ui/components/src',
792
+ 'packages/ui/primitives/src',
793
+ ]
794
+
795
+ // Recursively walk a directory and collect all .ts/.tsx files
796
+ // (excluding tests).
797
+ function walk(dir: string, out: string[] = []): string[] {
798
+ let entries: import('node:fs').Dirent[]
799
+ try {
800
+ entries = fs.readdirSync(dir, { withFileTypes: true })
801
+ } catch {
802
+ // Directory may not exist (e.g., new package not yet created).
803
+ // The test should pass — this is a clean state.
804
+ return out
805
+ }
806
+ for (const entry of entries) {
807
+ const full = path.join(dir, entry.name)
808
+ if (entry.isDirectory()) {
809
+ // Skip test directories — the lint rule exempts them and
810
+ // they may legitimately use `process` for test env detection.
811
+ if (
812
+ entry.name === 'tests' ||
813
+ entry.name === '__tests__' ||
814
+ entry.name === 'test' ||
815
+ entry.name === 'node_modules' ||
816
+ entry.name === 'dist' ||
817
+ entry.name === 'lib'
818
+ ) {
819
+ continue
820
+ }
821
+ walk(full, out)
822
+ } else if (
823
+ (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) &&
824
+ !entry.name.endsWith('.d.ts') &&
825
+ !entry.name.includes('.test.') &&
826
+ !entry.name.includes('.spec.')
827
+ ) {
828
+ out.push(full)
829
+ }
830
+ }
831
+ return out
832
+ }
833
+
834
+ const offenders: string[] = []
835
+ // The exact pattern we forbid. We use a strict regex that requires
836
+ // the FULL combined pattern including the `&&` and the NODE_ENV
837
+ // check, which only appears in actual buggy code (not isolated
838
+ // typeof process or NODE_ENV checks).
839
+ const brokenPattern =
840
+ /typeof\s+process\s*!==\s*['"]undefined['"]\s*&&\s*process\.env\.NODE_ENV\s*!==\s*['"]production['"]/
841
+
842
+ // Strip comments before matching — explanation comments in fixed
843
+ // files (e.g., `flow/src/layout.ts:warnIgnoredOptions`) legitimately
844
+ // mention the bad pattern as documentation. We only care about
845
+ // executable code.
846
+ function stripComments(source: string): string {
847
+ return source
848
+ .replace(/\/\*[\s\S]*?\*\//g, '') // block comments
849
+ .replace(/^\s*\/\/.*$/gm, '') // line comments at start of line
850
+ .replace(/([^:])\/\/.*$/gm, '$1') // trailing line comments (avoid breaking URLs)
851
+ }
852
+
853
+ for (const root of browserPackageRoots) {
854
+ const absRoot = path.join(workspaceRoot, root)
855
+ const files = walk(absRoot)
856
+ for (const file of files) {
857
+ const source = fs.readFileSync(file, 'utf-8')
858
+ const codeOnly = stripComments(source)
859
+ if (brokenPattern.test(codeOnly)) {
860
+ offenders.push(path.relative(workspaceRoot, file))
861
+ }
862
+ }
863
+ }
864
+
865
+ // If this fails, the offending files are listed. Each one needs
866
+ // the broken pattern replaced with `import.meta.env?.DEV === true`
867
+ // (or the inline `if (!import.meta.env?.DEV) return` form). See
868
+ // `packages/fundamentals/flow/src/layout.ts:warnIgnoredOptions`
869
+ // for the reference implementation.
870
+ expect(
871
+ offenders,
872
+ `Browser-package source files with the broken \`typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'\` dev gate. ` +
873
+ `This pattern is dead code in real Vite browser bundles because Vite does not polyfill \`process\`. ` +
874
+ `Replace with \`const __DEV__ = import.meta.env?.DEV === true\`. ` +
875
+ `See pyreon/no-process-dev-gate lint rule for details.`,
876
+ ).toEqual([])
877
+ })
878
+
619
879
  it('pyreon/no-error-without-prefix: flags throw without [Pyreon]', () => {
620
880
  const source = `throw new Error("something went wrong")`
621
881
  const result = lintSource(source)
@@ -1013,7 +1273,7 @@ describe('Ignore filter', () => {
1013
1273
  describe('Presets', () => {
1014
1274
  it('recommended should include all rules', () => {
1015
1275
  const config = getPreset('recommended')
1016
- expect(Object.keys(config.rules).length).toBe(57)
1276
+ expect(Object.keys(config.rules).length).toBe(58)
1017
1277
  })
1018
1278
 
1019
1279
  it('strict should promote all warns to errors', () => {
@@ -1034,10 +1294,19 @@ describe('Presets', () => {
1034
1294
  expect(app.rules['pyreon/no-cross-layer-import']).toBe('off')
1035
1295
  })
1036
1296
 
1297
+ it('app preset KEEPS no-process-dev-gate enabled (browser bug, not a lib-only concern)', () => {
1298
+ // The browser-dead-code bug hits user-facing code regardless of
1299
+ // whether the project is a library or an app. Apps that build for
1300
+ // the browser still need the warning.
1301
+ const app = getPreset('app')
1302
+ expect(app.rules['pyreon/no-process-dev-gate']).toBe('error')
1303
+ })
1304
+
1037
1305
  it('lib should have architecture rules as error', () => {
1038
1306
  const lib = getPreset('lib')
1039
1307
  expect(lib.rules['pyreon/no-circular-import']).toBe('error')
1040
1308
  expect(lib.rules['pyreon/no-cross-layer-import']).toBe('error')
1041
1309
  expect(lib.rules['pyreon/dev-guard-warnings']).toBe('error')
1310
+ expect(lib.rules['pyreon/no-process-dev-gate']).toBe('error')
1042
1311
  })
1043
1312
  })