@nitra/cursor 1.8.155 → 1.8.156

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/mdc/js-run.mdc CHANGED
@@ -66,16 +66,16 @@ export const pool = new SQL({ url: env.PG_CONN })
66
66
  а так до GraphQL:
67
67
 
68
68
  ```js
69
- import { checkEnv } from '@nitra/check-env'
69
+ import { checkEnv, env } from '@nitra/check-env'
70
70
  import { GraphQLClient } from '@nitra/graphql-request'
71
71
 
72
72
  checkEnv(['QL', 'X_HASURA_ADMIN_SECRET'])
73
73
 
74
74
  export { gql } from '@nitra/graphql-request'
75
75
 
76
- export const graphQLClientSmart = new GraphQLClient(process.env.QL, {
76
+ export const graphQLClientSmart = new GraphQLClient(env.QL, {
77
77
  headers: {
78
- 'X-Hasura-Admin-Secret': process.env.X_HASURA_ADMIN_SECRET
78
+ 'X-Hasura-Admin-Secret': env.X_HASURA_ADMIN_SECRET
79
79
  }
80
80
  })
81
81
  ```
@@ -95,7 +95,7 @@ import { gql, graphQLClient } from '@nitra/graphql-request'
95
95
 
96
96
  ## CheckEnv
97
97
 
98
- Усі змінні оточення, які використовуються в коді, повинні бути перевірені за допомогою `checkEnv` з пакету `@nitra/check-env`. Це гарантує, що всі необхідні змінні оточення встановлені перед запуском програми. (Виключенням можуть бути задані коментарем)
98
+ Усі змінні оточення, які використовуються в коді, повинні бути перевірені за допомогою `checkEnv` з пакету `@nitra/check-env`. Це гарантує, що всі необхідні змінні оточення встановлені перед запуском програми.
99
99
 
100
100
 
101
101
  ```javascript title="Приклад підключення до PostgreSQL в /src/conn/pg.js"
@@ -106,10 +106,28 @@ checkEnv(['PG_CONN'])
106
106
 
107
107
  export const pool = new SQL({ url: env.PG_CONN })
108
108
 
109
- // @nitra/cursor ignore-next-line checkEnv
110
- console.log(process.env.OPTIONAL_ENV_VAR)
111
109
  ```
112
110
 
111
+
112
+ ## process.env
113
+
114
+ Прямий доступ до `process.env.X` у коді заборонений — його треба замінити на `env`:
115
+
116
+ - **обов'язкова змінна** — `import { checkEnv, env } from '@nitra/check-env'` плюс `checkEnv(['X'])`
117
+ у тому ж файлі (приклад див. вище в розділі **CheckEnv**);
118
+ - **опційна змінна** — `import { env } from 'node:process'`:
119
+
120
+ ```javascript title="Опційна змінна — env з node:process"
121
+ import { env } from 'node:process'
122
+
123
+ console.log(env.OPTIONAL_ENV_VAR)
124
+ ```
125
+
126
+ Тимчасово приглушити перевірку для конкретного рядка можна коментарем
127
+ `// @nitra/cursor ignore-next-line checkEnv` безпосередньо перед використанням
128
+ (escape-hatch для legacy-коду, не для нових файлів).
129
+
130
+
113
131
  ## Перевірка
114
132
 
115
133
  `npx @nitra/cursor check js-run`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.155",
3
+ "version": "1.8.156",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -12,10 +12,12 @@
12
12
  * дозволені лише у каталозі conn (за замовчуванням `src/conn/`; за наявності
13
13
  * `package.json#imports['#conn/*']` — у його цільовому каталозі); поза ним — порушення
14
14
  * (див. `utils/conn-imports-scan.mjs`);
