@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.
@@ -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.14.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": "./lib/cli.js"
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.123.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 — 59 rules across 12 categories, config files, watch mode, AST cache, CLI + LSP',
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 (10), JSX (11), lifecycle (4), performance (4), SSR (3), architecture (7), store (3), form (3), styling (4), hooks (3), accessibility (3), router (4) — 59 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).',
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
- '59 rules across 12 categories',
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 59 rules
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
- '59 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.',
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 59 rules
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` / `if (!import.meta.env.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
- if (!isDevFlag(arg)) return false
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 the broken `typeof process` dev-mode gate
8
- * pattern that is dead code in real Vite browser bundles.
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
- * The pattern this rule catches:
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
- * ```ts
13
- * const __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'
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
- * This works in vitest (Node, `process` is defined) but is **silently dead
17
- * code in real Vite browser bundles** because Vite does not polyfill
18
- * `process` for the client. Every dev warning gated on this constant never
19
- * fires for real users in dev mode.
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 fix is to use `import.meta.env.DEV`, which Vite/Rolldown literal-replace
22
- * at build time:
30
+ * **The bundler-agnostic standard** (used by React, Vue, Preact, Solid,
31
+ * MobX, Redux):
23
32
  *
24
33
  * ```ts
25
- * // No const needed — read directly at the use site so the bundler can fold:
26
- * if (!import.meta.env?.DEV) return
34
+ * if (process.env.NODE_ENV !== 'production') console.warn('...')
27
35
  * ```
28
36
  *
29
- * Vitest sets `import.meta.env.DEV === true` automatically (because it is
30
- * Vite-based), so existing tests continue to pass.
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 assignment with `import.meta.env?.DEV === true`.
35
- * Does NOT delete the const declaration — that has to happen by hand because
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 for
39
- * server-only code (Node environments where `process` is always defined and
40
- * the pattern is correct). Configure in `.pyreonlintrc.json`:
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"` 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.',
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`, the gate works there, and
63
- // tests are not shipped to users. Universal, not a heuristic.
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
- * Match the broken pattern at the AST level. We're looking for any
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
- if (
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
- const left = node.left
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
- if (
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
- function isBrokenDevGate(node: any): boolean {
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 (!isBrokenDevGate(node)) return
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
- '`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`.',
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
  }
@@ -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 (10)
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 (4)
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,