@nitra/cursor 12.8.6 → 12.8.7
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/rules/adr/js/hooks.mdc +32 -0
- package/rules/adr/js/madr_format.mdc +96 -0
- package/rules/adr/js/settings_policy.mdc +34 -0
- package/rules/adr/main.mdc +13 -95
- package/rules/bun/js/bunfig.mdc +12 -0
- package/rules/bun/js/layout.mdc +60 -0
- package/rules/bun/js/lint.mdc +9 -0
- package/rules/bun/js/package_json.mdc +19 -0
- package/rules/bun/main.mdc +9 -61
- package/rules/capacitor/js/ios_spm.mdc +69 -0
- package/rules/capacitor/js/version.mdc +29 -0
- package/rules/capacitor/main.mdc +8 -22
- package/rules/changelog/js/agent-workflow.mdc +15 -0
- package/rules/changelog/js/changelog-format.mdc +33 -0
- package/rules/changelog/js/comparison-models.mdc +40 -0
- package/rules/changelog/main.mdc +4 -98
- package/rules/ci4/js/marksman_config.mdc +31 -0
- package/rules/ci4/js/vscode_extensions.mdc +33 -0
- package/rules/ci4/main.mdc +14 -14
- package/rules/docker/js/compile.mdc +44 -0
- package/rules/docker/js/hadolint.mdc +50 -0
- package/rules/docker/js/mirror.mdc +13 -0
- package/rules/docker/js/multistage.mdc +13 -0
- package/rules/docker/js/native-addon.mdc +43 -0
- package/rules/docker/js/nginx-tag.mdc +7 -0
- package/rules/docker/js/nginx-user.mdc +37 -0
- package/rules/docker/js/non-root.mdc +39 -0
- package/rules/docker/main.mdc +15 -196
- package/rules/ga/js/lint_toolchain.mdc +15 -0
- package/rules/ga/js/required_workflows.mdc +35 -0
- package/rules/ga/js/vscode.mdc +17 -0
- package/rules/ga/js/workflow_common.mdc +108 -0
- package/rules/ga/js/workflows.mdc +32 -0
- package/rules/ga/js/zizmor.mdc +7 -0
- package/rules/ga/main.mdc +17 -125
- package/rules/graphql/js/tooling.mdc +13 -0
- package/rules/graphql/js/vscode_extensions.mdc +13 -0
- package/rules/graphql/main.mdc +3 -22
- package/rules/hasura/js/internal_urls.mdc +27 -0
- package/rules/hasura/js/migrations.mdc +13 -0
- package/rules/hasura/js/svc_hl.mdc +17 -0
- package/rules/hasura/main.mdc +8 -30
- package/rules/image-avif/js/avif_generation.mdc +26 -0
- package/rules/image-avif/js/package_json_optout.mdc +21 -0
- package/rules/image-avif/main.mdc +7 -34
- package/rules/image-compress/js/package_json.mdc +7 -0
- package/rules/image-compress/js/package_setup.mdc +13 -0
- package/rules/image-compress/main.mdc +4 -12
- package/rules/js/docs/index.md +3 -3
- package/rules/js/js/dep-policy.mdc +17 -0
- package/rules/js/js/eslint-config.mdc +28 -0
- package/rules/js/js/extensions.mdc +8 -0
- package/rules/js/js/file-extensions.mdc +12 -0
- package/rules/js/js/for-in.mdc +26 -0
- package/rules/js/js/jscpd.mdc +42 -0
- package/rules/js/js/knip.mdc +15 -0
- package/rules/js/js/lint-js-workflow.mdc +58 -0
- package/rules/js/js/oxlintrc.mdc +20 -0
- package/rules/js/js/package-json.mdc +31 -0
- package/rules/js/js/tests.mdc +9 -0
- package/rules/js/js/utils-lib-structure.mdc +15 -0
- package/rules/js/main.mdc +21 -214
- package/rules/js-bun-db/js/bun-sql-migration.mdc +15 -0
- package/rules/js-bun-db/js/connection.mdc +42 -0
- package/rules/js-bun-db/js/pg-format-identifiers.mdc +102 -0
- package/rules/js-bun-db/js/pg-format-shim.mdc +99 -0
- package/rules/js-bun-db/js/pg-leftover.mdc +27 -0
- package/rules/js-bun-db/js/pg-listen-notify.mdc +51 -0
- package/rules/js-bun-db/js/query-safety.mdc +117 -0
- package/rules/js-bun-db/js/sql-array.mdc +88 -0
- package/rules/js-bun-db/js/unsafe.mdc +65 -0
- package/rules/js-bun-db/main.mdc +15 -605
- package/rules/js-bun-redis/js/imports.mdc +47 -0
- package/rules/js-bun-redis/js/package_json.mdc +44 -0
- package/rules/js-bun-redis/main.mdc +3 -11
- package/rules/js-mssql/js/mssql-in-list.mdc +38 -0
- package/rules/js-mssql/js/mssql-pool.mdc +56 -0
- package/rules/js-mssql/js/mssql-query-template.mdc +33 -0
- package/rules/js-mssql/js/mssql-tvp.mdc +75 -0
- package/rules/js-mssql/js/mssql-version.mdc +7 -0
- package/rules/js-mssql/main.mdc +10 -198
- package/rules/js-run/js/check-env.mdc +35 -0
- package/rules/js-run/js/conn-aliases.mdc +109 -0
- package/rules/js-run/js/jsconfig.mdc +20 -0
- package/rules/js-run/js/otel-configmap.mdc +6 -0
- package/rules/js-run/js/pino.mdc +6 -0
- package/rules/js-run/js/project-structure.mdc +11 -0
- package/rules/js-run/js/runtime.mdc +14 -0
- package/rules/js-run/js/scope.mdc +11 -0
- package/rules/js-run/js/settimeout.mdc +11 -0
- package/rules/js-run/js/temporal.mdc +5 -0
- package/rules/js-run/main.mdc +16 -218
- package/rules/k8s/js/configmap.mdc +41 -0
- package/rules/k8s/js/deployment_resources.mdc +49 -0
- package/rules/k8s/js/hasura_httproute.mdc +91 -0
- package/rules/k8s/js/hpa_apiversion.mdc +27 -0
- package/rules/k8s/js/ingress_gateway.mdc +16 -0
- package/rules/k8s/js/kustomize_structure.mdc +144 -0
- package/rules/k8s/js/lint_k8s.mdc +72 -0
- package/rules/k8s/js/multidoc_yaml.mdc +5 -0
- package/rules/k8s/js/network_policy.mdc +136 -0
- package/rules/k8s/js/schema_modeline.mdc +57 -0
- package/rules/k8s/js/service.mdc +44 -0
- package/rules/k8s/js/topology_hpa_pdb.mdc +181 -0
- package/rules/k8s/main.mdc +30 -843
- package/rules/nginx-default-tpl/js/dockerfile.mdc +36 -0
- package/rules/nginx-default-tpl/js/http-route.mdc +41 -0
- package/rules/nginx-default-tpl/js/ini-keys.mdc +21 -0
- package/rules/nginx-default-tpl/js/template-structure.mdc +86 -0
- package/rules/nginx-default-tpl/js/vscode.mdc +37 -0
- package/rules/nginx-default-tpl/main.mdc +6 -112
- package/rules/npm-module/js/docs/index.md +5 -5
- package/rules/npm-module/js/docs/rule_meta.md +6 -6
- package/rules/npm-module/js/docs/skill_meta.md +8 -8
- package/rules/npm-module/js/header_doc_pointer.mdc +18 -0
- package/rules/npm-module/js/package_structure.mdc +62 -0
- package/rules/npm-module/js/rule_meta.mdc +11 -0
- package/rules/npm-module/js/skill_meta.mdc +11 -0
- package/rules/npm-module/main.mdc +10 -55
- package/rules/php/js/lint_php_yml.mdc +12 -0
- package/rules/php/js/tooling.mdc +66 -0
- package/rules/php/main.mdc +7 -66
- package/rules/python/js/lint_python_yml.mdc +23 -0
- package/rules/python/js/pyproject_toml.mdc +32 -0
- package/rules/python/js/tooling.mdc +23 -0
- package/rules/python/main.mdc +9 -33
- package/rules/rego/js/rego-lint.mdc +31 -0
- package/rules/rego/js/vscode_extensions.mdc +11 -0
- package/rules/rego/js/vscode_settings.mdc +13 -0
- package/rules/rego/main.mdc +8 -24
- package/rules/rust/js/coverage.mdc +28 -0
- package/rules/rust/js/lint.mdc +22 -0
- package/rules/rust/js/tauri_composition.mdc +8 -0
- package/rules/rust/js/vscode_extensions.mdc +12 -0
- package/rules/rust/main.mdc +8 -38
- package/rules/security/js/rego_policies.mdc +15 -0
- package/rules/security/js/sample_secret.mdc +19 -0
- package/rules/security/js/trufflehog.mdc +21 -0
- package/rules/security/main.mdc +7 -35
- package/rules/style/js/admin-table.mdc +88 -0
- package/rules/style/js/colors.mdc +21 -0
- package/rules/style/js/gap.mdc +22 -0
- package/rules/style/js/quasar-fixes.mdc +32 -0
- package/rules/style/js/quasar.mdc +7 -0
- package/rules/style/js/tooling.mdc +85 -0
- package/rules/style/main.mdc +13 -253
- package/rules/tauri/js/cargo_mutants_config.mdc +39 -0
- package/rules/tauri/js/tool_surface.mdc +21 -0
- package/rules/tauri/js/tooling.mdc +25 -0
- package/rules/tauri/main.mdc +8 -78
- package/rules/test/js/cargo_mutants_config.mdc +18 -0
- package/rules/test/js/docs/index.md +7 -7
- package/rules/test/js/location.mdc +52 -0
- package/rules/test/js/no-console-store-restore.mdc +11 -0
- package/rules/test/js/no-process-chdir.mdc +15 -0
- package/rules/test/js/no-relative-fs-path.mdc +22 -0
- package/rules/test/js/sandbox-aware-test.mdc +28 -0
- package/rules/test/js/stryker_config.mdc +26 -0
- package/rules/test/js/vitest-config-pool-forks.mdc +33 -0
- package/rules/test/main.mdc +18 -184
- package/rules/text/js/ci-lint-text.mdc +15 -0
- package/rules/text/js/cspell.mdc +81 -0
- package/rules/text/js/dotenv-linter.mdc +16 -0
- package/rules/text/js/forbidden-prettier.mdc +13 -0
- package/rules/text/js/markdownlint.mdc +25 -0
- package/rules/text/js/oxfmt.mdc +35 -0
- package/rules/text/js/package-json.mdc +26 -0
- package/rules/text/js/shellcheck.mdc +18 -0
- package/rules/text/js/v8r.mdc +23 -0
- package/rules/text/js/vscode.mdc +86 -0
- package/rules/text/main.mdc +20 -237
- package/rules/vue/js/composition-api.mdc +82 -0
- package/rules/vue/js/nheader-layout.mdc +171 -0
- package/rules/vue/js/node-imports.mdc +25 -0
- package/rules/vue/js/quasar-ui.mdc +32 -0
- package/rules/vue/js/structure.mdc +101 -0
- package/rules/vue/js/testing.mdc +32 -0
- package/rules/vue/js/tfm-translations.mdc +26 -0
- package/rules/vue/js/vite-config.mdc +126 -0
- package/rules/vue/js/vite-env.mdc +55 -0
- package/rules/vue/js/vue-imports.mdc +25 -0
- package/rules/vue/main.mdc +16 -640
- package/scripts/docs/index.md +16 -16
- package/scripts/lib/docs/index.md +36 -36
- package/scripts/utils/docs/index.md +14 -14
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
## `.jscpd.json` — виявлення дублювання коду
|
|
2
|
+
|
|
3
|
+
У корені проєкту має бути `.jscpd.json`. Мінімум: увімкнути облік `.gitignore`, ненульовий код виходу при знаходженні клонів, консольний звіт.
|
|
4
|
+
|
|
5
|
+
```json title=".jscpd.json"
|
|
6
|
+
{
|
|
7
|
+
"gitignore": true,
|
|
8
|
+
"exitCode": 1,
|
|
9
|
+
"reporters": ["console"],
|
|
10
|
+
"minLines": 25,
|
|
11
|
+
"ignore": [".claude/worktrees/**", "**/dist/**", "**/CHANGELOG.md"]
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Канон ключів `.jscpd.json` (`gitignore`, `exitCode`, `reporters`, `minLines`, `ignore` як subset-of): [.jscpd.json.snippet.json](./policy/jscpd/template/.jscpd.json.snippet.json)
|
|
16
|
+
|
|
17
|
+
### Ігнорування `.claude/worktrees/`
|
|
18
|
+
|
|
19
|
+
Каталог `.claude/worktrees/` (робочі копії, які Claude Code створює через **superpowers:using-git-worktrees**) має ігноруватися:
|
|
20
|
+
- додай `.claude/worktrees/` у кореневий `.gitignore` (штатне місце для не-комітних робочих копій);
|
|
21
|
+
- у `.jscpd.json` додай `.claude/worktrees/**` у `ignore` як страховку на випадок запуску без `gitignore: true`.
|
|
22
|
+
|
|
23
|
+
Без цього jscpd сканує паралельну копію репо в worktree і фіксує самозбіги між дзеркальними файлами.
|
|
24
|
+
|
|
25
|
+
```text title=".gitignore (фрагмент)"
|
|
26
|
+
.claude/worktrees/
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Ігнорування `**/CHANGELOG.md`
|
|
30
|
+
|
|
31
|
+
`**/CHANGELOG.md` у каноні `ignore`: release-журнали різних пакетів структурно повторюються (заголовки `## [x.y.z] - YYYY-MM-DD`, секції `### Added / Changed / Fixed` за Keep a Changelog), і `jscpd` за `minLines: 25` фіксує їх як клон — це false positive. Без цього в монорепо легко зловити критичний `bun run lint` на парі CHANGELOG-ів довжиною від ~25 рядків.
|
|
32
|
+
|
|
33
|
+
### Рефакторинг vs конфіг
|
|
34
|
+
|
|
35
|
+
Коли **jscpd** знаходить клони, спочатку зменшуй дублювання кодом, а не конфігом.
|
|
36
|
+
|
|
37
|
+
- **Рефакторинг:** винеси спільні фрагменти в функції, модулі, утиліти, composables/hooks, спільні компоненти або базові типи — залежно від контексту.
|
|
38
|
+
- **Структура:** якщо одна й та сама логіка розмазана між файлами чи пакетами, запропонуй зміну структури (наприклад, спільний модуль, `shared/`, внутрішній пакет у monorepo), щоб була **одна канонічна реалізація** і повторні місця лише викликали її.
|
|
39
|
+
|
|
40
|
+
Розширення `ignore` чи завищення `minLines` лише щоб прибрати звіт — не заміна рефакторингу для справжніх клонів. Якщо збіг **семантично випадковий** (генерований код, формальні шаблони без спільної логіки), після оцінки допустимо точковий `ignore` або зміна порогу — з коротким обґрунтуванням.
|
|
41
|
+
|
|
42
|
+
Перед рефакторингом перевір чи є тести на блоки які підлягають зміні (Bun.test для js, playwright для vue). Якщо тестів немає або вони не покривають — спочатку створи їх, перевір що вони відпрацьовують коректно, потім роби рефакторинг і ще раз запускай тести.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
## `knip.json` — аналіз невикористаних залежностей та експортів
|
|
2
|
+
|
|
3
|
+
Залежнісний аналіз (knip — невикористані залежності/експорти, `knip.json` канон) крос-файловий; запускається автоматично у `lint --full` разом з jscpd.
|
|
4
|
+
|
|
5
|
+
У корені проєкту має бути **`knip.json`**, який стартує з канонічного baseline з пакета `@nitra/cursor` — файл [`npm/rules/js/js/tooling/knip-canonical.json`](./js/tooling/knip-canonical.json). Канон покриває типові false-positives для наших правил:
|
|
6
|
+
|
|
7
|
+
- `entry` зі CLI-конфігами (eslint, stylelint, oxlint, jscpd, markdownlint-cli2, `commitlint`);
|
|
8
|
+
- `project` для `**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts}`;
|
|
9
|
+
- `ignore` для `**/__fixtures__/**`;
|
|
10
|
+
- `ignoreDependencies` для пакетів, посилання на які є лише в не-JS-конфігах (`@nitra/cspell-dict`, `/@cspell\/dict-.+/`, `graphql`);
|
|
11
|
+
- `ignoreBinaries` для CLI, які канон вимагає викликати через `npx`/`bunx` і яких заборонено додавати в `devDependencies` (`actionlint`, `cspell`, `eslint`, `git-ai`, `jscpd`, `markdownlint-cli2`, `oxfmt`, `oxlint`, `shellcheck`, `uvx`, `v8r`, `zizmor`).
|
|
12
|
+
|
|
13
|
+
Якщо `knip.json` відсутній — `npx @nitra/cursor fix js` копіює канон у корінь проєкту (side effect). Після створення модифікуй файл під свій проєкт як завгодно: перевіряємо лише наявність, зміст подальших змін не валідується.
|
|
14
|
+
|
|
15
|
+
Пакет `knip` окремо в `devDependencies` не додавай — `bunx knip` тягне його ad-hoc, як oxlint/eslint/jscpd.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
## GitHub Actions: `lint-js.yml` — CI-лінт JS
|
|
2
|
+
|
|
3
|
+
Додай workflow для лінту JS у CI:
|
|
4
|
+
|
|
5
|
+
```yaml title=".github/workflows/lint-js.yml"
|
|
6
|
+
name: Lint JS
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches:
|
|
11
|
+
- dev
|
|
12
|
+
- main
|
|
13
|
+
paths:
|
|
14
|
+
- '**/*.js'
|
|
15
|
+
- '**/*.mjs'
|
|
16
|
+
- '**/*.cjs'
|
|
17
|
+
- '**/*.jsx'
|
|
18
|
+
- '**/*.ts'
|
|
19
|
+
- '**/*.tsx'
|
|
20
|
+
- '**/*.vue'
|
|
21
|
+
- '**/eslint.config.*'
|
|
22
|
+
|
|
23
|
+
pull_request:
|
|
24
|
+
branches:
|
|
25
|
+
- dev
|
|
26
|
+
- main
|
|
27
|
+
|
|
28
|
+
concurrency:
|
|
29
|
+
group: ${{ github.ref }}-${{ github.workflow }}
|
|
30
|
+
cancel-in-progress: true
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
eslint:
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
permissions:
|
|
36
|
+
contents: read
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v6
|
|
39
|
+
with:
|
|
40
|
+
persist-credentials: false
|
|
41
|
+
|
|
42
|
+
- uses: ./.github/actions/setup-bun-deps
|
|
43
|
+
|
|
44
|
+
- name: Eslint
|
|
45
|
+
run: |
|
|
46
|
+
bunx oxlint
|
|
47
|
+
bunx eslint .
|
|
48
|
+
bunx jscpd .
|
|
49
|
+
bunx knip --no-config-hints
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Канон workflow `.github/workflows/lint-js.yml`: [lint-js.yml.snippet.yml](./policy/lint_js_yml/template/lint-js.yml.snippet.yml)
|
|
53
|
+
|
|
54
|
+
Перед `./.github/actions/setup-bun-deps` — `actions/checkout@v6` (з `persist-credentials: false`, див. `ga.mdc`). Composite action: Node 24, Bun, кеш, `bun install --frozen-lockfile`.
|
|
55
|
+
|
|
56
|
+
Один workflow на лінт JS; зайвий `lint.yml` з тими самими кроками (`bunx oxlint`, `bunx eslint`, `jscpd`) — прибери.
|
|
57
|
+
|
|
58
|
+
У CI `--fix` для oxlint/eslint — **заборонено**: CI лише перевіряє, не виправляє.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
## `.oxlintrc.json` — канонічна конфігурація oxlint
|
|
2
|
+
|
|
3
|
+
У корені має бути `.oxlintrc.json`, який **збігається з каноном** oxlint з пакета `@nitra/cursor`: файл `npm/rules/js/js/data/tooling/oxlint-canonical.json` (plugins, jsPlugins з `@e18e/eslint-plugin`, categories, повний набір `rules` із канону — додаткові записи в `rules` дозволені; також `settings`, `env`, `globals`).
|
|
4
|
+
|
|
5
|
+
Поле `ignorePatterns` працює як `rules`: канонічні патерни (наразі `**/schema.graphql`, `**/auto-imports.d.ts`) мають бути присутні; додаткові локальні glob-и дозволені.
|
|
6
|
+
|
|
7
|
+
Мінімум для розуміння структури (реальний корінь конфігу має збігатися з каноном повністю):
|
|
8
|
+
|
|
9
|
+
```json title=".oxlintrc.json (фрагмент)"
|
|
10
|
+
{
|
|
11
|
+
"jsPlugins": ["@e18e/eslint-plugin"],
|
|
12
|
+
"rules": {
|
|
13
|
+
"e18e/prefer-includes": "error"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Модуль `@e18e/eslint-plugin` не оголошуй окремо в `package.json` — він уже в залежностях `@nitra/eslint-config` (з **3.8.0**), oxlint підвантажує його з `node_modules`.
|
|
19
|
+
|
|
20
|
+
Канон `oxlint-canonical.json` — source-of-truth, редагується напряму; у споживачі оновлюється копіюванням файлу з репозиторію пакета.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
## `package.json` — модульна система та runtime-вимоги
|
|
2
|
+
|
|
3
|
+
### `"type": "module"` у кожному `package.json`
|
|
4
|
+
|
|
5
|
+
У **кожному** `package.json` проєкту (корінь і всі workspace-пакети) має бути `"type": "module"` — весь код у ESM.
|
|
6
|
+
|
|
7
|
+
### `engines` — мінімальні версії Node і Bun
|
|
8
|
+
|
|
9
|
+
```json title="package.json"
|
|
10
|
+
{
|
|
11
|
+
"type": "module",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=24",
|
|
14
|
+
"bun": ">=1.3"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@nitra/eslint-config": "^3.10.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Канон `type`, мінімальна `@nitra/eslint-config` (semver-поріг `devDependencies`) і `engines`: [package.json.snippet.json](./policy/package_json/template/package.json.snippet.json).
|
|
23
|
+
|
|
24
|
+
### `@nitra/eslint-config` у `devDependencies`
|
|
25
|
+
|
|
26
|
+
У `devDependencies` кореневого `package.json` має бути `@nitra/eslint-config` — версія не нижче канонічного мінімуму зі snippet (semver-поріг, єдине джерело істини):
|
|
27
|
+
- з **3.8.0** — правило `no-restricted-syntax` забороняє `for...in`;
|
|
28
|
+
- з **3.9.2** — у `getConfig` вбудовано ignore для `**/adr/**`;
|
|
29
|
+
- транзитивно йде `@e18e/eslint-plugin` для oxlint.
|
|
30
|
+
|
|
31
|
+
Окремого `lint-js` скрипта немає — лінт через `n-cursor lint js` (CI — `--read-only`).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## Тести та покриття
|
|
2
|
+
|
|
3
|
+
### Unit-тести (Bun test)
|
|
4
|
+
|
|
5
|
+
Проєкт має бути покритий unit-тестами (**Bun test**). Код: синтаксис Node **24+**, **top level await** (узгоджено з `engines.node` у `package.json`).
|
|
6
|
+
|
|
7
|
+
### Покриття + мутаційне тестування JS
|
|
8
|
+
|
|
9
|
+
Покриття + мутаційне тестування JS постачаються через `n-cursor coverage` (правило `test.mdc`). Реалізація провайдера — у `npm/rules/js/coverage/coverage.mjs`: `bun test --coverage --coverage-reporter=lcov` + `bunx stryker run`. Stryker конфігурується в `stryker.config.mjs` у JS-корені (single-package або `workspaces[0]`).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
## Структура спільних модулів: `utils/` vs `lib/`
|
|
2
|
+
|
|
3
|
+
Коли спільні методи виносяться з кількох JS-файлів в окремий каталог — назву каталога обирай за призначенням модулів усередині.
|
|
4
|
+
|
|
5
|
+
- **`utils/`** — низькорівневі, чисті, generic helpers без бізнес-логіки і без залежностей від домену проєкту. Те, що могло б жити в окремому npm-пакеті. Приклади: `formatDate()`, `chunk(array, size)`, `retry(fn, opts)`, `deepMerge()`, `parseDuration('5m')`, `sleep(ms)`.
|
|
6
|
+
|
|
7
|
+
- **`lib/`** — внутрішні модулі / підсистеми проєкту, які знають про домен. Більші за `utils/`, часто з власним state, конфігом або side effects. Приклади: `lib/gmail-client.js`, `lib/circuit-breaker.js`, `lib/skill-loader.js`, `lib/safety/dry-run.js`.
|
|
8
|
+
|
|
9
|
+
Швидкий тест: якщо файл завтра можна опублікувати окремим npm-пакетом без переписування — це `utils/`; якщо він тримає domain-state, читає конфіг проєкту або викликає зовнішні сервіси/файли — це `lib/`. Не плутай із чужими каталогами на кшталт `shared/` чи `common/`: канонічні назви — лише `utils/` і `lib/`.
|
|
10
|
+
|
|
11
|
+
### Автоматична перевірка імпортів у `utils/`
|
|
12
|
+
|
|
13
|
+
`npx @nitra/cursor fix js` (концерн `utils_imports`) обходить кожен `utils/`-каталог у воркспейсах і падає, якщо знаходить relative-імпорт з `..` (вихід за межі каталогу) у будь-якому не-тестовому файлі. Це механічне втілення правила «utils не знає про домен»: тільки same-dir (`./X`), bare-пакети та `node:*` дозволені; cross-rule, конфіги проєкту чи sibling-utils — fail.
|
|
14
|
+
|
|
15
|
+
Якщо потрібна domain-залежність — перенеси файл у `lib/`.
|
package/rules/js/main.mdc
CHANGED
|
@@ -5,232 +5,39 @@ alwaysApply: false
|
|
|
5
5
|
version: '1.30'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
**oxlint**, **ESLint**, **jscpd**, **knip**. Запуск — **`n-cursor lint js`** (локально; у CI — `--read-only`, без **`--fix`** для oxlint/eslint
|
|
8
|
+
**oxlint**, **ESLint**, **jscpd**, **knip**. Запуск — **`n-cursor lint js`** (локально; у CI — `--read-only`, без **`--fix`** для oxlint/eslint). Без **prettier** і **@nitra/prettier-config**.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
[js-file-extensions](./js/file-extensions.mdc)
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
{
|
|
14
|
-
"type": "module",
|
|
15
|
-
"devDependencies": {
|
|
16
|
-
"@nitra/eslint-config": "^3.10.0"
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
```
|
|
12
|
+
[js-package-json](./js/package-json.mdc)
|
|
20
13
|
|
|
21
|
-
|
|
14
|
+
[js-eslint-config](./js/eslint-config.mdc)
|
|
22
15
|
|
|
23
|
-
|
|
16
|
+
[js-oxlintrc](./js/oxlintrc.mdc)
|
|
24
17
|
|
|
25
|
-
|
|
18
|
+
[js-extensions](./js/extensions.mdc)
|
|
26
19
|
|
|
27
|
-
-
|
|
28
|
-
- **`.cjs`** — для CommonJS, де він справді потрібен.
|
|
20
|
+
[js-jscpd](./js/jscpd.mdc)
|
|
29
21
|
|
|
30
|
-
|
|
22
|
+
[js-knip](./js/knip.mdc)
|
|
31
23
|
|
|
32
|
-
|
|
24
|
+
[js-dep-policy](./js/dep-policy.mdc)
|
|
33
25
|
|
|
34
|
-
|
|
26
|
+
[js-lint-js-workflow](./js/lint-js-workflow.mdc)
|
|
35
27
|
|
|
36
|
-
|
|
28
|
+
[js-utils-lib-structure](./js/utils-lib-structure.mdc)
|
|
37
29
|
|
|
38
|
-
|
|
30
|
+
[js-for-in](./js/for-in.mdc)
|
|
39
31
|
|
|
40
|
-
|
|
32
|
+
[js-tests](./js/tests.mdc)
|
|
41
33
|
|
|
42
|
-
|
|
43
|
-
{
|
|
44
|
-
"jsPlugins": ["@e18e/eslint-plugin"],
|
|
45
|
-
"rules": {
|
|
46
|
-
"e18e/prefer-includes": "error"
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
```
|
|
34
|
+
## Швидкий gate через conftest
|
|
50
35
|
|
|
51
|
-
|
|
36
|
+
Rego-пакети у `policy/` — запускаються `npx @nitra/cursor fix js` або `conftest`:
|
|
52
37
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"gitignore": true,
|
|
60
|
-
"exitCode": 1,
|
|
61
|
-
"reporters": ["console"],
|
|
62
|
-
"minLines": 25,
|
|
63
|
-
"ignore": [".claude/worktrees/**", "**/dist/**", "**/CHANGELOG.md"]
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Канон ключів `.jscpd.json` (`gitignore`, `exitCode`, `reporters`, `minLines`, `ignore` як subset-of): [.jscpd.json.snippet.json](./policy/jscpd/template/.jscpd.json.snippet.json)
|
|
68
|
-
|
|
69
|
-
```text title=".gitignore (фрагмент)"
|
|
70
|
-
.claude/worktrees/
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## Залежнісна політика (що не додавати)
|
|
74
|
-
|
|
75
|
-
`@e18e/eslint-plugin` окремо не додавай — він уже в залежностях `@nitra/eslint-config` (з **3.8.0**), oxlint підвантажує його з `node_modules`. Пакети oxlint/eslint/jscpd/knip теж не додавай у `devDependencies` без потреби монорепо — `bunx` тягне їх ad-hoc.
|
|
76
|
-
|
|
77
|
-
## knip
|
|
78
|
-
|
|
79
|
-
Залежнісний аналіз (knip — невикористані залежності/експорти, `knip.json` канон) крос-файловий; запускається автоматично у `lint --full` разом з jscpd.
|
|
80
|
-
|
|
81
|
-
У корені проєкту має бути **`knip.json`**, який стартує з канонічного baseline з пакета `@nitra/cursor` — файл [`npm/rules/js/js/tooling/knip-canonical.json`](./js/tooling/knip-canonical.json). Він покриває типові false-positives для наших правил: `entry` зі CLI-конфігами (eslint, stylelint, oxlint, jscpd, markdownlint-cli2, `commitlint`), `project` для `**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts}`, `ignore` для `**/__fixtures__/**`, `ignoreDependencies` для пакетів, посилання на які є лише в не-JS-конфігах (`@nitra/cspell-dict`, `/@cspell\/dict-.+/`, `graphql`), і `ignoreBinaries` для CLI, які канон вимагає викликати через `npx`/`bunx` і яких заборонено додавати в `devDependencies` (`actionlint`, `cspell`, `eslint`, `git-ai`, `jscpd`, `markdownlint-cli2`, `oxfmt`, `oxlint`, `shellcheck`, `uvx`, `v8r`, `zizmor`).
|
|
82
|
-
|
|
83
|
-
Якщо `knip.json` відсутній — `npx @nitra/cursor fix js` копіює канон у корінь проєкту (side effect). Після створення модифікуй файл під свій проєкт як завгодно: перевіряємо лише наявність, зміст подальших змін не валідується.
|
|
84
|
-
|
|
85
|
-
Пакет `knip` окремо в `devDependencies` не додавай — `bunx knip` тягне його ad-hoc, як oxlint/eslint/jscpd.
|
|
86
|
-
|
|
87
|
-
## Заборона `@nitra/as-integrations-fastify`
|
|
88
|
-
|
|
89
|
-
Пакет **`@nitra/as-integrations-fastify`** заборонений у **`dependencies`**, **`peerDependencies`** та в import-specifier-ах. Це чистий републіш upstream, застряглий на peer `@apollo/server: "^4.0.0"`, тож на Apollo 5 `bun install` дає `warn: incorrect peer dependency`. Заміна — upstream **`@as-integrations/fastify`** (**`^3.1.0`**): peer `@apollo/server: "^4.0.0 || ^5.0.0"` + `fastify: "^5.3.0"`.
|
|
90
|
-
|
|
91
|
-
Міграція — лише specifier у трьох місцях: залежність у `package.json`, `import`, та `vi.mock(...)` / `await import(...)` у тестах. **Код не міняється**, експорти ті самі: `default` → `fastifyApollo`, named → `fastifyApolloDrainPlugin`.
|
|
92
|
-
|
|
93
|
-
```javascript title="❌ до"
|
|
94
|
-
import fastifyApollo, { fastifyApolloDrainPlugin } from '@nitra/as-integrations-fastify'
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
```javascript title="✅ після"
|
|
98
|
-
import fastifyApollo, { fastifyApolloDrainPlugin } from '@as-integrations/fastify'
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## jscpd: рефакторинг і структура
|
|
102
|
-
|
|
103
|
-
Коли **jscpd** знаходить клони, спочатку зменшуй дублювання кодом, а не конфігом.
|
|
104
|
-
|
|
105
|
-
- **Рефакторинг:** винеси спільні фрагменти в функції, модулі, утиліти, composables/hooks, спільні компоненти або базові типи — залежно від контексту.
|
|
106
|
-
- **Структура:** якщо одна й та сама логіка розмазана між файлами чи пакетами, запропонуй зміну структури (наприклад, спільний модуль, `shared/`, внутрішній пакет у monorepo), щоб була **одна канонічна реалізація** і повторні місця лише викликали її.
|
|
107
|
-
|
|
108
|
-
Так ти уникаєш **хибних обходів** перевірки: розширення `ignore` чи завищення `minLines` лише щоб прибрати звіт — не заміна рефакторингу для справжніх клонів. Якщо збіг **семантично випадковий** (генерований код, формальні шаблони без спільної логіки), після оцінки допустимо точковий `ignore` або зміна порогу — з коротким обґрунтуванням.
|
|
109
|
-
|
|
110
|
-
Але обов'язково перед рефакторингом перевір чи є тести на блоки які підлягають зміні, а саме Bun.test для js та playwright для vue. Якщо є, то перевір чи вони покривають блоки які підлягають зміні. Якщо не покривають або тестів немає — спочатку створи їх, перевір що вони покривають і відпрацьовують коректно, а потім роби рефакторинг і ще раз запускай тести, але якщо тести не відпрацьовують коректно після рефакторингу, то не роби рефакторинг.
|
|
111
|
-
|
|
112
|
-
## Структура спільних модулів: `utils/` vs `lib/`
|
|
113
|
-
|
|
114
|
-
Коли спільні методи виносяться з кількох JS-файлів в окремий каталог — назву каталога обирай за призначенням модулів усередині.
|
|
115
|
-
|
|
116
|
-
- **`utils/`** — низькорівневі, чисті, generic helpers без бізнес-логіки і без залежностей від домену проєкту. Те, що могло б жити в окремому npm-пакеті. Приклади: `formatDate()`, `chunk(array, size)`, `retry(fn, opts)`, `deepMerge()`, `parseDuration('5m')`, `sleep(ms)`.
|
|
117
|
-
|
|
118
|
-
- **`lib/`** — внутрішні модулі / підсистеми проєкту, які знають про домен. Більші за `utils/`, часто з власним state, конфігом або side effects. Приклади: `lib/gmail-client.js`, `lib/circuit-breaker.js`, `lib/skill-loader.js`, `lib/safety/dry-run.js`.
|
|
119
|
-
|
|
120
|
-
Швидкий тест: якщо файл завтра можна опублікувати окремим npm-пакетом без переписування — це `utils/`; якщо він тримає domain-state, читає конфіг проєкту або викликає зовнішні сервіси/файли — це `lib/`. Не плутай із чужими каталогами на кшталт `shared/` чи `common/`: канонічні назви — лише `utils/` і `lib/`.
|
|
121
|
-
|
|
122
|
-
Автоматично гарантується: `npx @nitra/cursor fix js` (концерн `utils_imports`) обходить кожен `utils/`-каталог у воркспейсах і падає, якщо знаходить relative-імпорт з `..` (вихід за межі каталогу) у будь-якому не-тестовому файлі. Це механічне втілення правила «utils не знає про домен»: тільки same-dir (`./X`), bare-пакети та `node:*` дозволені; cross-rule, конфіги проєкту чи sibling-utils — fail. Якщо потрібна domain-залежність — перенеси файл у `lib/`.
|
|
123
|
-
|
|
124
|
-
Додай workflow:
|
|
125
|
-
|
|
126
|
-
```yaml title=".github/workflows/lint-js.yml"
|
|
127
|
-
name: Lint JS
|
|
128
|
-
|
|
129
|
-
on:
|
|
130
|
-
push:
|
|
131
|
-
branches:
|
|
132
|
-
- dev
|
|
133
|
-
- main
|
|
134
|
-
paths:
|
|
135
|
-
- '**/*.js'
|
|
136
|
-
- '**/*.mjs'
|
|
137
|
-
- '**/*.cjs'
|
|
138
|
-
- '**/*.jsx'
|
|
139
|
-
- '**/*.ts'
|
|
140
|
-
- '**/*.tsx'
|
|
141
|
-
- '**/*.vue'
|
|
142
|
-
- '**/eslint.config.*'
|
|
143
|
-
|
|
144
|
-
pull_request:
|
|
145
|
-
branches:
|
|
146
|
-
- dev
|
|
147
|
-
- main
|
|
148
|
-
|
|
149
|
-
concurrency:
|
|
150
|
-
group: ${{ github.ref }}-${{ github.workflow }}
|
|
151
|
-
cancel-in-progress: true
|
|
152
|
-
|
|
153
|
-
jobs:
|
|
154
|
-
eslint:
|
|
155
|
-
runs-on: ubuntu-latest
|
|
156
|
-
permissions:
|
|
157
|
-
contents: read
|
|
158
|
-
steps:
|
|
159
|
-
- uses: actions/checkout@v6
|
|
160
|
-
with:
|
|
161
|
-
persist-credentials: false
|
|
162
|
-
|
|
163
|
-
- uses: ./.github/actions/setup-bun-deps
|
|
164
|
-
|
|
165
|
-
- name: Eslint
|
|
166
|
-
run: |
|
|
167
|
-
bunx oxlint
|
|
168
|
-
bunx eslint .
|
|
169
|
-
bunx jscpd .
|
|
170
|
-
bunx knip --no-config-hints
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
Канон workflow `.github/workflows/lint-js.yml`: [lint-js.yml.snippet.yml](./policy/lint_js_yml/template/lint-js.yml.snippet.yml)
|
|
174
|
-
|
|
175
|
-
Перед **`./.github/actions/setup-bun-deps`** — **`actions/checkout@v6`** (див. **ga.mdc**). Composite: Node 24, Bun, кеш, `bun install --frozen-lockfile`.
|
|
176
|
-
|
|
177
|
-
Один workflow на лінт JS; зайвий `lint.yml` з тими самими кроками — прибери.
|
|
178
|
-
|
|
179
|
-
```javascript title="eslint.config.js"
|
|
180
|
-
import { getConfig } from '@nitra/eslint-config'
|
|
181
|
-
|
|
182
|
-
export default [
|
|
183
|
-
{
|
|
184
|
-
ignores: ['**/auto-imports.d.ts']
|
|
185
|
-
},
|
|
186
|
-
...getConfig({
|
|
187
|
-
node: ['npm']
|
|
188
|
-
})
|
|
189
|
-
]
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
У монорепо пакети з Vite (frontend) вкажи в секції `vue`, решту — у секції `node` у виклику `getConfig`.
|
|
193
|
-
|
|
194
|
-
## Додаткові js правила
|
|
195
|
-
|
|
196
|
-
Завжди додавай до package.json що підтримується 24+ версія node і Bun 1.3+:
|
|
197
|
-
|
|
198
|
-
```json title="package.json"
|
|
199
|
-
"engines": {
|
|
200
|
-
"node": ">=24",
|
|
201
|
-
"bun": ">=1.3"
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
## `for...in` заборонено — рефакторити на `for...of`
|
|
206
|
-
|
|
207
|
-
Конструкція `for (const k in obj)` обходить успадковані ключі прототипу, тому майже завжди тягне `Object.hasOwn(obj, k)`-guard. Заборонена у `@nitra/eslint-config` через `no-restricted-syntax` для `ForInStatement` (з версії **3.8.0**). У каноні oxlint лишається `guard-for-in` як часткова страховка (oxlint не підтримує `no-restricted-syntax`). Замість цього обходь масив напряму, а обʼєкт — через `Object.entries` / `Object.keys` / `Object.values`. У такому коді guard за `Object.hasOwn` стає непотрібним і має зникнути разом із `for...in`.
|
|
208
|
-
|
|
209
|
-
```javascript title="❌ до"
|
|
210
|
-
for (const k in obj) {
|
|
211
|
-
if (!Object.hasOwn(obj, k)) continue
|
|
212
|
-
use(k, obj[k])
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
for (const i in arr) {
|
|
216
|
-
use(arr[i])
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
```javascript title="✅ після"
|
|
221
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
222
|
-
use(k, v)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
for (const item of arr) {
|
|
226
|
-
use(item)
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
## Тести
|
|
231
|
-
|
|
232
|
-
Проєкт має бути покритий unit-тестами (**Bun test**). Код: синтаксис Node **24+**, **top level await** (узгоджено з `engines.node` у `package.json`).
|
|
233
|
-
|
|
234
|
-
## Покриття + мутаційне тестування JS
|
|
235
|
-
|
|
236
|
-
Покриття + мутаційне тестування JS постачаються через `n-cursor coverage` (правило `test.mdc`). Реалізація провайдера — у `npm/rules/js/coverage/coverage.mjs`: `bun test --coverage --coverage-reporter=lcov` + `bunx stryker run`. Stryker конфігурується в `stryker.config.mjs` у JS-корені (single-package або `workspaces[0]`).
|
|
38
|
+
| Namespace | Що перевіряє |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `js_lint.package_json` | `package.json`: `"type": "module"`, `engines.node >= 24`, `engines.bun >= 1.3`, `@nitra/eslint-config` >= semver-поріг |
|
|
41
|
+
| `js_lint.jscpd` | `.jscpd.json`: `gitignore`, `exitCode`, `reporters` (subset), `minLines >= канон` |
|
|
42
|
+
| `js_lint.lint_js_yml` | `.github/workflows/lint-js.yml`: required uses/run кроки, `persist-credentials: false`, заборона `--fix` у CI |
|
|
43
|
+
| `js_lint.vscode_extensions` | `.vscode/extensions.json`: наявність трьох канонічних recommendations |
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
## Перехід з `pg` / `mysql2` на Bun native SQL
|
|
2
|
+
|
|
3
|
+
PostgreSQL 18+, MariaDB 10.6+ (сумісний з MySQL-протоколом, підключаємось як `mysql://`).
|
|
4
|
+
|
|
5
|
+
Якщо в проєкті використовуються бібліотеки `pg`, `pg-format` або `mysql2`, їх потрібно замінити на Bun native SQL: <https://bun.com/docs/runtime/sql>.
|
|
6
|
+
|
|
7
|
+
- Видалити з `dependencies`: `pg-pool`, `pg-native`, `pg-format`, `mysql`, `mysql2`.
|
|
8
|
+
- Видалити з коду: усі `import` / `require` цих пакетів та власні обгортки над ними.
|
|
9
|
+
- Замінити на `import { sql, SQL } from 'bun'` — Bun має вбудований клієнт із пулом, prepared statements та tagged templates.
|
|
10
|
+
|
|
11
|
+
Канон заборонених `dependencies` (`pg-format`, `mysql2`): [package.json.deny.json](./policy/package_json/template/package.json.deny.json). Сам `pg` із денилисту прибрано — він має одне легітимне виключення (LISTEN/NOTIFY), яке зважує AST-сканер; деталі — у `js/pg-listen-notify.mdc`.
|
|
12
|
+
|
|
13
|
+
`pg-format` (unscoped) — це ручне форматування SQL через escape (`format('... %L ...', value)`); такі рядки легко поламати неправильним типом, locale-залежним escape або забутим `%L`. Tagged template Bun SQL параметризує значення нативно (`sql\`... ${value} ...\``) і не лишає простору для injection — окремий «форматер» **для значень** не потрібен.
|
|
14
|
+
|
|
15
|
+
Виключення є **лише** для **динамічних identifiers** (назви схем / таблиць / колонок / індексів / ролей / БД) і whitelist-фрагментів типу `ASC`/`DESC`: Bun SQL їх параметризувати не вміє, тож тут дозволено окремий пакет **`@scaleleap/pg-format`** (scoped форк, не unscoped `pg-format`) — деталі й приклади у `js/pg-format-identifiers.mdc`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
## Підключення (singleton + env)
|
|
2
|
+
|
|
3
|
+
Дефолтний експорт `sql` з `'bun'` сам читає змінні середовища (`DATABASE_URL`, `POSTGRES_URL`, `MYSQL_URL`, `PGHOST`/`PGUSER`/... та `MYSQL_HOST`/`MYSQL_USER`/...) і керує пулом — окремий `Pool` як у `pg` створювати не треба.
|
|
4
|
+
|
|
5
|
+
Для явного конфігу — `new SQL(...)` як **singleton** на рівні модуля, а не на кожен запит. Файл кладеться у `src/conn/db.mjs` і експортує іменовані константи `pgWrite` (основний запис) та `pgRead` (read-only replica), щоб glob `**/src/conn/**` у правилах покривав ці файли:
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
// src/conn/db.mjs
|
|
9
|
+
import { SQL } from 'bun'
|
|
10
|
+
|
|
11
|
+
export const pgWrite = new SQL({
|
|
12
|
+
url: process.env.DATABASE_URL,
|
|
13
|
+
max: 20,
|
|
14
|
+
idleTimeout: 30,
|
|
15
|
+
connectionTimeout: 10
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export const pgRead = new SQL({
|
|
19
|
+
url: process.env.PG_CONN_READ ?? process.env.DATABASE_URL,
|
|
20
|
+
max: 10,
|
|
21
|
+
idleTimeout: 30,
|
|
22
|
+
connectionTimeout: 10
|
|
23
|
+
})
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Connection string обирає адаптер автоматично:
|
|
27
|
+
|
|
28
|
+
- `postgres://...` / `postgresql://...` → PostgreSQL
|
|
29
|
+
- `mysql://...` / `mysql2://...` → MySQL/MariaDB
|
|
30
|
+
- `sqlite://...` / `file://...` / `:memory:` → SQLite
|
|
31
|
+
|
|
32
|
+
### Не створювати підключення на кожен запит
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
// ❌ нове підключення/інстанс на кожен виклик
|
|
36
|
+
function getUser(id) {
|
|
37
|
+
const pgWrite = new SQL(process.env.DATABASE_URL)
|
|
38
|
+
return pgWrite`SELECT * FROM users WHERE id = ${id}`
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`new SQL(...)` має створюватись **один раз** на рівні модуля. Bun сам тримає пул (`max`, `idleTimeout`, `maxLifetime`) — окремих `Pool`/`Client` як у `pg` не потрібно.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
## Динамічна SQL-структура: `@scaleleap/pg-format` для identifiers
|
|
2
|
+
|
|
3
|
+
Bun SQL **не вміє** параметризувати назви схем, таблиць, колонок, індексів, ролей, БД — а `sql\`SELECT * FROM ${table}\`` забіндив би це як значення і зламав би синтаксис. Для **динамічних identifiers** дозволено окремий пакет:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun add @scaleleap/pg-format
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
⚠️ Це **scoped `@scaleleap/pg-format`**, а не unscoped `pg-format` (той у [deny-списку](./policy/package_json/template/package.json.deny.json)). Беремо форк `@scaleleap` **тільки** заради `%I` / `%s`-можливостей; значення все одно проходять через Bun parameters, **не** через `%L`.
|
|
10
|
+
|
|
11
|
+
### Дозволений патерн
|
|
12
|
+
|
|
13
|
+
- **`%I`** — escape SQL identifier (schema / table / column / index / role / database).
|
|
14
|
+
- **`%s`** — raw fragment, **тільки** для whitelist-значень (`ASC` / `DESC`, тип JOIN'у тощо).
|
|
15
|
+
- Значення — позиційні параметри `$1, $2, …`, які передаються другим аргументом у `sql.unsafe(query, [bindParams])`.
|
|
16
|
+
- На рядку виклику `sql.unsafe(...)` обов'язковий маркер `// allow-unsafe: <причина>` (div. `js/unsafe.mdc`).
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
import format from '@scaleleap/pg-format'
|
|
20
|
+
import { sql } from 'bun'
|
|
21
|
+
|
|
22
|
+
const allowedColumns = new Set(['created_at', 'email', 'name'])
|
|
23
|
+
if (!allowedColumns.has(sortBy)) throw new Error('Invalid sort column')
|
|
24
|
+
|
|
25
|
+
const direction = sortDir === 'asc' ? 'ASC' : 'DESC'
|
|
26
|
+
|
|
27
|
+
const query = format(
|
|
28
|
+
'SELECT * FROM %I.%I ORDER BY %I %s LIMIT $1',
|
|
29
|
+
schemaName,
|
|
30
|
+
tableName,
|
|
31
|
+
sortBy,
|
|
32
|
+
direction
|
|
33
|
+
)
|
|
34
|
+
// allow-unsafe: динамічні schema/table/column; значення біндяться через $N
|
|
35
|
+
const rows = await sql.unsafe(query, [limit])
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Multi-row `INSERT` через `VALUES %L` теж типовий легітимний кейс, але передавай значення колонок як паралельні масиви через `unnest(...)` Bun SQL — `format('VALUES %L', rows)` лишай тільки коли альтернатива з `unnest` неможлива:
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
const query = format(
|
|
42
|
+
/* sql */ `
|
|
43
|
+
INSERT INTO "order".delivery_status (order_id, status, changed_at)
|
|
44
|
+
SELECT v.order_id::uuid, v.status, v.changed_at::timestamptz
|
|
45
|
+
FROM (VALUES %L) AS v(order_id, status, changed_at)
|
|
46
|
+
`,
|
|
47
|
+
values
|
|
48
|
+
)
|
|
49
|
+
// allow-unsafe: multi-row VALUES для бекфілу; values формуються з валідованого input
|
|
50
|
+
await sql.unsafe(query)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Заборонено й після підключення `@scaleleap/pg-format`
|
|
54
|
+
|
|
55
|
+
- **`%L` для user input** — це повернення `pg-format`-стилю. Завжди bind через Bun (`sql\`... = ${value}\``) або позиційний параметр `$N` + `sql.unsafe(query, [params])`.
|
|
56
|
+
- Збирати весь `WHERE` через `format(...)` з `%L` — користуйся whitelist полів і ручним складанням `$N`-placeholder'ів (приклад нижче).
|
|
57
|
+
- Власні функції `format` / `pgFormat` / `sqlFormat` / `pgFmt` з тілом, що містить `%L` / `%I` / `%s`, — `fail` сканера (це шим, а не імпорт з бібліотеки).
|
|
58
|
+
- Експортовані `quoteLiteral` / `quoteIdent` / `escapeLiteral` / `escapeIdent` — `fail` сканера (pg-format-специфічні API замість Bun parameters).
|
|
59
|
+
|
|
60
|
+
### Dynamic `WHERE` — без `format(...)`, через whitelist + `$N`
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
const conditions = []
|
|
64
|
+
const values = []
|
|
65
|
+
|
|
66
|
+
if (email) {
|
|
67
|
+
values.push(email)
|
|
68
|
+
conditions.push(`email = $${values.length}`)
|
|
69
|
+
}
|
|
70
|
+
if (status) {
|
|
71
|
+
values.push(status)
|
|
72
|
+
conditions.push(`status = $${values.length}`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''
|
|
76
|
+
const query = `SELECT * FROM users ${where}`
|
|
77
|
+
// allow-unsafe: динамічний WHERE з whitelist-полів; значення біндяться через $N
|
|
78
|
+
const rows = await sql.unsafe(query, values)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Коротка таблиця рішень
|
|
82
|
+
|
|
83
|
+
| Сценарій | Що використовувати |
|
|
84
|
+
| --------------------------------- | ---------------------------------------------------- |
|
|
85
|
+
| `WHERE id = ${...}` | Bun SQL tagged template |
|
|
86
|
+
| `INSERT` одного рядка | Bun SQL tagged template |
|
|
87
|
+
| `INSERT` масиву (object/colset) | Bun SQL helper `sql(rows, 'a', 'b')` або `unnest` |
|
|
88
|
+
| `UPDATE field = ${value}` | Bun SQL tagged template |
|
|
89
|
+
| Динамічна назва schema / table | `@scaleleap/pg-format` `%I` + `sql.unsafe(q, [...])` |
|
|
90
|
+
| Динамічна назва колонки | `@scaleleap/pg-format` `%I` + bind |
|
|
91
|
+
| Динамічний `ORDER BY column` | whitelist + `%I` |
|
|
92
|
+
| `ASC` / `DESC`, тип JOIN'у | whitelist + `%s` |
|
|
93
|
+
| Динамічний `WHERE` (полів багато) | whitelist + ручні `$N` + `sql.unsafe(text, vals)` |
|
|
94
|
+
| Сирий migration / DDL | `sql.unsafe(text)` з `// allow-unsafe: <причина>` |
|
|
95
|
+
| User input як value | **тільки** Bun parameters / `$N` bind |
|
|
96
|
+
| Масив значень у `unnest(...)` | `sql.array(arr, type)` — обов'язково з типом |
|
|
97
|
+
|
|
98
|
+
Головне правило:
|
|
99
|
+
|
|
100
|
+
- **SQL values** → Bun SQL parameters (tagged template `${value}` або `$N` + `sql.unsafe(text, values)`).
|
|
101
|
+
- **SQL identifiers** → `@scaleleap/pg-format` `%I` (schema, table, column, index, role, database).
|
|
102
|
+
- **SQL fragments** (`ASC`/`DESC` тощо) → whitelist + `%s`.
|