15
- * - «CheckEnv»: кожне `process.env.X` (включно з `process.env['X']` і деструктуризацією
16
- * `const { X } = process.env`) має бути закрите літеральним викликом `checkEnv(['X', ...])`
17
- * у тому ж файлі або коментарем `// @nitra/cursor ignore-next-line checkEnv` на попередньому
18
- * рядку (див. `utils/check-env-scan.mjs`).
15
+ * - «process.env / CheckEnv»: пряме `process.env.X` має бути замінено на `env`
16
+ * з `@nitra/check-env` (для обов'язкових змінних, із `checkEnv([...])`) або з
17
+ * `node:process` (для опційних). Коли `env` імпортовано з `@nitra/check-env`,
18
+ * кожен `env.X` має бути закритий літеральним викликом `checkEnv(['X', ...])`
19
+ * у тому ж файлі або коментарем `// @nitra/cursor ignore-next-line checkEnv`
20
+ * на попередньому рядку (див. `utils/check-env-scan.mjs`).
19
21
  */
20
22
  import { existsSync } from 'node:fs'
21
23
  import { readFile } from 'node:fs/promises'
@@ -134,9 +136,11 @@ async function checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail) {
134
136
  const content = await readFile(absPath, 'utf8')
135
137
  for (const v of findUncheckedProcessEnvInText(content, rel)) {
136
138
  violations++
137
- fail(
138
- `${label}${rel}:${v.line} process.env.${v.name} без checkEnv(['${v.name}']) (або '// @nitra/cursor ignore-next-line checkEnv' попереду)`
139
- )
139
+ const message =
140
+ v.kind === 'process-env'
141
+ ? `${label}${rel}:${v.line} — process.env.${v.name}: заміни на env з '@nitra/check-env' (обов'язкова змінна + checkEnv(['${v.name}'])) або з 'node:process' (опційна)`
142
+ : `${label}${rel}:${v.line} — env.${v.name} (з '@nitra/check-env') без checkEnv(['${v.name}']) (або '// @nitra/cursor ignore-next-line checkEnv' попереду)`
143
+ fail(message)
140
144
  }
141
145
  }
142
146
  return violations
@@ -184,7 +188,9 @@ async function checkWorkspacePackage(rootDir, fail, passFn) {
184
188
 
185
189
  const envViolations = await checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail)
186
190
  if (envViolations === 0) {
187
- passFn(`${label}усі process.env.* закриті checkEnv(['…']) або '// @nitra/cursor ignore-next-line checkEnv'`)
191
+ passFn(
192
+ `${label}немає прямого process.env.*; усі env.* з '@nitra/check-env' закриті checkEnv(['…']) (або '// @nitra/cursor ignore-next-line checkEnv')`
193
+ )
188
194
  }
189
195
 
190
196
  const configmapPath = join(rootDir, 'k8s', 'base', 'configmap.yaml')
@@ -1,23 +1,32 @@
1
1
  /**
2
- * AST-сканер для правила CheckEnv (js-run.mdc).
2
+ * AST-сканер правила «process.env / CheckEnv» (js-run.mdc).
3
3
  *
4
- * Кожне використання `process.env.X` у JS/TS-коді має бути «закрите» одним з двох способів:
5
- * - перед використанням у тому ж файлі викликано `checkEnv(['X', ...])` з пакету `@nitra/check-env`;
6
- * - на рядку безпосередньо перед `process.env.X` стоїть коментар-маркер
7
- * `// @nitra/cursor ignore-next-line checkEnv` (роздільники пробілів довільні; саме слово
8
- * `checkEnv` чутливе до регістру, як в усіх прикладах документа).
4
+ * Правило в .mdc формулює два контракти:
5
+ * 1. Прямий доступ до `process.env.X` має бути замінено на `env` з пакета
6
+ * `@nitra/check-env` (для обов'язкових змінних, із викликом `checkEnv([...])`)
7
+ * або з `node:process` (для опційних). Тому будь-яке `process.env.X` сканер
8
+ * завжди реєструє як порушення з порадою про конкретну заміну.
9
+ * 2. Якщо у файл імпортовано `env` саме з `@nitra/check-env`, то кожне `env.X`
10
+ * має бути закрите літеральним викликом `checkEnv(['X', ...])` у тому ж файлі
11
+ * (порядок не важливий, кілька викликів зливаються в один список).
9
12
  *
10
- * Семантика береться з **oxc-parser** через `parseProgramOrNull`: regex по тілу файлу не
11
- * використовується, лише сирий текст рядка з коментарем перевіряється на маркер. Якщо
12
- * файл не парситься повертаємо порожній результат, спочатку треба полагодити синтаксис.
13
+ * Обидва контракти можна точково «приглушити» коментарем-маркером
14
+ * `// @nitra/cursor ignore-next-line checkEnv` на рядку безпосередньо перед
15
+ * порушеннямце залишається сумісним escape-hatch для legacy-коду.
13
16
  *
14
- * Покриті форми доступу до `process.env`:
15
- * - `process.env.X` (звичайний MemberExpression);
16
- * - `process.env['X']` (computed з рядковим літералом);
17
- * - `const { X, Y } = process.env` (ObjectPattern; ім'я з ключа);
18
- * - `const { X: alias } = process.env` (ім'я з ключа, не з alias).
17
+ * Семантика береться з **oxc-parser** через `parseProgramOrNull`: regex по тілу
18
+ * файлу не використовується, лише сирий текст рядка з коментарем перевіряється
19
+ * на маркер. Якщо файл не парситься — повертаємо порожній результат, спочатку
20
+ * треба полагодити синтаксис.
19
21
  *
20
- * Якщо ключ обчислюваний (наприклад, `process.env[varName]`) — пропускаємо без помилки,
22
+ * Покриті форми доступу:
23
+ * - `process.env.X` / `process.env['X']` (як MemberExpression);
24
+ * - `const { X, Y } = process.env` (ObjectPattern; ім'я з ключа, не з alias);
25
+ * - аналогічно для `env.X` / `env['X']` / `const { X } = env`,
26
+ * де `env` має бути імпортований з `@nitra/check-env` (інакше ігноруємо —
27
+ * це може бути локальна змінна чи `env` з `node:process`).
28
+ *
29
+ * Якщо ключ обчислюваний (`process.env[varName]`) — пропускаємо без помилки,
21
30
  * бо за статичним AST неможливо встановити, яка саме змінна оточення використовується.
22
31
  */
