@pyreon/lint 0.11.5 → 0.11.6

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.
Files changed (86) hide show
  1. package/README.md +91 -91
  2. package/lib/analysis/cli.js.html +1 -1
  3. package/lib/analysis/index.js.html +1 -1
  4. package/lib/cli.js +214 -1
  5. package/lib/cli.js.map +1 -1
  6. package/lib/index.js +207 -1
  7. package/lib/index.js.map +1 -1
  8. package/lib/types/index.d.ts +30 -5
  9. package/lib/types/index.d.ts.map +1 -1
  10. package/package.json +15 -15
  11. package/src/cache.ts +1 -1
  12. package/src/cli.ts +38 -28
  13. package/src/config/ignore.ts +23 -23
  14. package/src/config/loader.ts +8 -8
  15. package/src/config/presets.ts +11 -11
  16. package/src/index.ts +14 -12
  17. package/src/lint.ts +19 -19
  18. package/src/lsp/index.ts +225 -0
  19. package/src/reporter.ts +17 -17
  20. package/src/rules/accessibility/dialog-a11y.ts +10 -10
  21. package/src/rules/accessibility/overlay-a11y.ts +11 -11
  22. package/src/rules/accessibility/toast-a11y.ts +11 -11
  23. package/src/rules/architecture/dev-guard-warnings.ts +19 -19
  24. package/src/rules/architecture/no-circular-import.ts +16 -16
  25. package/src/rules/architecture/no-cross-layer-import.ts +35 -35
  26. package/src/rules/architecture/no-deep-import.ts +7 -7
  27. package/src/rules/architecture/no-error-without-prefix.ts +20 -20
  28. package/src/rules/form/no-submit-without-validation.ts +13 -13
  29. package/src/rules/form/no-unregistered-field.ts +12 -12
  30. package/src/rules/form/prefer-field-array.ts +11 -11
  31. package/src/rules/hooks/no-raw-addeventlistener.ts +9 -9
  32. package/src/rules/hooks/no-raw-localstorage.ts +11 -11
  33. package/src/rules/hooks/no-raw-setinterval.ts +11 -11
  34. package/src/rules/index.ts +60 -57
  35. package/src/rules/jsx/no-and-conditional.ts +8 -8
  36. package/src/rules/jsx/no-children-access.ts +12 -12
  37. package/src/rules/jsx/no-classname.ts +10 -10
  38. package/src/rules/jsx/no-htmlfor.ts +10 -10
  39. package/src/rules/jsx/no-index-as-by.ts +17 -17
  40. package/src/rules/jsx/no-map-in-jsx.ts +9 -9
  41. package/src/rules/jsx/no-missing-for-by.ts +9 -9
  42. package/src/rules/jsx/no-onchange.ts +12 -12
  43. package/src/rules/jsx/no-props-destructure.ts +11 -11
  44. package/src/rules/jsx/no-ternary-conditional.ts +8 -8
  45. package/src/rules/jsx/use-by-not-key.ts +12 -12
  46. package/src/rules/lifecycle/no-dom-in-setup.ts +18 -18
  47. package/src/rules/lifecycle/no-effect-in-mount.ts +11 -11
  48. package/src/rules/lifecycle/no-missing-cleanup.ts +19 -19
  49. package/src/rules/lifecycle/no-mount-in-effect.ts +11 -11
  50. package/src/rules/performance/no-eager-import.ts +7 -7
  51. package/src/rules/performance/no-effect-in-for.ts +10 -10
  52. package/src/rules/performance/no-large-for-without-by.ts +9 -9
  53. package/src/rules/performance/prefer-show-over-display.ts +16 -16
  54. package/src/rules/reactivity/no-bare-signal-in-jsx.ts +10 -10
  55. package/src/rules/reactivity/no-context-destructure.ts +45 -0
  56. package/src/rules/reactivity/no-effect-assignment.ts +16 -16
  57. package/src/rules/reactivity/no-nested-effect.ts +10 -10
  58. package/src/rules/reactivity/no-peek-in-tracked.ts +10 -10
  59. package/src/rules/reactivity/no-signal-in-loop.ts +13 -13
  60. package/src/rules/reactivity/no-signal-leak.ts +9 -9
  61. package/src/rules/reactivity/no-unbatched-updates.ts +12 -12
  62. package/src/rules/reactivity/prefer-computed.ts +13 -13
  63. package/src/rules/router/index.ts +4 -4
  64. package/src/rules/router/no-href-navigation.ts +14 -14
  65. package/src/rules/router/no-imperative-navigate-in-render.ts +19 -19
  66. package/src/rules/router/no-missing-fallback.ts +16 -16
  67. package/src/rules/router/prefer-use-is-active.ts +11 -11
  68. package/src/rules/ssr/no-mismatch-risk.ts +11 -11
  69. package/src/rules/ssr/no-window-in-ssr.ts +22 -22
  70. package/src/rules/ssr/prefer-request-context.ts +14 -14
  71. package/src/rules/store/no-duplicate-store-id.ts +9 -9
  72. package/src/rules/store/no-mutate-store-state.ts +11 -11
  73. package/src/rules/store/no-store-outside-provider.ts +15 -15
  74. package/src/rules/styling/no-dynamic-styled.ts +13 -13
  75. package/src/rules/styling/no-inline-style-object.ts +10 -10
  76. package/src/rules/styling/no-theme-outside-provider.ts +11 -11
  77. package/src/rules/styling/prefer-cx.ts +12 -12
  78. package/src/runner.ts +13 -13
  79. package/src/tests/lsp.test.ts +88 -0
  80. package/src/tests/runner.test.ts +325 -325
  81. package/src/types.ts +15 -15
  82. package/src/utils/ast.ts +50 -50
  83. package/src/utils/imports.ts +53 -53
  84. package/src/utils/index.ts +5 -5
  85. package/src/utils/source.ts +2 -2
  86. package/src/watcher.ts +19 -19
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Minimal LSP server for @pyreon/lint.
3
+ *
4
+ * Provides real-time Pyreon-specific diagnostics in editors that support
5
+ * the Language Server Protocol (VS Code, Neovim, etc.).
6
+ *
7
+ * Usage: pyreon-lint --lsp
8
+ *
9
+ * The server communicates via JSON-RPC over stdin/stdout following the
10
+ * LSP specification (https://microsoft.github.io/language-server-protocol/).
11
+ *
12
+ * Supported capabilities:
13
+ * - textDocument/didOpen — lint on open
14
+ * - textDocument/didSave — lint on save
15
+ * - textDocument/didChange — lint on change (debounced)
16
+ *
17
+ * @module
18
+ */
19
+
20
+ import { AstCache } from '../cache'
21
+ import { getPreset } from '../config/presets'
22
+ import { allRules } from '../rules/index'
23
+ import { lintFile } from '../runner'
24
+ import type { Diagnostic, LintConfig } from '../types'
25
+
26
+ const cache = new AstCache()
27
+ const config: LintConfig = getPreset('recommended')
28
+
29
+ // ─── JSON-RPC message types ────────────────────────────────────────────────
30
+
31
+ interface JsonRpcMessage {
32
+ jsonrpc: '2.0'
33
+ id?: number | string | undefined
34
+ method?: string | undefined
35
+ params?: any
36
+ result?: any
37
+ }
38
+
39
+ // ─── LSP Diagnostic conversion ─────────────────────────────────────────────
40
+
41
+ interface LspDiagnostic {
42
+ range: { start: { line: number; character: number }; end: { line: number; character: number } }
43
+ severity: number
44
+ source: string
45
+ message: string
46
+ code: string
47
+ }
48
+
49
+ function toLspDiagnostics(diagnostics: Diagnostic[]): LspDiagnostic[] {
50
+ return diagnostics.map((d) => ({
51
+ range: {
52
+ start: { line: d.loc.line - 1, character: d.loc.column - 1 },
53
+ end: { line: d.loc.line - 1, character: d.loc.column - 1 + (d.span.end - d.span.start) },
54
+ },
55
+ severity: d.severity === 'error' ? 1 : d.severity === 'warn' ? 2 : 3,
56
+ source: 'pyreon-lint',
57
+ message: d.message,
58
+ code: d.ruleId,
59
+ }))
60
+ }
61
+
62
+ // ─── Lint a document ───────────────────────────────────────────────────────
63
+
64
+ function lintDocument(uri: string, text: string): LspDiagnostic[] {
65
+ try {
66
+ const filePath = uri.replace('file://', '')
67
+ const result = lintFile(filePath, text, allRules, config, cache)
68
+ return toLspDiagnostics(result.diagnostics)
69
+ } catch {
70
+ // Parse errors, unsupported file types — return empty diagnostics
71
+ return []
72
+ }
73
+ }
74
+
75
+ // ─── Debounce ──────────────────────────────────────────────────────────────
76
+
77
+ const DEBOUNCE_MS = 150
78
+ const debounceTimers = new Map<string, ReturnType<typeof setTimeout>>()
79
+
80
+ function debounceLint(uri: string, text: string): void {
81
+ const existing = debounceTimers.get(uri)
82
+ if (existing) clearTimeout(existing)
83
+ debounceTimers.set(
84
+ uri,
85
+ setTimeout(() => {
86
+ debounceTimers.delete(uri)
87
+ const diagnostics = lintDocument(uri, text)
88
+ sendNotification('textDocument/publishDiagnostics', { uri, diagnostics })
89
+ }, DEBOUNCE_MS),
90
+ )
91
+ }
92
+
93
+ // ─── Message handling ──────────────────────────────────────────────────────
94
+
95
+ const openDocuments = new Map<string, string>()
96
+
97
+ function handleMessage(msg: JsonRpcMessage): JsonRpcMessage | null {
98
+ if (msg.method === 'initialize') {
99
+ return {
100
+ jsonrpc: '2.0',
101
+ id: msg.id,
102
+ result: {
103
+ capabilities: {
104
+ textDocumentSync: 1, // Full sync
105
+ diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false },
106
+ },
107
+ serverInfo: { name: 'pyreon-lint', version: '0.11.5' },
108
+ },
109
+ }
110
+ }
111
+
112
+ if (msg.method === 'initialized') {
113
+ return null // no response needed
114
+ }
115
+
116
+ if (msg.method === 'textDocument/didOpen') {
117
+ const { uri, text } = msg.params.textDocument
118
+ openDocuments.set(uri, text)
119
+ const diagnostics = lintDocument(uri, text)
120
+ sendNotification('textDocument/publishDiagnostics', { uri, diagnostics })
121
+ return null
122
+ }
123
+
124
+ if (msg.method === 'textDocument/didChange') {
125
+ const uri = msg.params.textDocument.uri
126
+ const text = msg.params.contentChanges[0]?.text
127
+ if (text != null) {
128
+ openDocuments.set(uri, text)
129
+ // Debounce: wait 150ms after last keystroke before linting
130
+ debounceLint(uri, text)
131
+ }
132
+ return null
133
+ }
134
+
135
+ if (msg.method === 'textDocument/didSave') {
136
+ const uri = msg.params.textDocument.uri
137
+ const text = openDocuments.get(uri)
138
+ if (text) {
139
+ const diagnostics = lintDocument(uri, text)
140
+ sendNotification('textDocument/publishDiagnostics', { uri, diagnostics })
141
+ }
142
+ return null
143
+ }
144
+
145
+ if (msg.method === 'textDocument/didClose') {
146
+ const uri = msg.params.textDocument.uri
147
+ openDocuments.delete(uri)
148
+ sendNotification('textDocument/publishDiagnostics', { uri, diagnostics: [] })
149
+ return null
150
+ }
151
+
152
+ if (msg.method === 'shutdown') {
153
+ return { jsonrpc: '2.0', id: msg.id, result: null }
154
+ }
155
+
156
+ if (msg.method === 'exit') {
157
+ process.exit(0)
158
+ }
159
+
160
+ // Unknown method
161
+ if (msg.id != null) {
162
+ return {
163
+ jsonrpc: '2.0',
164
+ id: msg.id,
165
+ result: null,
166
+ }
167
+ }
168
+
169
+ return null
170
+ }
171
+
172
+ // ─── JSON-RPC transport (stdin/stdout) ─────────────────────────────────────
173
+
174
+ function sendMessage(msg: JsonRpcMessage) {
175
+ const body = JSON.stringify(msg)
176
+ const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`
177
+ process.stdout.write(header + body)
178
+ }
179
+
180
+ function sendNotification(method: string, params: any) {
181
+ sendMessage({ jsonrpc: '2.0', method, params })
182
+ }
183
+
184
+ /**
185
+ * Start the LSP server. Reads JSON-RPC messages from stdin,
186
+ * processes them, and writes responses to stdout.
187
+ */
188
+ export function startLspServer(): void {
189
+ let buffer = ''
190
+
191
+ process.stdin.setEncoding('utf-8')
192
+ process.stdin.on('data', (chunk: string) => {
193
+ buffer += chunk
194
+
195
+ // Parse Content-Length header + body
196
+ while (true) {
197
+ const headerEnd = buffer.indexOf('\r\n\r\n')
198
+ if (headerEnd === -1) break
199
+
200
+ const header = buffer.slice(0, headerEnd)
201
+ const match = header.match(/Content-Length:\s*(\d+)/i)
202
+ if (!match) {
203
+ buffer = buffer.slice(headerEnd + 4)
204
+ continue
205
+ }
206
+
207
+ const contentLength = Number.parseInt(match[1]!, 10)
208
+ const bodyStart = headerEnd + 4
209
+ if (buffer.length < bodyStart + contentLength) break
210
+
211
+ const body = buffer.slice(bodyStart, bodyStart + contentLength)
212
+ buffer = buffer.slice(bodyStart + contentLength)
213
+
214
+ try {
215
+ const msg = JSON.parse(body) as JsonRpcMessage
216
+ const response = handleMessage(msg)
217
+ if (response) sendMessage(response)
218
+ } catch {
219
+ // malformed JSON — ignore
220
+ }
221
+ }
222
+ })
223
+
224
+ process.stderr.write('[pyreon-lint] LSP server started\n')
225
+ }
package/src/reporter.ts CHANGED
@@ -1,25 +1,25 @@
1
- import type { LintResult, Severity } from "./types"
1
+ import type { LintResult, Severity } from './types'
2
2
 
3
3
  // ANSI colors
4
- const BOLD = "\x1b[1m"
5
- const RED = "\x1b[31m"
6
- const YELLOW = "\x1b[33m"
7
- const BLUE = "\x1b[34m"
8
- const DIM = "\x1b[2m"
9
- const RESET = "\x1b[0m"
4
+ const BOLD = '\x1b[1m'
5
+ const RED = '\x1b[31m'
6
+ const YELLOW = '\x1b[33m'
7
+ const BLUE = '\x1b[34m'
8
+ const DIM = '\x1b[2m'
9
+ const RESET = '\x1b[0m'
10
10
 
11
11
  const SEVERITY_SYMBOL: Record<Severity, string> = {
12
12
  error: `${RED}\u2716${RESET}`,
13
13
  warn: `${YELLOW}\u26A0${RESET}`,
14
14
  info: `${BLUE}\u2139${RESET}`,
15
- off: "",
15
+ off: '',
16
16
  }
17
17
 
18
18
  const SEVERITY_LABEL: Record<Severity, string> = {
19
19
  error: `${RED}error${RESET}`,
20
20
  warn: `${YELLOW}warning${RESET}`,
21
21
  info: `${BLUE}info${RESET}`,
22
- off: "",
22
+ off: '',
23
23
  }
24
24
 
25
25
  /**
@@ -31,7 +31,7 @@ export function formatText(result: LintResult): string {
31
31
  for (const file of result.files) {
32
32
  if (file.diagnostics.length === 0) continue
33
33
 
34
- lines.push("")
34
+ lines.push('')
35
35
  lines.push(`${BOLD}${file.filePath}${RESET}`)
36
36
 
37
37
  for (const d of file.diagnostics) {
@@ -44,20 +44,20 @@ export function formatText(result: LintResult): string {
44
44
 
45
45
  const total = result.totalErrors + result.totalWarnings + result.totalInfos
46
46
  if (total > 0) {
47
- lines.push("")
47
+ lines.push('')
48
48
  const parts: string[] = []
49
49
  if (result.totalErrors > 0)
50
- parts.push(`${RED}${result.totalErrors} error${result.totalErrors === 1 ? "" : "s"}${RESET}`)
50
+ parts.push(`${RED}${result.totalErrors} error${result.totalErrors === 1 ? '' : 's'}${RESET}`)
51
51
  if (result.totalWarnings > 0)
52
52
  parts.push(
53
- `${YELLOW}${result.totalWarnings} warning${result.totalWarnings === 1 ? "" : "s"}${RESET}`,
53
+ `${YELLOW}${result.totalWarnings} warning${result.totalWarnings === 1 ? '' : 's'}${RESET}`,
54
54
  )
55
55
  if (result.totalInfos > 0) parts.push(`${BLUE}${result.totalInfos} info${RESET}`)
56
- lines.push(`${SEVERITY_SYMBOL.error} ${parts.join(", ")}`)
57
- lines.push("")
56
+ lines.push(`${SEVERITY_SYMBOL.error} ${parts.join(', ')}`)
57
+ lines.push('')
58
58
  }
59
59
 
60
- return lines.join("\n")
60
+ return lines.join('\n')
61
61
  }
62
62
 
63
63
  /**
@@ -81,5 +81,5 @@ export function formatCompact(result: LintResult): string {
81
81
  }
82
82
  }
83
83
 
84
- return lines.join("\n")
84
+ return lines.join('\n')
85
85
  }
@@ -1,27 +1,27 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, hasJSXAttribute } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, hasJSXAttribute } from '../../utils/ast'
3
3
 
4
4
  export const dialogA11y: Rule = {
5
5
  meta: {
6
- id: "pyreon/dialog-a11y",
7
- category: "accessibility",
8
- description: "Warn when <dialog> is missing aria-label or aria-labelledby.",
9
- severity: "warn",
6
+ id: 'pyreon/dialog-a11y',
7
+ category: 'accessibility',
8
+ description: 'Warn when <dialog> is missing aria-label or aria-labelledby.',
9
+ severity: 'warn',
10
10
  fixable: false,
11
11
  },
12
12
  create(context) {
13
13
  const callbacks: VisitorCallbacks = {
14
14
  JSXOpeningElement(node: any) {
15
15
  const name = node.name
16
- if (!name || name.type !== "JSXIdentifier" || name.name !== "dialog") return
16
+ if (!name || name.type !== 'JSXIdentifier' || name.name !== 'dialog') return
17
17
 
18
- const hasLabel = hasJSXAttribute(node, "aria-label")
19
- const hasLabelledBy = hasJSXAttribute(node, "aria-labelledby")
18
+ const hasLabel = hasJSXAttribute(node, 'aria-label')
19
+ const hasLabelledBy = hasJSXAttribute(node, 'aria-labelledby')
20
20
 
21
21
  if (!hasLabel && !hasLabelledBy) {
22
22
  context.report({
23
23
  message:
24
- "`<dialog>` missing `aria-label` or `aria-labelledby` — provide an accessible label for screen readers.",
24
+ '`<dialog>` missing `aria-label` or `aria-labelledby` — provide an accessible label for screen readers.',
25
25
  span: getSpan(node),
26
26
  })
27
27
  }
@@ -1,28 +1,28 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, hasJSXAttribute } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, hasJSXAttribute } from '../../utils/ast'
3
3
 
4
4
  export const overlayA11y: Rule = {
5
5
  meta: {
6
- id: "pyreon/overlay-a11y",
7
- category: "accessibility",
8
- description: "Warn when <Overlay> is missing role, aria-label, or aria-labelledby.",
9
- severity: "warn",
6
+ id: 'pyreon/overlay-a11y',
7
+ category: 'accessibility',
8
+ description: 'Warn when <Overlay> is missing role, aria-label, or aria-labelledby.',
9
+ severity: 'warn',
10
10
  fixable: false,
11
11
  },
12
12
  create(context) {
13
13
  const callbacks: VisitorCallbacks = {
14
14
  JSXOpeningElement(node: any) {
15
15
  const name = node.name
16
- if (!name || name.type !== "JSXIdentifier" || name.name !== "Overlay") return
16
+ if (!name || name.type !== 'JSXIdentifier' || name.name !== 'Overlay') return
17
17
 
18
- const hasRole = hasJSXAttribute(node, "role")
19
- const hasLabel = hasJSXAttribute(node, "aria-label")
20
- const hasLabelledBy = hasJSXAttribute(node, "aria-labelledby")
18
+ const hasRole = hasJSXAttribute(node, 'role')
19
+ const hasLabel = hasJSXAttribute(node, 'aria-label')
20
+ const hasLabelledBy = hasJSXAttribute(node, 'aria-labelledby')
21
21
 
22
22
  if (!hasRole && !hasLabel && !hasLabelledBy) {
23
23
  context.report({
24
24
  message:
25
- "`<Overlay>` missing `role`, `aria-label`, or `aria-labelledby` — provide accessibility attributes for screen readers.",
25
+ '`<Overlay>` missing `role`, `aria-label`, or `aria-labelledby` — provide accessibility attributes for screen readers.',
26
26
  span: getSpan(node),
27
27
  })
28
28
  }
@@ -1,29 +1,29 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan, hasJSXAttribute } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan, hasJSXAttribute } from '../../utils/ast'
3
3
 
4
4
  export const toastA11y: Rule = {
5
5
  meta: {
6
- id: "pyreon/toast-a11y",
7
- category: "accessibility",
8
- description: "Warn when toast-like components are missing role or aria-live attributes.",
9
- severity: "warn",
6
+ id: 'pyreon/toast-a11y',
7
+ category: 'accessibility',
8
+ description: 'Warn when toast-like components are missing role or aria-live attributes.',
9
+ severity: 'warn',
10
10
  fixable: false,
11
11
  },
12
12
  create(context) {
13
13
  const callbacks: VisitorCallbacks = {
14
14
  JSXOpeningElement(node: any) {
15
15
  const name = node.name
16
- if (!name || name.type !== "JSXIdentifier") return
16
+ if (!name || name.type !== 'JSXIdentifier') return
17
17
 
18
18
  const tagName: string = name.name
19
19
  // Skip non-PascalCase and the Toaster container itself
20
- if (tagName === "Toaster") return
20
+ if (tagName === 'Toaster') return
21
21
  const firstChar = tagName[0]
22
22
  if (!firstChar || firstChar !== firstChar.toUpperCase()) return
23
- if (!tagName.toLowerCase().includes("toast")) return
23
+ if (!tagName.toLowerCase().includes('toast')) return
24
24
 
25
- const hasRole = hasJSXAttribute(node, "role")
26
- const hasAriaLive = hasJSXAttribute(node, "aria-live")
25
+ const hasRole = hasJSXAttribute(node, 'role')
26
+ const hasAriaLive = hasJSXAttribute(node, 'aria-live')
27
27
 
28
28
  if (!hasRole && !hasAriaLive) {
29
29
  context.report({
@@ -1,23 +1,23 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan } from "../../utils/ast"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan } from '../../utils/ast'
3
3
 
4
4
  export const devGuardWarnings: Rule = {
5
5
  meta: {
6
- id: "pyreon/dev-guard-warnings",
7
- category: "architecture",
8
- description: "Require console.warn/error calls to be wrapped in `if (__DEV__)` guards.",
9
- severity: "error",
6
+ id: 'pyreon/dev-guard-warnings',
7
+ category: 'architecture',
8
+ description: 'Require console.warn/error calls to be wrapped in `if (__DEV__)` guards.',
9
+ severity: 'error',
10
10
  fixable: false,
11
11
  },
12
12
  create(context) {
13
13
  const filePath = context.getFilePath()
14
14
  // Skip test and example files
15
15
  if (
16
- filePath.includes("/tests/") ||
17
- filePath.includes("/test/") ||
18
- filePath.includes("/examples/") ||
19
- filePath.includes(".test.") ||
20
- filePath.includes(".spec.")
16
+ filePath.includes('/tests/') ||
17
+ filePath.includes('/test/') ||
18
+ filePath.includes('/examples/') ||
19
+ filePath.includes('.test.') ||
20
+ filePath.includes('.spec.')
21
21
  ) {
22
22
  return {}
23
23
  }
@@ -25,12 +25,12 @@ export const devGuardWarnings: Rule = {
25
25
  let devGuardDepth = 0
26
26
  const callbacks: VisitorCallbacks = {
27
27
  IfStatement(node: any) {
28
- if (node.test?.type === "Identifier" && node.test.name === "__DEV__") {
28
+ if (node.test?.type === 'Identifier' && node.test.name === '__DEV__') {
29
29
  devGuardDepth++
30
30
  }
31
31
  },
32
- "IfStatement:exit"(node: any) {
33
- if (node.test?.type === "Identifier" && node.test.name === "__DEV__") {
32
+ 'IfStatement:exit'(node: any) {
33
+ if (node.test?.type === 'Identifier' && node.test.name === '__DEV__') {
34
34
  devGuardDepth--
35
35
  }
36
36
  },
@@ -39,11 +39,11 @@ export const devGuardWarnings: Rule = {
39
39
 
40
40
  const callee = node.callee
41
41
  if (
42
- callee?.type === "MemberExpression" &&
43
- callee.object?.type === "Identifier" &&
44
- callee.object.name === "console" &&
45
- callee.property?.type === "Identifier" &&
46
- (callee.property.name === "warn" || callee.property.name === "error")
42
+ callee?.type === 'MemberExpression' &&
43
+ callee.object?.type === 'Identifier' &&
44
+ callee.object.name === 'console' &&
45
+ callee.property?.type === 'Identifier' &&
46
+ (callee.property.name === 'warn' || callee.property.name === 'error')
47
47
  ) {
48
48
  context.report({
49
49
  message: `\`console.${callee.property.name}()\` without \`__DEV__\` guard — dev warnings must be tree-shakeable in production. Wrap in \`if (__DEV__) { ... }\`.`,
@@ -1,16 +1,16 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan } from "../../utils/ast"
3
- import { isPyreonImport } from "../../utils/imports"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan } from '../../utils/ast'
3
+ import { isPyreonImport } from '../../utils/imports'
4
4
 
