@pyreon/lint 0.12.10 → 0.12.12
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/analysis/cli.js.html +1 -1
- package/lib/analysis/index.js.html +1 -1
- package/lib/cli.js +133 -1
- package/lib/cli.js.map +1 -1
- package/lib/index.js +133 -1
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/config/presets.ts +4 -0
- package/src/rules/architecture/no-process-dev-gate.ts +183 -0
- package/src/rules/index.ts +4 -1
- package/src/tests/runner.test.ts +273 -4
package/lib/types/index.d.ts.map
CHANGED
|
@@ -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":";KAEY,QAAA;AAAA,UAEK,cAAA;EACf,IAAA;EACA,MAAA;AAAA;AAAA,UAGe,IAAA;EACf,KAAA;EACA,GAAA;AAAA;AAAA,UAGe,GAAA;EACf,IAAA,EAAM,IAAA;EACN,WAAA;AAAA;AAAA,UAGe,UAAA;EACf,MAAA;EACA,QAAA,EAAU,QAAA;EACV,OAAA;EACA,IAAA,EAAM,IAAA;EACN,GAAA,EAAK,cAAA;EACL,GAAA,GAAM,GAAA;AAAA;AAAA,KAKI,YAAA;AAAA,UAcK,QAAA;EACf,EAAA;EACA,QAAA,EAAU,YAAA;EACV,WAAA;EACA,QAAA,EAAU,QAAA;EACV,OAAA;AAAA;AAAA,UAKe,WAAA;EACf,MAAA,CAAO,UAAA,EAAY,IAAA,CAAK,UAAA;EACxB,aAAA;EACA,WAAA;AAAA;AAAA,KAGU,eAAA,IAAmB,IAAA,OAAW,MAAA;AAAA,UAEzB,gBAAA;EAAA,CACd,QAAA,WAAmB,eAAA;AAAA;AAAA,UAKL,IAAA;EACf,IAAA,EAAM,QAAA;EACN,MAAA,CAAO,OAAA,EAAS,WAAA,GAAc,gBAAA;AAAA;AAAA,UAKf,UAAA;EACf,KAAA,EAAO,MAAA,SAAe,QAAA;EACtB,OAAA;EACA,OAAA;AAAA;AAAA,UAGe,cAAA;EACf,MAAA,GAAS,UAAA;EACT,KAAA,GAAQ,MAAA,SAAe,QAAA;EACvB,OAAA;EACA,OAAA;AAAA;AAAA,KAGU,UAAA;AAAA,UAIK,cAAA;EACf,QAAA;EACA,WAAA,EAAa,UAAA;EACb,WAAA;AAAA;AAAA,UAGe,UAAA;EACf,KAAA,EAAO,cAAA;EACP,WAAA;EACA,aAAA;EACA,UAAA;AAAA;AAAA,UAKe,WAAA;EACf,KAAA;EACA,MAAA,GAAS,UAAA;EACT,GAAA;EACA,KAAA;EACA,aAAA,GAAgB,MAAA,SAAe,QAAA;EAC/B,MAAA;EACA,MAAA;AAAA;AAAA,UAKe,UAAA;EACf,MAAA;EACA,UAAA,EAAY,KAAA;IAAQ,QAAA;IAAkB,KAAA;EAAA;EACtC,SAAA;EACA,WAAA;AAAA;;;AAzHF;;;AAAA,cCGa,SAAA;EAAA,QACH,UAAA;cAEI,UAAA;EDJiB;ECc7B,MAAA,CAAO,MAAA,WAAiB,cAAA;AAAA;;;ADhB1B;;;;;AAEA;;;;;AAKA;;;;;AAKA;;;AAZA,cEkBa,QAAA;EAAA,QACH,KAAA;EAER,GAAA,CAAI,UAAA;IAAuB,OAAA;IAAc,SAAA,EAAW,SAAA;EAAA;EAKpD,GAAA,CAAI,UAAA,UAAoB,KAAA;IAAS,OAAA;IAAc,SAAA,EAAW,SAAA;EAAA;EAK1D,KAAA,CAAA;EAAA,IAII,IAAA,CAAA;AAAA;;;;AFnCN;;;;;AAEA;;;;;AAKA;;iBGOgB,kBAAA,CACd,GAAA,UACA,WAAA,yBACE,QAAA;;;AHjBJ;;;;;AAEA;;;;;AAKA;;;;;AAKA;;AAZA,iBIqBgB,UAAA,CAAW,GAAA,WAAc,cAAA;;;;iBAmCzB,kBAAA,CAAmB,QAAA,WAAmB,cAAA;;;
|
|
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":";KAEY,QAAA;AAAA,UAEK,cAAA;EACf,IAAA;EACA,MAAA;AAAA;AAAA,UAGe,IAAA;EACf,KAAA;EACA,GAAA;AAAA;AAAA,UAGe,GAAA;EACf,IAAA,EAAM,IAAA;EACN,WAAA;AAAA;AAAA,UAGe,UAAA;EACf,MAAA;EACA,QAAA,EAAU,QAAA;EACV,OAAA;EACA,IAAA,EAAM,IAAA;EACN,GAAA,EAAK,cAAA;EACL,GAAA,GAAM,GAAA;AAAA;AAAA,KAKI,YAAA;AAAA,UAcK,QAAA;EACf,EAAA;EACA,QAAA,EAAU,YAAA;EACV,WAAA;EACA,QAAA,EAAU,QAAA;EACV,OAAA;AAAA;AAAA,UAKe,WAAA;EACf,MAAA,CAAO,UAAA,EAAY,IAAA,CAAK,UAAA;EACxB,aAAA;EACA,WAAA;AAAA;AAAA,KAGU,eAAA,IAAmB,IAAA,OAAW,MAAA;AAAA,UAEzB,gBAAA;EAAA,CACd,QAAA,WAAmB,eAAA;AAAA;AAAA,UAKL,IAAA;EACf,IAAA,EAAM,QAAA;EACN,MAAA,CAAO,OAAA,EAAS,WAAA,GAAc,gBAAA;AAAA;AAAA,UAKf,UAAA;EACf,KAAA,EAAO,MAAA,SAAe,QAAA;EACtB,OAAA;EACA,OAAA;AAAA;AAAA,UAGe,cAAA;EACf,MAAA,GAAS,UAAA;EACT,KAAA,GAAQ,MAAA,SAAe,QAAA;EACvB,OAAA;EACA,OAAA;AAAA;AAAA,KAGU,UAAA;AAAA,UAIK,cAAA;EACf,QAAA;EACA,WAAA,EAAa,UAAA;EACb,WAAA;AAAA;AAAA,UAGe,UAAA;EACf,KAAA,EAAO,cAAA;EACP,WAAA;EACA,aAAA;EACA,UAAA;AAAA;AAAA,UAKe,WAAA;EACf,KAAA;EACA,MAAA,GAAS,UAAA;EACT,GAAA;EACA,KAAA;EACA,aAAA,GAAgB,MAAA,SAAe,QAAA;EAC/B,MAAA;EACA,MAAA;AAAA;AAAA,UAKe,UAAA;EACf,MAAA;EACA,UAAA,EAAY,KAAA;IAAQ,QAAA;IAAkB,KAAA;EAAA;EACtC,SAAA;EACA,WAAA;AAAA;;;AAzHF;;;AAAA,cCGa,SAAA;EAAA,QACH,UAAA;cAEI,UAAA;EDJiB;ECc7B,MAAA,CAAO,MAAA,WAAiB,cAAA;AAAA;;;ADhB1B;;;;;AAEA;;;;;AAKA;;;;;AAKA;;;AAZA,cEkBa,QAAA;EAAA,QACH,KAAA;EAER,GAAA,CAAI,UAAA;IAAuB,OAAA;IAAc,SAAA,EAAW,SAAA;EAAA;EAKpD,GAAA,CAAI,UAAA,UAAoB,KAAA;IAAS,OAAA;IAAc,SAAA,EAAW,SAAA;EAAA;EAK1D,KAAA,CAAA;EAAA,IAII,IAAA,CAAA;AAAA;;;;AFnCN;;;;;AAEA;;;;;AAKA;;iBGOgB,kBAAA,CACd,GAAA,UACA,WAAA,yBACE,QAAA;;;AHjBJ;;;;;AAEA;;;;;AAKA;;;;;AAKA;;AAZA,iBIqBgB,UAAA,CAAW,GAAA,WAAc,cAAA;;;;iBAmCzB,kBAAA,CAAmB,QAAA,WAAmB,cAAA;;;iBCGtC,SAAA,CAAU,IAAA,EAAM,UAAA,GAAa,UAAA;;;AL3D7C;;;;;AAEA;;;;;AAKA;AAPA,iBM0KgB,IAAA,CAAK,OAAA,EAAS,WAAA,GAAc,UAAA;;;;AN9J5C;;;;;;;;;iBM2MgB,SAAA,CAAA,GAAa,QAAA;;;ANvN7B;;;AAAA,iBOyBgB,UAAA,CAAW,MAAA,EAAQ,UAAA;;APvBnC;;iBO6DgB,UAAA,CAAW,MAAA,EAAQ,UAAA;;;APxDnC;iBO+DgB,aAAA,CAAc,MAAA,EAAQ,UAAA;;;;APtEtC;;;;;AAEA;;;;;AAKA;;;;;AAKA;;;;;;iBQ6KgB,cAAA,CAAA;;;cCnHH,QAAA,EAAU,IAAA;;;;;;;ATpEvB;;;;;iBUwFgB,QAAA,CACd,QAAA,UACA,UAAA,UACA,KAAA,EAAO,IAAA,IACP,MAAA,EAAQ,UAAA,EACR,KAAA,GAAQ,QAAA,eACP,cAAA;;;;;iBA+Da,UAAA,CAAW,UAAA,UAAoB,WAAA,EAAa,UAAA;;;iBC/F5C,cAAA,CAAe,MAAA;AAAA,iBAIf,eAAA,CAAgB,MAAA;AAAA,iBAIhB,iBAAA,CAAkB,IAAA,QAAY,UAAA;AAAA,iBA2B9B,WAAA,CAAY,OAAA,EAAS,UAAA,IAAc,IAAA,UAAc,WAAA;AAAA,iBAQjD,YAAA,CACd,OAAA,EAAS,UAAA,IACT,IAAA,UACA,WAAA;;;AX9GF;;;;;AAEA;;;;;AAKA;;;AAPA,iBY4BgB,YAAA,CAAa,OAAA,EAAS,WAAA;EAAgB,MAAA;AAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/lint",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.12",
|
|
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": {
|
package/src/config/presets.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/rules/index.ts
CHANGED
|
@@ -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 (
|
|
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,
|
package/src/tests/runner.test.ts
CHANGED
|
@@ -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
|
|
40
|
-
expect(allRules.length).toBe(
|
|
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(
|
|
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(
|
|
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
|
})
|