23
32
  import { offsetToLine, parseProgramOrNull, walkAstWithAncestors } from './ast-scan-utils.mjs'
@@ -25,6 +34,8 @@ import { offsetToLine, parseProgramOrNull, walkAstWithAncestors } from './ast-sc
25
34
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
26
35
  const IGNORE_DIRECTIVE_RE = /\/\/\s*@nitra\/cursor\s+ignore-next-line\s+checkEnv\b/u
27
36
 
37
+ const CHECK_ENV_PACKAGE = '@nitra/check-env'
38
+
28
39
  /**
29
40
  * Чи є цей вузол виразом `process.env`.
30
41
  * @param {unknown} node AST вузол
@@ -46,8 +57,8 @@ function isProcessEnvAccess(node) {
46
57
  }
47
58
 
48
59
  /**
49
- * Витягує ім'я ENV з MemberExpression-вузла `process.env.X` або `process.env['X']`.
50
- * @param {Record<string, unknown>} node MemberExpression, чий object — `process.env`
60
+ * Витягує ім'я ENV з MemberExpression `obj.X` або `obj['X']`.
61
+ * @param {Record<string, unknown>} node MemberExpression, чий object — `process.env` або `env`
51
62
  * @returns {string | null} ім'я змінної оточення або null, якщо ключ не статичний
52
63
  */
