Instruction file imported from nitra/telegram (
.cursor/rules/n-js-run.mdc). Copyright stays with the author.
Правило охоплює backend Node.js workspace-пакети (jobs, GraphQL/HTTP-сервери, CLI) — визначення меж застосування, вимоги до runtime, структуру проекту, конфігурацію, логування, підключення до БД/GraphQL і безпечне використання env-змінних.
Швидкий gate через conftest
Rego-пакети, які запускає npx @nitra/cursor fix js-run / npx @nitra/cursor check:
jsconfig.json:
{
"compilerOptions": {
"lib": ["esnext"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "esnext",
"checkJs": false
},
"include": ["src/**/*"]
}
configmap.yaml:
data:
OTEL_RESOURCE_ATTRIBUTES:
- 'service.name='
- 'service.namespace='
package.json:
{
"dependencies": {
"bunyan": "використовуй стандартні логери (js-run.mdc)",
"@nitra/bunyan": "використовуй стандартні логери (js-run.mdc)"
},
"devDependencies": {
"bunyan": "використовуй стандартні логери (js-run.mdc)",
"@nitra/bunyan": "використовуй стандартні логери (js-run.mdc)"
},
"scriptsForbidden": [
{
"id": "node-runner",
"pattern": "\\bnode(\\s|$)",
"message": "заміни `node` на `bun` у scripts — один runtime у dev і prod (js-run.mdc)"
},
{
"id": "env-cat-bun",
"pattern": "\\benv\\s+\\$\\(cat\\s+[^)]+\\)\\s+bun\\b",
"message": "заміни `env $(cat A B) bun` на `bun --env-file=A --env-file=B` (нативний Bun, js-run.mdc)"
}
]
}
CheckEnv та заборона прямого process.env
CheckEnv
Усі змінні оточення, які використовуються в коді, повинні бути перевірені за допомогою checkEnv з пакету @nitra/check-env. Це гарантує, що всі необхідні змінні оточення встановлені перед запуском програми.
import { checkEnv, env } from '@nitra/check-env'
import { SQL } from 'bun'
checkEnv(['PG_CONN'])
export const db = new SQL({ url: env.PG_CONN })
process.env
Прямий доступ до process.env.X у коді заборонений — його треба замінити на env:
Стосується лише backend-пакетів (див. Область застосування). У frontend-пакетах (
viteуdevDependencies) — не змінюйprocess.env.*і не додавай імпортnode:process.
- обов'язкова змінна —
import { checkEnv, env } from '@nitra/check-env'плюсcheckEnv(['X'])у тому ж файлі (приклад див. вище в розділі CheckEnv); - опційна змінна —
import { env } from 'node:process':
import { env } from 'node:process'
console.log(env.OPTIONAL_ENV_VAR)
Тимчасово приглушити перевірку для конкретного рядка можна коментарем
// @nitra/cursor ignore-next-line checkEnv безпосередньо перед використанням
(escape-hatch для legacy-коду, не для нових файлів).
Внутрішні аліаси для підключень до БД і GraphQL
Якщо в проекті є підключення до баз даних, зовнішніх graphql на кшталт:
import { SQL } from 'bun'
// або
import sql from 'mssql'
// або
import { GraphQLClient } from '@nitra/graphql-request'
то ці підключення повинні бути винесені в окремий файл, наприклад /src/conn/pg.mjs, в package.json повинні бути додано аліас:
{
"imports": {
"#conn/*": "./src/conn/*"
},
}
так виглядатиме підключення до PostgreSQL в коді:
import { checkEnv, env } from '@nitra/check-env'
import { SQL } from 'bun'
checkEnv(['PG_CONN'])
export const db = new SQL({ url: env.PG_CONN })
а так до GraphQL:
import { checkEnv, env } from '@nitra/check-env'
import { GraphQLClient } from '@nitra/graphql-request'
checkEnv(['QL', 'X_HASURA_ADMIN_SECRET'])
export { gql } from '@nitra/graphql-request'
export const graphQLClientSmart = new GraphQLClient(env.QL, {
headers: {
'X-Hasura-Admin-Secret': env.X_HASURA_ADMIN_SECRET
}
})
а в коді повинно бути використано:
import { pool } from '#conn/pg.mjs'
// або
import { gql, graphQLClient } from '@nitra/graphql-request'
Нейминг файлів у src/conn/
Назва файла в src/conn/ має одразу повідомляти, до чого підключаємось і в якому режимі:
- GraphQL — префікс
ql-, далі ідентифікатор endpoint:src/conn/ql-contract.mjssrc/conn/ql-smart.mjs
- PostgreSQL — префікс
pg-, далі тип підключення (репліка vs мастер):readабоwrite:src/conn/pg-read.mjssrc/conn/pg-write.mjs
- PostgreSQL до кількох БД — додатково ідентифікатор підключення після типу:
src/conn/pg-read-smart.mjssrc/conn/pg-write-contract.mjs
- MySQL — префікс
mysql-за тією ж схемою (mysql-read.mjs,mysql-write-<id>.mjsтощо). - MSSQL — префікс
mssql-за тією ж схемою (mssql-read.mjs,mssql-write-<id>.mjsтощо). Хоча npm-пакет один (mssql), а драйвер MS SQL Server під капотом T-SQL — у файловій назві відрізняємо MS SQL Server від MySQL, бо це різні СУБД, різні діалекти, різні рантаймні залежності. Якщо проєкт історично використовуєmysql-…для MSSQL-підключень — він валідний і далі (для backward-compat), але новий код пишемо з префіксомmssql-.
Підключення до БД обов'язково має бути ідентифіковано як read (репліка) або write (мастер). Якщо з імені змінної оточення (наприклад, env.PG_CONN) це не очевидно — визнач режим за операціями в коді: якщо немає операцій зміни даних (INSERT/UPDATE/DELETE/DDL) — це pg-read.mjs, інакше pg-write.mjs.
Експорти у файлах src/conn/
У файлах підключень заборонений export default. Експорт має бути іменований і збігатися з назвою файла в camelCase.
Приклад — src/conn/ql-smart.mjs:
export default new GraphQLClient(env.SMART_QL, {
headers: {
'X-Hasura-Admin-Secret': env.SMART_X_HASURA_ADMIN_SECRET
}
})
export const qlSmart = new GraphQLClient(env.SMART_QL, {
headers: {
'X-Hasura-Admin-Secret': env.SMART_X_HASURA_ADMIN_SECRET
}
})
Відповідно: pg-read.mjs → export const pgRead = …, pg-write-contract.mjs → export const pgWriteContract = …, ql-contract.mjs → export const qlContract = ….
Файли index.* у conn-каталозі пропускаються як можливий reexport-барель.
jsconfig.json (редактор / перевірка типів)
Якщо в backend workspace-пакеті (без vite у devDependencies) є каталог src/, у корені цього пакета має бути jsconfig.json. Якщо файлу ще немає — створи його з таким вмістом (канон js-run):
{
"compilerOptions": {
"lib": ["esnext"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "esnext",
"checkJs": false
},
"include": ["src/**/*"]
}
Канон: jsconfig.json.snippet.json
Якщо пакет не слідує структурі з src/ (наприклад, лише scripts/ у корені) — ця вимога не застосовується; для типових сервісів із src/ файл обов'язковий і має збігатися з каноном.
OTEL ConfigMap (k8s)
В /k8s/base/configmap.yaml повинен бути заданий OTEL_RESOURCE_ATTRIBUTES: 'service.name=<project_name>,service.namespace=<project_namespace>'
а в директоріях з kustomize повинні бути перевизначені значення OTEL_RESOURCE_ATTRIBUTES і в них service.namespace повинен відповідати namespace, в якому знаходиться дана директорія.
Канон обовʼязкових substring у data.OTEL_RESOURCE_ATTRIBUTES (service.name=, service.namespace=): configmap.yaml.contains.yml
Використання @nitra/pino для логування
Проект використовує @nitra/pino для логування.
Якщо в проекті присутній @nitra/bunyan, то він повинен бути замінений на @nitra/pino — як у package.json, так і в коді: усі import / require / динамічні import() з @nitra/bunyan (і застарілого bunyan) треба замінити на @nitra/pino і за потреби адаптувати виклики під його API.
Канон заборонених dependencies / devDependencies (bunyan, @nitra/bunyan): package.json.deny.json
Структура проекту
Рекомендується використовувати таку структуру проекту:
k8s/ # тут всі файли для деплойменту в Kubernetes, включаючи kustomize
src/ # тут всі файли необхідні для роботи проекту
Dockerfile
package.json
readme.md
Runtime у package.json#scripts
У backend-пакетах (без vite у devDependencies) код запускають через Bun, не через бінарник node у значеннях scripts:
"start": "node src/index.js"→"start": "bun src/index.js"(абоbun run …, якщо так прийнято в репо);node --watch app.js→bun --watch app.js;NODE_OPTIONS=… node app.js→NODE_OPTIONS=… bun app.js.env $(cat .env .env.local) bun src/index.js→bun --env-file=.env --env-file=.env.local src/index.js(нативне завантаження env у Bun, безenv/cat).
Заборонено викликати node у ланцюжках (&&, ;, |). Заборонено обгортку env $(cat …) bun — файли з cat перелічуй у --env-file= (по одному прапорцю на файл, порядок як у cat). Допустимо: bun, bunx, npx (див. bun.mdc), інші CLI, якщо вони не підміняють рантайм на node.
Це не стосується поля engines.node (мінімальна версія Node для сумісності інструментів) і не стосується frontend-пакетів з vite у devDependencies.
Канон заборонених патернів у scripts: package.json.deny.json (scriptsForbidden).
Область застосування
Правило стосується виключно backend Node.js workspace-пакетів (jobs, GraphQL/HTTP-сервери, CLI). Не застосовується до frontend-пакетів, які бандляться в браузер: маркер — наявність vite у devDependencies пакета (site/, мобільні Capacitor-пакети, будь-яка Vue/Quasar SPA).
У браузерному середовищі:
- немає
node:process— імпортimport { env } from 'node:process'resolve'иться уundefined, іenv.Xпадає зTypeError: Cannot read properties of undefined; process.env.Xу джерелах пакета відсутнє в рантаймі — Vite або взагалі не підставляє його, або підставляє лишеprocess.env.NODE_ENV;- усі змінні оточення для frontend задаються через
VITE_*і доступні якimport.meta.env.VITE_X(типобезпечно черезvite-check-env); режим —import.meta.env.MODE/import.meta.env.PROD.
Тому у frontend-пакетах не торкайся process.env.* і не додавай import { env } from 'node:process'. Якщо натрапив на process.env.NODE_ENV у frontend-коді — заміна, якщо взагалі потрібна, лише на import.meta.env.MODE.
Паузи через setTimeout
Заборонено робити паузи через await new Promise(resolve => setTimeout(resolve, ms)) — таку обгортку треба замінити на promise-варіант setTimeout з node:timers/promises:
import { setTimeout } from 'node:timers/promises'
await setTimeout(500)
Імпорт setTimeout з node:timers/promises затіняє глобальний таймер у файлі — якщо в тому ж файлі потрібен callback-варіант, імпортуй його під іншим іменем (наприклад, import { setTimeout as setTimeoutCb } from 'node:timers').
Temporal API (заборона у Bun runtime)
У backend/Bun runtime-коді не використовуй Temporal (Temporal.Now, Temporal.Instant, імпорти з polyfill тощо). Поточний Bun runtime ще не має глобального Temporal (typeof Temporal === "undefined"), тому агентам треба лишатися на сумісному Date API або передавати timestamp у чисті функції через параметр.
Перевірка npx @nitra/cursor fix js-run сканує JS/TS AST і падає на identifier Temporal у backend workspace-коді.
Rego-gate: OTEL ConfigMap (k8s)
Rego-пакет: js-run.configmap
Цільові файли: k8s/base/configmap.yaml (і будь-які ConfigMap у kustomize-оверлеях)
Умова спрацювання: input.kind == "ConfigMap"
Перевіряє, що поле data.OTEL_RESOURCE_ATTRIBUTES містить усі обовʼязкові substring-маркери, визначені у template:
service.name=service.namespace=
Канон маркерів: configmap.yaml.contains.yml
✓ Правильно
data:
OTEL_RESOURCE_ATTRIBUTES: 'service.name=my-svc,service.namespace=prod'
✗ Неправильно
data:
OTEL_RESOURCE_ATTRIBUTES: 'service.name=my-svc'
# Відсутній service.namespace= → deny
Ресурси типу kind: Deployment та інші non-ConfigMap — ігноруються.
Rego-gate: jsconfig.json
Rego-пакет: js-run.jsconfig
Цільові файли: jsconfig.json у корені backend workspace-пакету
Порівнює jsconfig.json з каноном через walker по всіх листах template:
compilerOptions.*— значення мають збігатись точноinclude— масив порівнюється як множина (точний склад)
Канон: jsconfig.json.snippet.json
Перевірені поля:
| Поле | Очікуване значення |
|---|---|
compilerOptions.module |
"NodeNext" |
compilerOptions.moduleResolution |
"NodeNext" |
compilerOptions.target |
"esnext" |
compilerOptions.lib |
["esnext"] |
compilerOptions.checkJs |
false |
include |
["src/**/*"] |
Якщо розділ compilerOptions відсутній або не є обʼєктом — deny.
Rego-gate: package.json (залежності та scripts)
Rego-пакет: js-run.package_json
Цільові файли: package.json у backend workspace-пакетах (без vite у devDependencies)
Перевіряє три класи порушень за deny-списком із template:
Канон deny-списку: package.json.deny.json
1. Заборонені залежності (dependencies / devDependencies)
| Пакет | Причина |
|---|---|
bunyan |
використовуй стандартні логери |
@nitra/bunyan |
використовуй стандартні логери |
2. Заборонений рантайм у scripts (лише backend-пакети без vite)
- Патерн
\bnode(\s|$)— заміниnodeнаbun - Патерн
\benv\s+\$\(cat\s+[^)]+\)\s+bun\b— заміниenv $(cat A B) bunнаbun --env-file=A --env-file=B
✓ Правильно
{ "scripts": { "start": "bun src/index.js" } }
{ "scripts": { "start": "bun --env-file=.env src/index.js" } }
✗ Неправильно
{ "scripts": { "start": "node src/index.js" } }
{ "scripts": { "start": "env $(cat .env .env.local) bun src/index.js" } }
{ "dependencies": { "bunyan": "^1.0.0" } }
Пакети з vite у devDependencies — frontend, поза областю js-run, перевірка scripts не застосовується.