@pyreon/lint 0.14.0 → 0.16.0
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/README.md +9 -7
- package/bin/pyreon-lint.js +2 -0
- package/lib/analysis/cli.js.html +1 -1
- package/lib/analysis/index.js.html +1 -1
- package/lib/cli.js +791 -69
- package/lib/index.js +791 -69
- package/lib/types/index.d.ts +1 -1
- package/package.json +5 -3
- package/src/manifest.ts +6 -6
- package/src/rules/architecture/dev-guard-warnings.ts +56 -6
- package/src/rules/architecture/no-process-dev-gate.ts +141 -62
- package/src/rules/index.ts +25 -2
- package/src/rules/jsx/no-props-destructure.ts +57 -7
- package/src/rules/lifecycle/no-imperative-effect-on-create.ts +278 -0
- package/src/rules/reactivity/no-async-effect.ts +84 -0
- package/src/rules/reactivity/no-signal-call-write.ts +60 -0
- package/src/rules/reactivity/storage-signal-v-forwarding.ts +184 -0
- package/src/rules/ssg/index.ts +3 -0
- package/src/rules/ssg/invalid-loader-export.ts +84 -0
- package/src/rules/ssg/missing-get-static-paths.ts +103 -0
- package/src/rules/ssg/revalidate-not-pure-literal.ts +69 -0
- package/src/runner.ts +8 -8
- package/src/tests/runner.test.ts +547 -9
- package/src/tests/ssg-rules.test.ts +211 -0
- package/src/tests/storage-signal-v-forwarding.test.ts +224 -0
- package/src/types.ts +1 -0
- package/src/utils/validate-options.ts +1 -1
- package/lib/cli.js.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
package/lib/types/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ interface Diagnostic {
|
|
|
20
20
|
loc: SourceLocation;
|
|
21
21
|
fix?: Fix | undefined;
|
|
22
22
|
}
|
|
23
|
-
type RuleCategory = 'reactivity' | 'jsx' | 'lifecycle' | 'performance' | 'ssr' | 'architecture' | 'store' | 'form' | 'styling' | 'hooks' | 'accessibility' | 'router';
|
|
23
|
+
type RuleCategory = 'reactivity' | 'jsx' | 'lifecycle' | 'performance' | 'ssr' | 'architecture' | 'store' | 'form' | 'styling' | 'hooks' | 'accessibility' | 'router' | 'ssg';
|
|
24
24
|
/**
|
|
25
25
|
* Declared type of an option slot. Minimal on purpose — sufficient for
|
|
26
26
|
* the exemption patterns we actually use. Extend when a rule needs more.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/lint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
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": {
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
"directory": "packages/tools/lint"
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
|
16
|
-
"pyreon-lint": "./
|
|
16
|
+
"pyreon-lint": "./bin/pyreon-lint.js"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
|
+
"bin",
|
|
19
20
|
"lib",
|
|
21
|
+
"!lib/**/*.map",
|
|
20
22
|
"src",
|
|
21
23
|
"schema",
|
|
22
24
|
"README.md",
|
|
@@ -52,7 +54,7 @@
|
|
|
52
54
|
},
|
|
53
55
|
"dependencies": {
|
|
54
56
|
"@oxc-project/types": "^0.123.0",
|
|
55
|
-
"oxc-parser": "^0.
|
|
57
|
+
"oxc-parser": "^0.129.0"
|
|
56
58
|
},
|
|
57
59
|
"devDependencies": {
|
|
58
60
|
"@pyreon/manifest": "0.13.1"
|
package/src/manifest.ts
CHANGED
|
@@ -4,12 +4,12 @@ export default defineManifest({
|
|
|
4
4
|
name: '@pyreon/lint',
|
|
5
5
|
title: 'Pyreon-specific Linter',
|
|
6
6
|
tagline:
|
|
7
|
-
'Pyreon-specific linter —
|
|
7
|
+
'Pyreon-specific linter — 62 rules across 12 categories, config files, watch mode, AST cache, CLI + LSP',
|
|
8
8
|
description:
|
|
9
|
-
'Pyreon-specific lint rules powered by `oxc-parser`. Covers reactivity (
|
|
9
|
+
'Pyreon-specific lint rules powered by `oxc-parser`. Covers reactivity (12), JSX (11), lifecycle (5), performance (4), SSR (3), architecture (7), store (3), form (3), styling (4), hooks (3), accessibility (3), router (4) — 62 rules total. Programmatic API (`lint`, `lintFile`), CLI (`pyreon-lint`), watch mode (fs.watch + 100ms debounce + AstCache), LSP server, and `.pyreonlintrc.json` config with per-rule options via ESLint-style tuple form. Notable rules: `pyreon/no-process-dev-gate` (auto-fixable; replaces dead-in-browser `typeof process` gates with `import.meta.env?.DEV`), `pyreon/require-browser-smoke-test` (locks in T1.1 browser-test durability).',
|
|
10
10
|
category: 'server',
|
|
11
11
|
features: [
|
|
12
|
-
'
|
|
12
|
+
'62 rules across 12 categories',
|
|
13
13
|
'lint(options) programmatic API + lintFile() low-level entry',
|
|
14
14
|
'CLI: pyreon-lint with --preset / --fix / --watch / --format / --rule-options',
|
|
15
15
|
'4 presets: recommended, strict, app, lib',
|
|
@@ -43,7 +43,7 @@ const fileResult = lintFile('app.tsx', source, allRules, config, cache)
|
|
|
43
43
|
// pyreon-lint --preset strict --quiet # CI mode
|
|
44
44
|
// pyreon-lint --fix # auto-fix
|
|
45
45
|
// pyreon-lint --watch src/ # watch mode
|
|
46
|
-
// pyreon-lint --list # list all
|
|
46
|
+
// pyreon-lint --list # list all 62 rules
|
|
47
47
|
// pyreon-lint --rule-options 'pyreon/no-window-in-ssr={"exemptPaths":["src/foundation/"]}' src/`,
|
|
48
48
|
api: [
|
|
49
49
|
{
|
|
@@ -51,7 +51,7 @@ const fileResult = lintFile('app.tsx', source, allRules, config, cache)
|
|
|
51
51
|
kind: 'function',
|
|
52
52
|
signature: 'lint(options?: LintOptions): LintResult',
|
|
53
53
|
summary:
|
|
54
|
-
'
|
|
54
|
+
'62 rules across 12 categories. Auto-loads `.pyreonlintrc.json`. Presets: `recommended`, `strict`, `app`, `lib`. Per-rule options via tuple form in config (`["error", { exemptPaths: [...] }]`) or `ruleOptionsOverrides`. Wrong-typed options surface on `result.configDiagnostics`. Uses `oxc-parser` with AST caching.',
|
|
55
55
|
example: `import { lint } from "@pyreon/lint"
|
|
56
56
|
|
|
57
57
|
const result = lint({ paths: ["src/"], preset: "recommended" })
|
|
@@ -94,7 +94,7 @@ const result = lintFile("app.tsx", source, allRules, config, cache, configSink)`
|
|
|
94
94
|
example: `pyreon-lint --preset strict --quiet # CI mode
|
|
95
95
|
pyreon-lint --fix # auto-fix
|
|
96
96
|
pyreon-lint --watch src/ # watch mode
|
|
97
|
-
pyreon-lint --list # list all
|
|
97
|
+
pyreon-lint --list # list all 62 rules
|
|
98
98
|
pyreon-lint --format json # machine-readable
|
|
99
99
|
pyreon-lint --rule-options 'pyreon/no-window-in-ssr={"exemptPaths":["src/foundation/"]}' src/`,
|
|
100
100
|
seeAlso: ['lint'],
|
|
@@ -90,11 +90,55 @@ export const devGuardWarnings: Rule = {
|
|
|
90
90
|
return false
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// `process.env.NODE_ENV` access — also matches optional-chaining shapes
|
|
94
|
+
// (`process?.env?.NODE_ENV`). Returns true when the node is the
|
|
95
|
+
// `process.env.NODE_ENV` member access itself.
|
|
96
|
+
function isProcessEnvNodeEnv(node: any): boolean {
|
|
97
|
+
let n = node
|
|
98
|
+
if (n?.type === 'ChainExpression') n = n.expression
|
|
99
|
+
if (n?.type !== 'MemberExpression') return false
|
|
100
|
+
if (n.property?.type !== 'Identifier' || n.property.name !== 'NODE_ENV') return false
|
|
101
|
+
let envObj = n.object
|
|
102
|
+
if (envObj?.type === 'ChainExpression') envObj = envObj.expression
|
|
103
|
+
if (envObj?.type !== 'MemberExpression') return false
|
|
104
|
+
if (envObj.property?.type !== 'Identifier' || envObj.property.name !== 'env') return false
|
|
105
|
+
const procObj = envObj.object
|
|
106
|
+
return procObj?.type === 'Identifier' && procObj.name === 'process'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Match a development-mode check: `process.env.NODE_ENV !== 'production'`
|
|
110
|
+
// (and `!=` variant). The bundler-agnostic library convention for dev
|
|
111
|
+
// gates — every modern bundler auto-replaces `process.env.NODE_ENV`.
|
|
112
|
+
function isDevelopmentCheck(node: any): boolean {
|
|
113
|
+
if (node?.type !== 'BinaryExpression') return false
|
|
114
|
+
if (node.operator !== '!==' && node.operator !== '!=') return false
|
|
115
|
+
const matches = (a: any, b: any) =>
|
|
116
|
+
isProcessEnvNodeEnv(a) &&
|
|
117
|
+
(b?.type === 'Literal' || b?.type === 'StringLiteral') &&
|
|
118
|
+
b.value === 'production'
|
|
119
|
+
return matches(node.left, node.right) || matches(node.right, node.left)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Match a production-mode check: `process.env.NODE_ENV === 'production'`.
|
|
123
|
+
// Used as the early-return guard form: `if (process.env.NODE_ENV === 'production') return`.
|
|
124
|
+
function isProductionCheck(node: any): boolean {
|
|
125
|
+
if (node?.type !== 'BinaryExpression') return false
|
|
126
|
+
if (node.operator !== '===' && node.operator !== '==') return false
|
|
127
|
+
const matches = (a: any, b: any) =>
|
|
128
|
+
isProcessEnvNodeEnv(a) &&
|
|
129
|
+
(b?.type === 'Literal' || b?.type === 'StringLiteral') &&
|
|
130
|
+
b.value === 'production'
|
|
131
|
+
return matches(node.left, node.right) || matches(node.right, node.left)
|
|
132
|
+
}
|
|
133
|
+
|
|
93
134
|
// Match `<flag>`, `<flag> && X`, `X && <flag>`, `<flag> === true`, etc.
|
|
94
135
|
// `&&` only — `||` doesn't guarantee dev-only execution.
|
|
136
|
+
// Also accepts the bundler-agnostic `process.env.NODE_ENV !== 'production'`
|
|
137
|
+
// dev check directly.
|
|
95
138
|
function containsDevGuard(test: any): boolean {
|
|
96
139
|
if (!test) return false
|
|
97
140
|
if (isDevFlag(test)) return true
|
|
141
|
+
if (isDevelopmentCheck(test)) return true
|
|
98
142
|
if (test.type === 'LogicalExpression' && test.operator === '&&') {
|
|
99
143
|
return containsDevGuard(test.left) || containsDevGuard(test.right)
|
|
100
144
|
}
|
|
@@ -109,18 +153,24 @@ export const devGuardWarnings: Rule = {
|
|
|
109
153
|
}
|
|
110
154
|
|
|
111
155
|
// Detects an early-return DEV guard at the head of a function body:
|
|
112
|
-
// `if (!__DEV__) return`
|
|
156
|
+
// `if (!__DEV__) return` (legacy)
|
|
157
|
+
// `if (!import.meta.env.DEV) return` (Vite-tied — also legacy)
|
|
158
|
+
// `if (process.env.NODE_ENV === 'production') return` (current bundler-agnostic standard)
|
|
113
159
|
// Everything after this in the function is implicitly dev-only.
|
|
114
160
|
function isEarlyReturnDevGuard(node: any): boolean {
|
|
115
161
|
if (!node || node.type !== 'IfStatement') return false
|
|
116
162
|
const t = node.test
|
|
163
|
+
const c = node.consequent
|
|
164
|
+
const isReturn =
|
|
165
|
+
c?.type === 'ReturnStatement' ||
|
|
166
|
+
(c?.type === 'BlockStatement' && c.body.length === 1 && c.body[0]?.type === 'ReturnStatement')
|
|
167
|
+
if (!isReturn) return false
|
|
168
|
+
// Bundler-agnostic: `if (process.env.NODE_ENV === 'production') return`
|
|
169
|
+
if (isProductionCheck(t)) return true
|
|
170
|
+
// Legacy: `if (!isDev) return`
|
|
117
171
|
const arg = t?.type === 'UnaryExpression' && t.operator === '!' ? t.argument : null
|
|
118
172
|
if (!arg) return false
|
|
119
|
-
|
|
120
|
-
const c = node.consequent
|
|
121
|
-
if (c?.type === 'ReturnStatement') return true
|
|
122
|
-
if (c?.type === 'BlockStatement' && c.body.length === 1 && c.body[0]?.type === 'ReturnStatement') return true
|
|
123
|
-
return false
|
|
173
|
+
return isDevFlag(arg)
|
|
124
174
|
}
|
|
125
175
|
|
|
126
176
|
let devGuardDepth = 0
|
|
@@ -4,40 +4,48 @@ import { isPathExempt } from '../../utils/exempt-paths'
|
|
|
4
4
|
import { isTestFile } from '../../utils/file-roles'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* `pyreon/no-process-dev-gate` — flag
|
|
8
|
-
*
|
|
7
|
+
* `pyreon/no-process-dev-gate` — flag bundler-coupled dev-gate patterns
|
|
8
|
+
* that are dead code or unsupported in some bundlers Pyreon ships to.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* Pyreon publishes libraries to npm. Consumers compile those libraries
|
|
11
|
+
* with whatever bundler they use — Vite, Webpack (Next.js), Rolldown,
|
|
12
|
+
* esbuild, Rollup, Parcel, Bun. The framework should not ship dev gates
|
|
13
|
+
* that only fire in one bundler.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
+
* **Two broken patterns this rule catches:**
|
|
16
|
+
*
|
|
17
|
+
* 1. `typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'`
|
|
18
|
+
* The `typeof process` guard isn't replaced by Vite, evaluates to
|
|
19
|
+
* `false` in the browser, and the whole expression is dead. Wrapped
|
|
20
|
+
* dev warnings never fire for users running Vite browser builds.
|
|
15
21
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
22
|
+
* 2. `import.meta.env.DEV` (and the `(import.meta as ViteMeta).env?.DEV`
|
|
23
|
+
* cast variant). Vite/Rolldown literal-replace this at build time, but
|
|
24
|
+
* Webpack/esbuild/Rollup/Parcel/Bun/Node-direct don't. In a Pyreon
|
|
25
|
+
* library shipped to a Next.js (Webpack) app, dev warnings never fire
|
|
26
|
+
* — even in development. PR #200 introduced this pattern as the
|
|
27
|
+
* "correct" replacement for the typeof-process compound; that
|
|
28
|
+
* direction was wrong for library code.
|
|
20
29
|
*
|
|
21
|
-
* The
|
|
22
|
-
*
|
|
30
|
+
* **The bundler-agnostic standard** (used by React, Vue, Preact, Solid,
|
|
31
|
+
* MobX, Redux):
|
|
23
32
|
*
|
|
24
33
|
* ```ts
|
|
25
|
-
*
|
|
26
|
-
* if (!import.meta.env?.DEV) return
|
|
34
|
+
* if (process.env.NODE_ENV !== 'production') console.warn('...')
|
|
27
35
|
* ```
|
|
28
36
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
37
|
+
* Every modern bundler auto-replaces `process.env.NODE_ENV` at consumer
|
|
38
|
+
* build time. No `typeof process` guard needed — bundlers replace the
|
|
39
|
+
* literal regardless of whether `process` is otherwise defined.
|
|
31
40
|
*
|
|
32
41
|
* Reference implementation: `packages/fundamentals/flow/src/layout.ts:warnIgnoredOptions`.
|
|
33
42
|
*
|
|
34
|
-
* **Auto-fix**: replaces the
|
|
35
|
-
*
|
|
36
|
-
* the variable name and downstream usages may need updating in callers.
|
|
43
|
+
* **Auto-fix**: replaces the broken expression with
|
|
44
|
+
* `process.env.NODE_ENV !== 'production'`.
|
|
37
45
|
*
|
|
38
|
-
* **Server-only exemption**: projects configure `exemptPaths` per-file
|
|
39
|
-
* server-only code (Node environments where
|
|
40
|
-
*
|
|
46
|
+
* **Server-only exemption**: projects configure `exemptPaths` per-file
|
|
47
|
+
* for server-only code (Node environments where the typeof-process
|
|
48
|
+
* compound is harmless). Configure in `.pyreonlintrc.json`:
|
|
41
49
|
*
|
|
42
50
|
* {
|
|
43
51
|
* "rules": {
|
|
@@ -54,54 +62,49 @@ export const noProcessDevGate: Rule = {
|
|
|
54
62
|
id: 'pyreon/no-process-dev-gate',
|
|
55
63
|
category: 'architecture',
|
|
56
64
|
description:
|
|
57
|
-
'Forbid `typeof process !== "undefined" && process.env.NODE_ENV !== "production"`
|
|
65
|
+
'Forbid bundler-coupled dev gates: `typeof process !== "undefined" && process.env.NODE_ENV !== "production"` is dead in Vite browser bundles, and `import.meta.env.DEV` is Vite/Rolldown-only (dead in Webpack/Next.js, esbuild, Rollup, Parcel, Bun). Use bundler-agnostic `process.env.NODE_ENV !== "production"` — every modern bundler auto-replaces it at consumer build time.',
|
|
58
66
|
severity: 'error',
|
|
59
67
|
fixable: true,
|
|
60
68
|
},
|
|
61
69
|
create(context) {
|
|
62
|
-
// Skip test files — vitest has `process
|
|
63
|
-
//
|
|
70
|
+
// Skip test files — vitest has both `process` and Vite's `import.meta.env`,
|
|
71
|
+
// both broken patterns work there, and tests aren't shipped to users.
|
|
64
72
|
if (isTestFile(context.getFilePath())) return {}
|
|
65
73
|
|
|
66
74
|
// Configurable `exemptPaths` option for server-only directories.
|
|
67
75
|
if (isPathExempt(context)) return {}
|
|
68
76
|
|
|
77
|
+
const REPLACEMENT = `process.env.NODE_ENV !== 'production'`
|
|
78
|
+
|
|
69
79
|
/**
|
|
70
|
-
*
|
|
71
|
-
* `LogicalExpression` whose two sides are:
|
|
72
|
-
*
|
|
73
|
-
* 1. `typeof process !== 'undefined'` (a UnaryExpression on the LHS
|
|
74
|
-
* of a BinaryExpression with operator `!==`)
|
|
75
|
-
* 2. `process.env.NODE_ENV !== 'production'` (a MemberExpression on
|
|
76
|
-
* the LHS of a BinaryExpression with operator `!==`)
|
|
77
|
-
*
|
|
78
|
-
* The order can be either way (process check first or NODE_ENV check
|
|
79
|
-
* first), and the operator can be `&&` or `||` (we only flag `&&`
|
|
80
|
-
* because `||` doesn't make sense as a dev gate).
|
|
80
|
+
* Pattern 1: `typeof process !== 'undefined'`
|
|
81
81
|
*/
|
|
82
82
|
function isTypeofProcessCheck(node: any): boolean {
|
|
83
|
-
// typeof process !== 'undefined'
|
|
84
83
|
if (node?.type !== 'BinaryExpression') return false
|
|
85
84
|
if (node.operator !== '!==' && node.operator !== '!=') return false
|
|
86
85
|
const left = node.left
|
|
87
86
|
const right = node.right
|
|
88
87
|
if (left?.type !== 'UnaryExpression' || left.operator !== 'typeof') return false
|
|
89
88
|
if (left.argument?.type !== 'Identifier' || left.argument.name !== 'process') return false
|
|
90
|
-
|
|
89
|
+
return (
|
|
91
90
|
(right?.type === 'Literal' || right?.type === 'StringLiteral') &&
|
|
92
91
|
right.value === 'undefined'
|
|
93
|
-
)
|
|
94
|
-
return true
|
|
95
|
-
}
|
|
96
|
-
return false
|
|
92
|
+
)
|
|
97
93
|
}
|
|
98
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Pattern 1: `process.env.NODE_ENV !== 'production'` — also matches
|
|
97
|
+
* optional-chaining variants (`process?.env?.NODE_ENV`,
|
|
98
|
+
* `process.env?.NODE_ENV`). ESTree wraps optional access in a
|
|
99
|
+
* `ChainExpression` whose `.expression` is the underlying
|
|
100
|
+
* `MemberExpression` chain.
|
|
101
|
+
*/
|
|
99
102
|
function isNodeEnvCheck(node: any): boolean {
|
|
100
|
-
// process.env.NODE_ENV !== 'production'
|
|
101
103
|
if (node?.type !== 'BinaryExpression') return false
|
|
102
104
|
if (node.operator !== '!==' && node.operator !== '!=') return false
|
|
103
|
-
|
|
105
|
+
let left = node.left
|
|
104
106
|
const right = node.right
|
|
107
|
+
if (left?.type === 'ChainExpression') left = left.expression
|
|
105
108
|
if (left?.type !== 'MemberExpression') return false
|
|
106
109
|
if (left.object?.type !== 'MemberExpression') return false
|
|
107
110
|
if (left.object.object?.type !== 'Identifier' || left.object.object.name !== 'process') {
|
|
@@ -111,42 +114,118 @@ export const noProcessDevGate: Rule = {
|
|
|
111
114
|
return false
|
|
112
115
|
}
|
|
113
116
|
if (left.property?.type !== 'Identifier' || left.property.name !== 'NODE_ENV') return false
|
|
114
|
-
|
|
117
|
+
return (
|
|
115
118
|
(right?.type === 'Literal' || right?.type === 'StringLiteral') &&
|
|
116
119
|
right.value === 'production'
|
|
117
|
-
)
|
|
118
|
-
return true
|
|
119
|
-
}
|
|
120
|
-
return false
|
|
120
|
+
)
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Match the typeof-process compound: a `LogicalExpression` whose
|
|
125
|
+
* sides are a typeof-process check + a NODE_ENV check, in either
|
|
126
|
+
* order, joined with `&&`.
|
|
127
|
+
*/
|
|
128
|
+
function isTypeofCompound(node: any): boolean {
|
|
124
129
|
if (node?.type !== 'LogicalExpression') return false
|
|
125
130
|
if (node.operator !== '&&') return false
|
|
126
|
-
// Order can be (typeof process) && (NODE_ENV) OR vice versa
|
|
127
131
|
return (
|
|
128
132
|
(isTypeofProcessCheck(node.left) && isNodeEnvCheck(node.right)) ||
|
|
129
133
|
(isNodeEnvCheck(node.left) && isTypeofProcessCheck(node.right))
|
|
130
134
|
)
|
|
131
135
|
}
|
|
132
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Strip layers an `import.meta.env.DEV` access can hide behind:
|
|
139
|
+
* - `ChainExpression` (optional chaining)
|
|
140
|
+
* - `TSAsExpression` / `TSTypeAssertion` (`(import.meta as ViteMeta)`)
|
|
141
|
+
* - `ParenthesizedExpression` (just parens)
|
|
142
|
+
*/
|
|
143
|
+
function unwrap(node: any): any {
|
|
144
|
+
let n = node
|
|
145
|
+
while (n) {
|
|
146
|
+
if (n.type === 'ChainExpression') n = n.expression
|
|
147
|
+
else if (n.type === 'TSAsExpression' || n.type === 'TSTypeAssertion') n = n.expression
|
|
148
|
+
else if (n.type === 'ParenthesizedExpression') n = n.expression
|
|
149
|
+
else break
|
|
150
|
+
}
|
|
151
|
+
return n
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isImportMeta(node: any): boolean {
|
|
155
|
+
const n = unwrap(node)
|
|
156
|
+
if (n?.type !== 'MetaProperty') return false
|
|
157
|
+
const meta = n.meta
|
|
158
|
+
const prop = n.property
|
|
159
|
+
return (
|
|
160
|
+
meta?.type === 'Identifier' &&
|
|
161
|
+
meta.name === 'import' &&
|
|
162
|
+
prop?.type === 'Identifier' &&
|
|
163
|
+
prop.name === 'meta'
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Pattern 2: `import.meta.env.DEV` access (any optional/cast variant).
|
|
169
|
+
* Returns the outermost expression node so the autofix replaces the
|
|
170
|
+
* full `import.meta.env.DEV` access (not just the `.DEV` property).
|
|
171
|
+
*/
|
|
172
|
+
function isImportMetaEnvDev(node: any): boolean {
|
|
173
|
+
const outer = unwrap(node)
|
|
174
|
+
if (outer?.type !== 'MemberExpression') return false
|
|
175
|
+
if (outer.property?.type !== 'Identifier' || outer.property.name !== 'DEV') return false
|
|
176
|
+
// outer.object should resolve to `import.meta.env` (an inner MemberExpression
|
|
177
|
+
// whose object is `import.meta` and property is `env`).
|
|
178
|
+
const envAccess = unwrap(outer.object)
|
|
179
|
+
if (envAccess?.type !== 'MemberExpression') return false
|
|
180
|
+
if (envAccess.property?.type !== 'Identifier' || envAccess.property.name !== 'env') {
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
return isImportMeta(envAccess.object)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Track ChainExpression-wrapped MemberExpressions so the inner visitor
|
|
187
|
+
// doesn't double-flag the same access.
|
|
188
|
+
const handledNodes = new WeakSet<object>()
|
|
189
|
+
|
|
133
190
|
const callbacks: VisitorCallbacks = {
|
|
134
191
|
LogicalExpression(node: any) {
|
|
135
|
-
if (!
|
|
136
|
-
|
|
192
|
+
if (!isTypeofCompound(node)) return
|
|
193
|
+
const span = getSpan(node)
|
|
194
|
+
context.report({
|
|
195
|
+
message:
|
|
196
|
+
'`typeof process !== "undefined" && process.env.NODE_ENV !== "production"` is dead code in real Vite browser bundles — Vite does not polyfill `process`, so the guard is `false` and any wrapped dev warnings never fire. Use the bundler-agnostic `process.env.NODE_ENV !== "production"` (no typeof guard) — every modern bundler replaces it at consumer build time. Reference: `packages/fundamentals/flow/src/layout.ts`.',
|
|
197
|
+
span,
|
|
198
|
+
fix: { span, replacement: REPLACEMENT },
|
|
199
|
+
})
|
|
200
|
+
},
|
|
201
|
+
ChainExpression(node: any) {
|
|
202
|
+
// Catch `import.meta.env?.DEV` shapes wrapped in ChainExpression at
|
|
203
|
+
// the outermost layer (e.g. when the optional `?.` is on the
|
|
204
|
+
// outermost `.DEV` access). The inner MemberExpression is then
|
|
205
|
+
// marked handled so the MemberExpression visitor skips it.
|
|
206
|
+
if (!isImportMetaEnvDev(node)) return
|
|
207
|
+
const inner = unwrap(node)
|
|
208
|
+
if (inner) handledNodes.add(inner)
|
|
209
|
+
const span = getSpan(node)
|
|
210
|
+
context.report({
|
|
211
|
+
message:
|
|
212
|
+
'`import.meta.env.DEV` is Vite/Rolldown-specific. In a Pyreon library shipped to consumers using Webpack (Next.js), esbuild, Rollup, Parcel, or Bun, `import.meta.env.DEV` is undefined and dev warnings never fire — even in development. Use bundler-agnostic `process.env.NODE_ENV !== "production"` instead. Reference: `packages/fundamentals/flow/src/layout.ts`.',
|
|
213
|
+
span,
|
|
214
|
+
fix: { span, replacement: REPLACEMENT },
|
|
215
|
+
})
|
|
216
|
+
},
|
|
217
|
+
MemberExpression(node: any) {
|
|
218
|
+
if (handledNodes.has(node)) return
|
|
219
|
+
if (!isImportMetaEnvDev(node)) return
|
|
220
|
+
// Only flag the OUTERMOST `.DEV` access — the visitor will also
|
|
221
|
+
// hit the inner `.env` MemberExpression, which we want to skip.
|
|
222
|
+
if (node.property?.name !== 'DEV') return
|
|
137
223
|
const span = getSpan(node)
|
|
138
|
-
// Auto-fix: replace the entire `typeof process ... && process.env.NODE_ENV ...`
|
|
139
|
-
// expression with `import.meta.env?.DEV === true`. We use optional
|
|
140
|
-
// chaining + strict equality so the expression is `false` (not
|
|
141
|
-
// `undefined`) when `import.meta.env` is missing — preserving the
|
|
142
|
-
// boolean shape callers expect.
|
|
143
|
-
const replacement = 'import.meta.env?.DEV === true'
|
|
144
|
-
|
|
145
224
|
context.report({
|
|
146
225
|
message:
|
|
147
|
-
'`
|
|
226
|
+
'`import.meta.env.DEV` is Vite/Rolldown-specific. In a Pyreon library shipped to consumers using Webpack (Next.js), esbuild, Rollup, Parcel, or Bun, `import.meta.env.DEV` is undefined and dev warnings never fire — even in development. Use bundler-agnostic `process.env.NODE_ENV !== "production"` instead. Reference: `packages/fundamentals/flow/src/layout.ts`.',
|
|
148
227
|
span,
|
|
149
|
-
fix: { span, replacement },
|
|
228
|
+
fix: { span, replacement: REPLACEMENT },
|
|
150
229
|
})
|
|
151
230
|
},
|
|
152
231
|
}
|
package/src/rules/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { noTernaryConditional } from './jsx/no-ternary-conditional'
|
|
|
33
33
|
import { useByNotKey } from './jsx/use-by-not-key'
|
|
34
34
|
import { noDomInSetup } from './lifecycle/no-dom-in-setup'
|
|
35
35
|
import { noEffectInMount } from './lifecycle/no-effect-in-mount'
|
|
36
|
+
import { noImperativeEffectOnCreate } from './lifecycle/no-imperative-effect-on-create'
|
|
36
37
|
// Lifecycle
|
|
37
38
|
import { noMissingCleanup } from './lifecycle/no-missing-cleanup'
|
|
38
39
|
import { noMountInEffect } from './lifecycle/no-mount-in-effect'
|
|
@@ -42,21 +43,28 @@ import { noEffectInFor } from './performance/no-effect-in-for'
|
|
|
42
43
|
import { noLargeForWithoutBy } from './performance/no-large-for-without-by'
|
|
43
44
|
import { preferShowOverDisplay } from './performance/prefer-show-over-display'
|
|
44
45
|
// Reactivity
|
|
46
|
+
import { noAsyncEffect } from './reactivity/no-async-effect'
|
|
45
47
|
import { noBareSignalInJsx } from './reactivity/no-bare-signal-in-jsx'
|
|
46
48
|
import { noContextDestructure } from './reactivity/no-context-destructure'
|
|
47
49
|
import { noEffectAssignment } from './reactivity/no-effect-assignment'
|
|
48
50
|
import { noNestedEffect } from './reactivity/no-nested-effect'
|
|
49
51
|
import { noPeekInTracked } from './reactivity/no-peek-in-tracked'
|
|
52
|
+
import { noSignalCallWrite } from './reactivity/no-signal-call-write'
|
|
50
53
|
import { noSignalInLoop } from './reactivity/no-signal-in-loop'
|
|
51
54
|
import { noSignalInProps } from './reactivity/no-signal-in-props'
|
|
52
55
|
import { noSignalLeak } from './reactivity/no-signal-leak'
|
|
53
56
|
import { noUnbatchedUpdates } from './reactivity/no-unbatched-updates'
|
|
54
57
|
import { preferComputed } from './reactivity/prefer-computed'
|
|
58
|
+
import { storageSignalVForwarding } from './reactivity/storage-signal-v-forwarding'
|
|
55
59
|
// Router
|
|
56
60
|
import { noHrefNavigation } from './router/no-href-navigation'
|
|
57
61
|
import { noImperativeNavigateInRender } from './router/no-imperative-navigate-in-render'
|
|
58
62
|
import { noMissingFallback } from './router/no-missing-fallback'
|
|
59
63
|
import { preferUseIsActive } from './router/prefer-use-is-active'
|
|
64
|
+
// SSG (M3.5)
|
|
65
|
+
import { invalidLoaderExport } from './ssg/invalid-loader-export'
|
|
66
|
+
import { missingGetStaticPaths } from './ssg/missing-get-static-paths'
|
|
67
|
+
import { revalidateNotPureLiteral } from './ssg/revalidate-not-pure-literal'
|
|
60
68
|
import { noMismatchRisk } from './ssr/no-mismatch-risk'
|
|
61
69
|
// SSR
|
|
62
70
|
import { noWindowInSsr } from './ssr/no-window-in-ssr'
|
|
@@ -72,7 +80,8 @@ import { noThemeOutsideProvider } from './styling/no-theme-outside-provider'
|
|
|
72
80
|
import { preferCx } from './styling/prefer-cx'
|
|
73
81
|
|
|
74
82
|
export const allRules: Rule[] = [
|
|
75
|
-
// Reactivity (
|
|
83
|
+
// Reactivity (13)
|
|
84
|
+
noAsyncEffect,
|
|
76
85
|
noBareSignalInJsx,
|
|
77
86
|
noContextDestructure,
|
|
78
87
|
noSignalInLoop,
|
|
@@ -83,6 +92,8 @@ export const allRules: Rule[] = [
|
|
|
83
92
|
preferComputed,
|
|
84
93
|
noEffectAssignment,
|
|
85
94
|
noSignalLeak,
|
|
95
|
+
noSignalCallWrite,
|
|
96
|
+
storageSignalVForwarding,
|
|
86
97
|
// JSX (11)
|
|
87
98
|
noMapInJsx,
|
|
88
99
|
useByNotKey,
|
|
@@ -95,11 +106,12 @@ export const allRules: Rule[] = [
|
|
|
95
106
|
noMissingForBy,
|
|
96
107
|
noPropsDestructure,
|
|
97
108
|
noChildrenAccess,
|
|
98
|
-
// Lifecycle (
|
|
109
|
+
// Lifecycle (5)
|
|
99
110
|
noMissingCleanup,
|
|
100
111
|
noMountInEffect,
|
|
101
112
|
noEffectInMount,
|
|
102
113
|
noDomInSetup,
|
|
114
|
+
noImperativeEffectOnCreate,
|
|
103
115
|
// Performance (4)
|
|
104
116
|
noLargeForWithoutBy,
|
|
105
117
|
noEffectInFor,
|
|
@@ -143,6 +155,10 @@ export const allRules: Rule[] = [
|
|
|
143
155
|
noImperativeNavigateInRender,
|
|
144
156
|
noMissingFallback,
|
|
145
157
|
preferUseIsActive,
|
|
158
|
+
// SSG (3) — M3.5
|
|
159
|
+
invalidLoaderExport,
|
|
160
|
+
missingGetStaticPaths,
|
|
161
|
+
revalidateNotPureLiteral,
|
|
146
162
|
]
|
|
147
163
|
|
|
148
164
|
// Re-export all rules individually
|
|
@@ -151,6 +167,7 @@ export {
|
|
|
151
167
|
dialogA11y,
|
|
152
168
|
noAndConditional,
|
|
153
169
|
// Reactivity
|
|
170
|
+
noAsyncEffect,
|
|
154
171
|
noBareSignalInJsx,
|
|
155
172
|
noContextDestructure,
|
|
156
173
|
noChildrenAccess,
|
|
@@ -169,6 +186,7 @@ export {
|
|
|
169
186
|
noErrorWithoutPrefix,
|
|
170
187
|
noHrefNavigation,
|
|
171
188
|
noHtmlFor,
|
|
189
|
+
noImperativeEffectOnCreate,
|
|
172
190
|
noImperativeNavigateInRender,
|
|
173
191
|
noIndexAsBy,
|
|
174
192
|
// Styling
|
|
@@ -189,6 +207,7 @@ export {
|
|
|
189
207
|
noPeekInTracked,
|
|
190
208
|
noProcessDevGate,
|
|
191
209
|
noPropsDestructure,
|
|
210
|
+
noSignalCallWrite,
|
|
192
211
|
requireBrowserSmokeTest,
|
|
193
212
|
// Hooks
|
|
194
213
|
noRawAddEventListener,
|
|
@@ -214,6 +233,10 @@ export {
|
|
|
214
233
|
preferRequestContext,
|
|
215
234
|
preferShowOverDisplay,
|
|
216
235
|
preferUseIsActive,
|
|
236
|
+
// SSG (M3.5)
|
|
237
|
+
invalidLoaderExport,
|
|
238
|
+
missingGetStaticPaths,
|
|
239
|
+
revalidateNotPureLiteral,
|
|
217
240
|
// Accessibility
|
|
218
241
|
toastA11y,
|
|
219
242
|
useByNotKey,
|