53
64
  function envNameFromMember(node) {
@@ -87,10 +98,40 @@ function collectCheckedEnvNames(programNode) {
87
98
  return out
88
99
  }
89
100
 
101
+ /**
102
+ * Чи імпортовано локальний ідентифікатор `env` саме з `@nitra/check-env`.
103
+ * Перевіряє ImportDeclaration на specifier {imported.name === 'env', local.name === 'env'}.
104
+ * Aliased-варіанти (`{ env as x }`) свідомо не підтримуються — у наших правилах
105
+ * приклади завжди використовують канонічне ім'я `env`.
106
+ * @param {unknown} programNode корінь AST
107
+ * @returns {boolean} true, якщо у файлі є `import { env } from '@nitra/check-env'`
108
+ */
109
+ function hasCheckEnvImport(programNode) {
110
+ let found = false
111
+ walkAstWithAncestors(programNode, [], node => {
112
+ if (found) return
113
+ if (node.type !== 'ImportDeclaration') return
114
+ const source = node.source
115
+ if (!source || typeof source !== 'object' || source.value !== CHECK_ENV_PACKAGE) return
116
+ const specifiers = node.specifiers
117
+ if (!Array.isArray(specifiers)) return
118
+ for (const s of specifiers) {
119
+ if (!s || typeof s !== 'object' || s.type !== 'ImportSpecifier') continue
120
+ const imported = s.imported
121
+ const local = s.local
122
+ if (!imported || imported.name !== 'env') continue
123
+ if (!local || local.name !== 'env') continue
124
+ found = true
125
+ return
126
+ }
127
+ })
128
+ return found
129
+ }
130
+
90
131
  /**
91
132
  * Чи закритий рядок ignore-коментарем `// @nitra/cursor ignore-next-line checkEnv`.
92
133
  * @param {string[]} lines рядки файлу (split за \n, без CR)
93
- * @param {number} oneBasedLine 1-based номер рядка з `process.env.X`
134
+ * @param {number} oneBasedLine 1-based номер рядка з порушенням
94
135
  * @returns {boolean} true, якщо попередній рядок містить маркер
95
136
  */
96
137
  function hasIgnoreDirective(lines, oneBasedLine) {
@@ -100,48 +141,64 @@ function hasIgnoreDirective(lines, oneBasedLine) {
100
141
  }
101
142
 
102
143
  /**
103
- * Знаходить всі доступи до `process.env.<NAME>`, які не покриті ні літеральним
104
- * `checkEnv([...])` у тому ж файлі, ні коментарем-маркером безпосередньо перед.
105
- *
106
- * @param {string} content вихідний код
107
- * @param {string} [virtualPath] шлях для вибору `lang` парсера
108
- * @returns {{ line: number, name: string }[]} список порушень
144
+ * Чи є вузол MemberExpression виду `env.X` / `env['X']`, де `env` Identifier
145
+ * (в AST oxc-parser globals і локальні імпорти не розрізняються — фільтр джерела
146
+ * робиться на рівні `hasCheckEnvImport`).
147
+ * @param {unknown} node AST вузол
148
+ * @returns {boolean} true, якщо це `env.<...>`
109
149
  */
110
- export function findUncheckedProcessEnvInText(content, virtualPath = 'scan.ts') {
111
- const program = parseProgramOrNull(content, virtualPath)
112
- if (!program) return []
150
+ function isEnvIdentifierMember(node) {
151
+ if (!node || typeof node !== 'object' || node.type !== 'MemberExpression') return false
152
+ const obj = node.object
153
+ return !!obj && obj.type === 'Identifier' && obj.name === 'env'
154
+ }
113
155
 
114
- const checked = collectCheckedEnvNames(program)
115
- const lines = content.split('\n').map(s => (s.endsWith('\r') ? s.slice(0, -1) : s))
156
+ /**
157
+ * @typedef {{
158
+ * line: number,
159
+ * name: string,
160
+ * kind: 'process-env' | 'check-env-missing-checkEnv'
161
+ * }} EnvViolation
162
+ */
116
163
 
117
- /** @type {{ line: number, name: string }[]} */
164
+ /**
165
+ * Перебирає AST і для кожного знайденого доступу до `process.env` чи `env`
166
+ * (де `env` — імпорт з `@nitra/check-env`) реєструє порушення відповідного типу.
167
+ * @param {unknown} program корінь AST
168
+ * @param {string} content вихідний код (для offset → line)
169
+ * @param {string[]} lines split-рядки content (для ignore-маркера)
170
+ * @param {Set<string>} checkedNames імена, закриті літеральним `checkEnv([...])`
171
+ * @param {boolean} envFromCheckEnv чи імпортовано `env` саме з `@nitra/check-env`
172
+ * @returns {EnvViolation[]} список порушень (відсортований за порядком зустрічі в AST)
173
+ */
174
+ function collectViolations(program, content, lines, checkedNames, envFromCheckEnv) {
175
+ /** @type {EnvViolation[]} */
118
176
  const out = []
119
177
  /** @type {Set<string>} */
120
178
  const reported = new Set()
121
179
 
122
180
  /**
123
- * Реєструє порушення з дедуплікацією за «name@line».
181
+ * Реєструє порушення з дедуплікацією за «kind|name|line» і урахуванням ignore-маркера.
182
+ * @param {'process-env' | 'check-env-missing-checkEnv'} kind тип порушення
124
183
  * @param {string} name ім'я ENV
125
184
  * @param {number} line 1-based рядок
126
185
  */
127
- function report(name, line) {
128
- if (checked.has(name)) return
186
+ function report(kind, name, line) {
129
187
  if (hasIgnoreDirective(lines, line)) return
130
- const key = `${name}@${line}`
188
+ const key = `${kind}|${name}|${line}`
131
189
  if (reported.has(key)) return
132
190
  reported.add(key)
133
- out.push({ name, line })
191
+ out.push({ kind, name, line })
134
192
  }
135
193
 
136
194
  walkAstWithAncestors(program, [], (node, ancestors) => {
195
+ // 1. process.env.X — завжди порушення (рекомендуємо замінити на env)
137
196
  if (isProcessEnvAccess(node)) {
138
197
  const parent = ancestors[ancestors.length - 1]
139
- // process.env.X / process.env['X']
140
198
  if (parent && typeof parent === 'object' && parent.type === 'MemberExpression' && parent.object === node) {
141
199
  const envName = envNameFromMember(parent)
142
- if (envName) report(envName, offsetToLine(content, parent.start))
200
+ if (envName) report('process-env', envName, offsetToLine(content, parent.start))
143
201
  }
144
- // const { X, Y } = process.env → беремо імена з ObjectPattern
145
202
  if (
146
203
  parent &&
147
204
  typeof parent === 'object' &&
@@ -152,15 +209,38 @@ export function findUncheckedProcessEnvInText(content, virtualPath = 'scan.ts')
152
209
  Array.isArray(parent.id.properties)
153
210
  ) {
154
211
  for (const p of parent.id.properties) {
155
- if (!p || typeof p !== 'object' || p.type !== 'Property') continue
156
- if (p.computed) continue
157
- const key = p.key
158
- if (!key || typeof key !== 'object') continue
159
- /** @type {string | null} */
160
- let name = null
161
- if (key.type === 'Identifier' && typeof key.name === 'string') name = key.name
162
- else if (key.type === 'Literal' && typeof key.value === 'string') name = key.value
163
- if (name) report(name, offsetToLine(content, p.start ?? parent.start))
212
+ const name = staticPropertyName(p)
213
+ if (name) report('process-env', name, offsetToLine(content, p.start ?? parent.start))
214
+ }
215
+ }
216
+ return
217
+ }
218
+
219
+ // 2. env.X порушення лише якщо env імпортовано з @nitra/check-env і немає checkEnv
220
+ if (!envFromCheckEnv) return
221
+
222
+ if (isEnvIdentifierMember(node)) {
223
+ const envName = envNameFromMember(node)
224
+ if (envName && !checkedNames.has(envName)) {
225
+ report('check-env-missing-checkEnv', envName, offsetToLine(content, node.start))
226
+ }
227
+ return
228
+ }
229
+
230
+ // const { X, Y } = env — теж потребує checkEnv для кожного імені
231
+ if (
232
+ node.type === 'VariableDeclarator' &&
233
+ node.init &&
234
+ node.init.type === 'Identifier' &&
235
+ node.init.name === 'env' &&
236
+ node.id &&
237
+ node.id.type === 'ObjectPattern' &&
238
+ Array.isArray(node.id.properties)
239
+ ) {
240
+ for (const p of node.id.properties) {
241
+ const name = staticPropertyName(p)
242
+ if (name && !checkedNames.has(name)) {
243
+ report('check-env-missing-checkEnv', name, offsetToLine(content, p.start ?? node.start))
164
244
  }
165
245
  }
166
246
  }
@@ -169,6 +249,39 @@ export function findUncheckedProcessEnvInText(content, virtualPath = 'scan.ts')
169
249
  return out
170
250
  }
171
251
 
252
+ /**
253
+ * Витягує статичне ім'я з вузла Property у ObjectPattern.
254
+ * @param {unknown} property AST-вузол ObjectPattern.properties[i]
255
+ * @returns {string | null} ім'я ключа або null
256
+ */
257
+ function staticPropertyName(property) {
258
+ if (!property || typeof property !== 'object' || property.type !== 'Property') return null
259
+ if (property.computed) return null
260
+ const key = property.key
261
+ if (!key || typeof key !== 'object') return null
262
+ if (key.type === 'Identifier' && typeof key.name === 'string') return key.name
263
+ if (key.type === 'Literal' && typeof key.value === 'string') return key.value
264
+ return null
265
+ }
266
+
267
+ /**
268
+ * Знаходить порушення правила «process.env / CheckEnv» у файлі.
269
+ *
270
+ * @param {string} content вихідний код
271
+ * @param {string} [virtualPath] шлях для вибору `lang` парсера
272
+ * @returns {EnvViolation[]} список порушень із типом, іменем змінної та рядком
273
+ */
274
+ export function findUncheckedProcessEnvInText(content, virtualPath = 'scan.ts') {
275
+ const program = parseProgramOrNull(content, virtualPath)
276
+ if (!program) return []
277
+
278
+ const checked = collectCheckedEnvNames(program)
279
+ const envFromCheckEnv = hasCheckEnvImport(program)
280
+ const lines = content.split('\n').map(s => (s.endsWith('\r') ? s.slice(0, -1) : s))
281
+
282
+ return collectViolations(program, content, lines, checked, envFromCheckEnv)
283
+ }
284
+
172
285
  /**
173
286
  * Чи сканувати цей файл за розширенням (JS/TS-сім'я, без `.d.ts`).
174
287
  * @param {string} relativePathPosix відносний шлях (posix)