@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 +24 -6
- package/package.json +1 -1
- package/scripts/check-js-run.mjs +14 -8
- package/scripts/utils/check-env-scan.mjs +160 -47
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(
|
|
76
|
+
export const graphQLClientSmart = new GraphQLClient(env.QL, {
|
|
77
77
|
headers: {
|
|
78
|
-
'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
package/scripts/check-js-run.mjs
CHANGED
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
* дозволені лише у каталозі conn (за замовчуванням `src/conn/`; за наявності
|
|
13
13
|
* `package.json#imports['#conn/*']` — у його цільовому каталозі); поза ним — порушення
|
|
14
14
|
* (див. `utils/conn-imports-scan.mjs`);
|
|
15
|
-
* - «CheckEnv»:
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
|
|
138
|
-
|
|
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(
|
|
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-сканер
|
|
2
|
+
* AST-сканер правила «process.env / CheckEnv» (js-run.mdc).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
+
* Обидва контракти можна точково «приглушити» коментарем-маркером
|
|
14
|
+
* `// @nitra/cursor ignore-next-line checkEnv` на рядку безпосередньо перед
|
|
15
|
+
* порушенням — це залишається сумісним escape-hatch для legacy-коду.
|
|
13
16
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* - `const { X: alias } = process.env` (ім'я з ключа, не з alias).
|
|
17
|
+
* Семантика береться з **oxc-parser** через `parseProgramOrNull`: regex по тілу
|
|
18
|
+
* файлу не використовується, лише сирий текст рядка з коментарем перевіряється
|
|
19
|
+
* на маркер. Якщо файл не парситься — повертаємо порожній результат, спочатку
|
|
20
|
+
* треба полагодити синтаксис.
|
|
19
21
|
*
|
|
20
|
-
*
|
|
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
|
|
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 номер рядка з
|
|
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
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* @param {
|
|
107
|
-
* @
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
156
|
+
/**
|
|
157
|
+
* @typedef {{
|
|
158
|
+
* line: number,
|
|
159
|
+
* name: string,
|
|
160
|
+
* kind: 'process-env' | 'check-env-missing-checkEnv'
|
|
161
|
+
* }} EnvViolation
|
|
162
|
+
*/
|
|
116
163
|
|
|
117
|
-
|
|
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
|
|
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}
|
|
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
|
-
|
|
156
|
-
if (p.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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)
|