5
5
  const LAYER_ORDER: Record<string, number> = {
6
- "@pyreon/reactivity": 0,
7
- "@pyreon/core": 1,
8
- "@pyreon/compiler": 1,
9
- "@pyreon/runtime-dom": 2,
10
- "@pyreon/runtime-server": 2,
11
- "@pyreon/router": 3,
12
- "@pyreon/head": 4,
13
- "@pyreon/server": 5,
6
+ '@pyreon/reactivity': 0,
7
+ '@pyreon/core': 1,
8
+ '@pyreon/compiler': 1,
9
+ '@pyreon/runtime-dom': 2,
10
+ '@pyreon/runtime-server': 2,
11
+ '@pyreon/router': 3,
12
+ '@pyreon/head': 4,
13
+ '@pyreon/server': 5,
14
14
  }
15
15
 
16
16
  function getLayer(source: string): number | null {
@@ -19,7 +19,7 @@ function getLayer(source: string): number | null {
19
19
 
20
20
  function getFileLayer(filePath: string): number | null {
21
21
  for (const [pkg, layer] of Object.entries(LAYER_ORDER)) {
22
- const pkgName = pkg.replace("@pyreon/", "")
22
+ const pkgName = pkg.replace('@pyreon/', '')
23
23
  if (filePath.includes(`/packages/core/${pkgName}/`)) return layer
24
24
  }
25
25
  return null
@@ -27,10 +27,10 @@ function getFileLayer(filePath: string): number | null {
27
27
 
28
28
  export const noCircularImport: Rule = {
29
29
  meta: {
30
- id: "pyreon/no-circular-import",
31
- category: "architecture",
32
- description: "Enforce package layer order to prevent circular imports between core packages.",
33
- severity: "error",
30
+ id: 'pyreon/no-circular-import',
31
+ category: 'architecture',
32
+ description: 'Enforce package layer order to prevent circular imports between core packages.',
33
+ severity: 'error',
34
34
  fixable: false,
35
35
  },
36
36
  create(context) {
@@ -1,60 +1,60 @@
1
- import type { Rule, VisitorCallbacks } from "../../types"
2
- import { getSpan } from "../../utils/ast"
3
- import { isPyreonImport } from "../../utils/imports"
1
+ import type { Rule, VisitorCallbacks } from '../../types'
2
+ import { getSpan } from '../../utils/ast'
3
+ import { isPyreonImport } from '../../utils/imports'
4
4
 
5
- type PackageCategory = "core" | "fundamentals" | "tools" | "ui-system"
5
+ type PackageCategory = 'core' | 'fundamentals' | 'tools' | 'ui-system'
6
6
 
7
7
  const CORE_PACKAGES = new Set([
8
- "@pyreon/reactivity",
9
- "@pyreon/core",
10
- "@pyreon/compiler",
11
- "@pyreon/runtime-dom",
12
- "@pyreon/runtime-server",
13
- "@pyreon/router",
14
- "@pyreon/head",
15
- "@pyreon/server",
8
+ '@pyreon/reactivity',
9
+ '@pyreon/core',
10
+ '@pyreon/compiler',
11
+ '@pyreon/runtime-dom',
12
+ '@pyreon/runtime-server',
13
+ '@pyreon/router',
14
+ '@pyreon/head',
15
+ '@pyreon/server',
16
16
  ])
17
17
 
18
18
  const UI_PACKAGES = new Set([
19
- "@pyreon/ui-core",
20
- "@pyreon/styler",
21
- "@pyreon/unistyle",
22
- "@pyreon/elements",
23
- "@pyreon/attrs",
24
- "@pyreon/rocketstyle",
25
- "@pyreon/coolgrid",
26
- "@pyreon/kinetic",
27
- "@pyreon/kinetic-presets",
28
- "@pyreon/connector-document",
29
- "@pyreon/document-primitives",
19
+ '@pyreon/ui-core',
20
+ '@pyreon/styler',
21
+ '@pyreon/unistyle',
22
+ '@pyreon/elements',
23
+ '@pyreon/attrs',
24
+ '@pyreon/rocketstyle',
25
+ '@pyreon/coolgrid',
26
+ '@pyreon/kinetic',
27
+ '@pyreon/kinetic-presets',
28
+ '@pyreon/connector-document',
29
+ '@pyreon/document-primitives',
30
30
  ])
31
31
 
32
32
  function getImportCategory(source: string): PackageCategory | null {
33
- if (CORE_PACKAGES.has(source)) return "core"
34
- if (UI_PACKAGES.has(source)) return "ui-system"
33
+ if (CORE_PACKAGES.has(source)) return 'core'
34
+ if (UI_PACKAGES.has(source)) return 'ui-system'
35
35
  return null
36
36
  }
37
37
 
38
38
  function getFileCategory(filePath: string): PackageCategory | null {
39
- if (filePath.includes("/packages/core/")) return "core"
40
- if (filePath.includes("/packages/ui-system/")) return "ui-system"
41
- if (filePath.includes("/packages/fundamentals/")) return "fundamentals"
42
- if (filePath.includes("/packages/tools/")) return "tools"
39
+ if (filePath.includes('/packages/core/')) return 'core'
40
+ if (filePath.includes('/packages/ui-system/')) return 'ui-system'
41
+ if (filePath.includes('/packages/fundamentals/')) return 'fundamentals'
42
+ if (filePath.includes('/packages/tools/')) return 'tools'
43
43
  return null
44
44
  }
45
45
 
46
46
  export const noCrossLayerImport: Rule = {
47
47
  meta: {
48
- id: "pyreon/no-cross-layer-import",
49
- category: "architecture",
50
- description: "Prevent core packages from importing ui-system packages.",
51
- severity: "error",
48
+ id: 'pyreon/no-cross-layer-import',
49
+ category: 'architecture',
50
+ description: 'Prevent core packages from importing ui-system packages.',
51
+ severity: 'error',
52
52
  fixable: false,
53
53
  },
54
54
  create(context) {
55
55
  const filePath = context.getFilePath()
56
56
  const fileCategory = getFileCategory(filePath)
57
- if (fileCategory !== "core") return {}
57
+ if (fileCategory !== 'core') return {}
58
58
 
59
59
  const callbacks: VisitorCallbacks = {
60
60
  ImportDeclaration(node: any) {
@@ -62,7 +62,7 @@ export const noCrossLayerImport: Rule = {
62
62
  if (!source || !isPyreonImport(source)) return
63
63
 
64
64
  const importCategory = getImportCategory(source)
65
- if (importCategory === "ui-system") {
65
+ if (importCategory === 'ui-system') {
66
66
  context.report({
67
67
  message: `Core package importing ui-system package \`${source}\` — core packages must not depend on ui-system.`,
68
68
  span: getSpan(node),