@7n/rules 1.7.0 → 1.7.1
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 +8 -0
- package/package.json +1 -1
- package/rules/abie/http_route_base/http_route_base.mdc +23 -1
- package/rules/adr/madr_format/concern.json +1 -0
- package/rules/adr/madr_format/madr_format.mdc +119 -0
- package/rules/bun/bunfig/bunfig.mdc +5 -0
- package/rules/bun/lint-surface/concern.json +3 -0
- package/rules/bun/lint-surface/lint-surface.mdc +13 -0
- package/rules/capacitor/platforms/docs/main.md +1 -1
- package/rules/capacitor/platforms/main.mjs +5 -1
- package/rules/capacitor/platforms/platforms.mdc +108 -0
- package/rules/changelog/consistency/comparison-models.mdc +46 -0
- package/rules/changelog/consistency/consistency.mdc +35 -0
- package/rules/docker/main.mdc +235 -2
- package/rules/image-avif/avif_generation/avif_generation.mdc +14 -0
- package/rules/js/check/check.mdc +26 -0
- package/rules/js/file-extensions/concern.json +3 -0
- package/rules/js/file-extensions/file-extensions.mdc +12 -0
- package/rules/js/jscpd_config/jscpd_config.mdc +28 -0
- package/rules/js/knip/knip.mdc +15 -0
- package/rules/js/utils_imports/utils_imports.mdc +15 -0
- package/rules/js-bun-db/connection/concern.json +3 -0
- package/rules/js-bun-db/connection/connection.mdc +42 -0
- package/rules/js-bun-db/package_json/package_json.mdc +15 -1
- package/rules/js-bun-db/pg_format_identifiers/concern.json +3 -0
- package/rules/js-bun-db/pg_format_identifiers/pg_format_identifiers.mdc +104 -0
- package/rules/js-bun-db/safety/safety.mdc +458 -0
- package/rules/js-mssql/main.mdc +130 -0
- package/rules/js-mssql/mssql-tvp/concern.json +3 -0
- package/rules/js-mssql/mssql-tvp/mssql-tvp.mdc +77 -0
- package/rules/js-run/configmap/configmap.mdc +6 -0
- package/rules/js-run/jsconfig/jsconfig.mdc +23 -0
- package/rules/js-run/package_json/package_json.mdc +6 -0
- package/rules/js-run/project-structure/concern.json +3 -0
- package/rules/js-run/project-structure/project-structure.mdc +11 -0
- package/rules/js-run/runtime/runtime.mdc +170 -0
- package/rules/js-run/scope/concern.json +3 -0
- package/rules/js-run/scope/scope.mdc +11 -0
- package/rules/k8s/hasura_configmap/hasura_configmap.mdc +6 -0
- package/rules/k8s/hpa_pdb/hpa_pdb.mdc +134 -0
- package/rules/k8s/kubeconform/kubeconform.mdc +38 -0
- package/rules/k8s/kustomization/kustomization.mdc +73 -0
- package/rules/k8s/main.mdc +68 -0
- package/rules/k8s/manifest/manifest.mdc +37 -0
- package/rules/k8s/manifests/docs/fix-manifests.md +3 -1
- package/rules/k8s/manifests/fix-manifests.mjs +11 -0
- package/rules/k8s/manifests/main.mjs +28 -0
- package/rules/k8s/network_policy/network_policy.mdc +33 -0
- package/rules/nginx-default-tpl/http-route/concern.json +1 -0
- package/rules/nginx-default-tpl/http-route/http-route.mdc +54 -0
- package/rules/nginx-default-tpl/template/template.mdc +152 -0
- package/rules/php/tooling/tooling.mdc +7 -6
- package/rules/python/pyproject_toml/pyproject_toml.mdc +17 -1
- package/rules/python/tooling/tooling.mdc +9 -10
- package/rules/rego/main.mdc +14 -0
- package/rules/rust/check/check.mdc +16 -0
- package/rules/style/admin_table/admin_table.mdc +88 -0
- package/rules/style/admin_table/concern.json +7 -0
- package/rules/style/admin_table/docs/index.md +9 -0
- package/rules/style/admin_table/docs/main.md +14 -0
- package/rules/style/admin_table/main.mjs +46 -0
- package/rules/style/colors/colors.mdc +21 -0
- package/rules/style/colors/concern.json +3 -0
- package/rules/style/gap/concern.json +7 -0
- package/rules/style/gap/docs/index.md +9 -0
- package/rules/style/gap/docs/main.md +15 -0
- package/rules/style/gap/gap.mdc +22 -0
- package/rules/style/gap/main.mjs +51 -0
- package/rules/style/quasar/concern.json +3 -0
- package/rules/style/quasar/quasar.mdc +7 -0
- package/rules/style/quasar_fixes/concern.json +7 -0
- package/rules/style/quasar_fixes/docs/index.md +9 -0
- package/rules/style/quasar_fixes/docs/main.md +16 -0
- package/rules/style/quasar_fixes/main.mjs +57 -0
- package/rules/style/quasar_fixes/quasar_fixes.mdc +32 -0
- package/rules/tauri/tool_surface/concern.json +16 -0
- package/rules/tauri/tool_surface/docs/index.md +9 -0
- package/rules/tauri/tool_surface/docs/main.md +24 -0
- package/rules/tauri/tool_surface/main.mjs +145 -0
- package/rules/tauri/tool_surface/tool_surface.mdc +29 -0
- package/rules/test/vitest-api-conventions/concern.json +7 -0
- package/rules/test/vitest-api-conventions/docs/index.md +9 -0
- package/rules/test/vitest-api-conventions/docs/main.md +40 -0
- package/rules/test/vitest-api-conventions/main.mjs +186 -0
- package/rules/test/vitest-api-conventions/vitest-api-conventions.mdc +129 -0
- package/rules/text/cspell/cspell.mdc +18 -0
- package/rules/text/markdownlint/markdownlint.mdc +4 -0
- package/rules/text/run-dotenv-linter/run-dotenv-linter.mdc +17 -0
- package/rules/text/run-shellcheck/run-shellcheck.mdc +17 -0
- package/rules/text/run-v8r/run-v8r.mdc +23 -0
- package/rules/vue/composition-api/composition-api.mdc +82 -0
- package/rules/vue/composition-api/concern.json +3 -0
- package/rules/vue/main.mdc +1 -1
- package/rules/vue/nheader-layout/concern.json +3 -0
- package/rules/vue/nheader-layout/nheader-layout.mdc +171 -0
- package/rules/vue/packages/packages.mdc +56 -0
- package/rules/vue/quasar-ui/concern.json +3 -0
- package/rules/vue/quasar-ui/quasar-ui.mdc +32 -0
- package/rules/vue/structure/concern.json +3 -0
- package/rules/vue/structure/structure.mdc +101 -0
- package/rules/vue/testing/concern.json +3 -0
- package/rules/vue/testing/testing.mdc +40 -0
- package/rules/vue/tfm-translations/concern.json +7 -0
- package/rules/vue/tfm-translations/docs/main.md +29 -0
- package/rules/vue/tfm-translations/main.mjs +55 -0
- package/rules/vue/tfm-translations/tfm-translations.mdc +32 -0
- package/rules/vue/vite-config/concern.json +3 -0
- package/rules/vue/vite-config/vite-config.mdc +153 -0
- package/rules/vue/vite-env/concern.json +3 -0
- package/rules/vue/vite-env/vite-env.mdc +61 -0
package/rules/docker/main.mdc
CHANGED
|
@@ -1,14 +1,247 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Dockerfile — lint-docker / hadolint; перевірка check-docker
|
|
3
|
-
version: '1.
|
|
3
|
+
version: '1.13'
|
|
4
4
|
globs: "**/Dockerfile*"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Docker — hadolint
|
|
9
9
|
|
|
10
|
-
Правило перевіряє Dockerfile / Containerfile: GCR-дзеркало замість Docker Hub, multistage build з дозволеними runtime-образами, компіляцію bun-проєктів у бінарник, виняток для нативних `.node`-аддонів, non-root у фінальному stage, nginx-специфічні вимоги та hadolint CI-workflow.
|
|
10
|
+
Правило перевіряє Dockerfile / Containerfile: GCR-дзеркало замість Docker Hub, multistage build з дозволеними runtime-образами, компіляцію bun-проєктів у бінарник, виняток для нативних `.node`-аддонів, non-root у фінальному stage, nginx-специфічні вимоги та hadolint CI-workflow. Усі перевірки нижче — один прохід по кожному Dockerfile/Containerfile у **`npm/rules/docker/lint/main.mjs`** (`npx @7n/rules lint docker`).
|
|
11
11
|
|
|
12
12
|
## Активація
|
|
13
13
|
|
|
14
14
|
Спрацьовує на всі файли `Dockerfile*`; `check docker` (`fix.mjs`) обробляє також `Containerfile` та `Containerfile.*`.
|
|
15
|
+
|
|
16
|
+
## GCR-дзеркало замість Docker Hub
|
|
17
|
+
|
|
18
|
+
Для образів з Docker Hub — **`oven/bun`**, **`alpine`**, **`nginx`**, **`nginxinc/nginx-unprivileged`**, **`node`** — у **`FROM`** треба вказувати дзеркало GCR, а не pull напряму з Hub:
|
|
19
|
+
|
|
20
|
+
| Docker Hub | GCR-дзеркало |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `oven/bun` | `mirror.gcr.io/oven/bun` |
|
|
23
|
+
| `alpine` | `mirror.gcr.io/library/alpine` |
|
|
24
|
+
| `nginx` | `mirror.gcr.io/library/nginx` |
|
|
25
|
+
| `node` | `mirror.gcr.io/library/node` |
|
|
26
|
+
| `nginxinc/nginx-unprivileged` | `mirror.gcr.io/nginxinc/nginx-unprivileged` |
|
|
27
|
+
|
|
28
|
+
Перевіряє **`npm/rules/docker/lib/docker-mirror.mjs`** (через `getMirrorGcrHint`).
|
|
29
|
+
|
|
30
|
+
## Multistage build і дозволені runtime-образи
|
|
31
|
+
|
|
32
|
+
Dockerfile/Containerfile **має бути multistage build**: окремий build stage (залежності/компіляція) і окремий runtime stage. У фінальному stage дозволені лише мінімальні базові образи:
|
|
33
|
+
|
|
34
|
+
- **backend (типово)**: `mirror.gcr.io/library/alpine:*`
|
|
35
|
+
- **ультра-легкі (glibc / одна статична збірка)**: `scratch` — тільки як `FROM scratch` (офіційний порожній базовий шар), коли весь **runtime** уже в `COPY --from=…`
|
|
36
|
+
- **glibc, Debian (slim)**: `mirror.gcr.io/library/debian:*` **лише** з тегом, у якому є `slim` (наприклад `bookworm-slim`, `trixie-slim`), а не `bookworm` без `slim`. Debian-slim виправданий **лише** коли потрібен саме glibc-рантайм (нативні glibc-залежності, prebuilds без musl-варіанта). **Non-root сам по собі не є підставою** переходити сюди: `mirror.gcr.io/oven/bun:alpine` уже має користувача `bun` (uid/gid 1000), тож `USER bun` дає non-root **без зміни бази**. Перехід Alpine→Debian заради лише non-root — **антипатерн** (більший образ + зайва musl→glibc міграція bun-бінарника)
|
|
37
|
+
- **виняток (інтерпретовані стеки)**: `mirror.gcr.io/library/php:*` або `mirror.gcr.io/library/python:*` — якщо сервіс має крутитися в офіційному runtime PHP чи Python, а не як один бінарник на Alpine; інакше лишай **alpine** у фінальному stage
|
|
38
|
+
- **frontend**: `mirror.gcr.io/nginxinc/nginx-unprivileged:*` або `mirror.gcr.io/openresty/openresty:*`
|
|
39
|
+
|
|
40
|
+
Це стримує зайвий build tooling (Bun, **node_modules** зі збірки) у фінальному образі; для **alpine** / **nginx** / **openresty** у **runtime** лишаються лише відповідні вимоги, для **php** / **python** (виняток) — цільовий інтерпретований **stack**; **scratch** і **debian** з тегом **`*slim*`** — коли glibc і мінімальне оточення Debian важливіші за musl в **alpine**.
|
|
41
|
+
|
|
42
|
+
Перевіряє `getMultistageAndRuntimeHint` у **`npm/rules/docker/lint/main.mjs`**.
|
|
43
|
+
|
|
44
|
+
## Компіляція bun-проєкту в бінарник
|
|
45
|
+
|
|
46
|
+
Якщо проект має `bun install` крок, та не є фронтенд проектом (тобто не має `bun build` крок без `--compile`), **і не має нативного `.node`-аддона з динамічним завантаженням** (див. розділ нижче про нативні аддони), то потрібно щоб була компіляція коду, і далі у фінальному образі був тільки бінарник. Цей образ не містить компілятора, npm, Bun — тільки runtime libs.
|
|
47
|
+
|
|
48
|
+
Тригер перевірки:
|
|
49
|
+
|
|
50
|
+
- у Dockerfile є крок `bun install` (або `bun i`);
|
|
51
|
+
- фінальний FROM — `mirror.gcr.io/library/alpine:*` (тобто не nginx/openresty frontend).
|
|
52
|
+
|
|
53
|
+
Очікування:
|
|
54
|
+
|
|
55
|
+
- у build stage є `bun build --compile`;
|
|
56
|
+
- у фінальному stage немає викликів `bun` (залишків build tooling).
|
|
57
|
+
|
|
58
|
+
### Канон — компільований бінарник на alpine
|
|
59
|
+
|
|
60
|
+
```dockerfile
|
|
61
|
+
FROM mirror.gcr.io/oven/bun:alpine AS build-env
|
|
62
|
+
|
|
63
|
+
WORKDIR /app
|
|
64
|
+
|
|
65
|
+
ENV NODE_ENV=production
|
|
66
|
+
|
|
67
|
+
COPY package.json .
|
|
68
|
+
COPY bunfig.toml .
|
|
69
|
+
|
|
70
|
+
RUN bun install --production
|
|
71
|
+
|
|
72
|
+
COPY ./src ./src
|
|
73
|
+
|
|
74
|
+
# Компілюємо в бінарник
|
|
75
|
+
RUN bun build --compile --outfile app ./src/index.js
|
|
76
|
+
|
|
77
|
+
FROM mirror.gcr.io/library/alpine:latest
|
|
78
|
+
|
|
79
|
+
# (libstdc++ libgcc) для Bun runtime, (tzdata) для часового поясу
|
|
80
|
+
RUN apk add --no-cache libstdc++ libgcc tzdata
|
|
81
|
+
|
|
82
|
+
WORKDIR /app
|
|
83
|
+
|
|
84
|
+
COPY --from=build-env /app/app ./app
|
|
85
|
+
|
|
86
|
+
CMD ["./app"]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Перевіряє `getBunCompileHint` у **`npm/rules/docker/lint/main.mjs`**.
|
|
90
|
+
|
|
91
|
+
## Виняток: нативний `.node`-аддон (sharp / @img/* / argon2)
|
|
92
|
+
|
|
93
|
+
Якщо в `package.json#dependencies` є нативний `.node`-аддон, який вантажиться через **динамічний `require`** — передусім **`sharp`**; той самий клас — **`@img/*`**, **`argon2`** — його **не можна** пакувати через `bun build --compile`.
|
|
94
|
+
|
|
95
|
+
`bun build --compile` не трейсить ``require(`@img/sharp-${platform}/sharp.node`)`` і не вшиває нативний біндинг → компільований бінарник падає в рантаймі: `Could not load the "sharp" module using the linuxmusl-arm64 runtime`. Доведено реальними docker-збірками (bun 1.3.14, sharp 0.34.5); відтворюється і на darwin-arm64 (тобто не musl/glibc-залежне). `apk add vips` **НЕ лікує** — він дає системний libvips, а бракує саме `sharp.node`.
|
|
96
|
+
|
|
97
|
+
Канон — ship `node_modules` і запускати через `bun <entry>` на базі `mirror.gcr.io/oven/bun:alpine` (це легітимний виняток до правила «лише alpine/scratch у фінальному stage» — тут потрібен саме bun-рантайм). База `oven/bun` уже має non-root користувача `bun` (uid/gid 1000). Entry бери з наявного `--outfile`-таргета / `package.json#main` / `scripts.start`; якщо не визначити — лиши TODO-маркер, не вгадуй.
|
|
98
|
+
|
|
99
|
+
Список нативних аддонів — розширювана константа `NATIVE_ADDON_PACKAGES` / `NATIVE_ADDON_SCOPES` у **`npm/rules/docker/lib/docker-native-addon.mjs`** (підключено в **`npm/rules/docker/lint/main.mjs`**).
|
|
100
|
+
|
|
101
|
+
### Антипатерн (це правило ловить)
|
|
102
|
+
|
|
103
|
+
```dockerfile
|
|
104
|
+
RUN bun build --compile --outfile app ./src/index.js # ← з sharp у deps
|
|
105
|
+
FROM mirror.gcr.io/library/alpine:latest
|
|
106
|
+
RUN apk add --no-cache ... vips # системний vips не рятує
|
|
107
|
+
COPY --from=build-env --chown=app:app /app/app ./app
|
|
108
|
+
USER app
|
|
109
|
+
CMD ["./app"]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Канон (привести до цього)
|
|
113
|
+
|
|
114
|
+
```dockerfile
|
|
115
|
+
FROM mirror.gcr.io/oven/bun:alpine AS build-env
|
|
116
|
+
WORKDIR /app
|
|
117
|
+
ENV NODE_ENV=production
|
|
118
|
+
COPY package.json .
|
|
119
|
+
RUN bun install --production
|
|
120
|
+
COPY ./src ./src
|
|
121
|
+
|
|
122
|
+
FROM mirror.gcr.io/oven/bun:alpine
|
|
123
|
+
RUN apk add --no-cache tzdata
|
|
124
|
+
WORKDIR /app
|
|
125
|
+
# база oven/bun має non-root користувача bun (uid/gid 1000)
|
|
126
|
+
COPY --from=build-env --chown=bun:bun /app/node_modules ./node_modules
|
|
127
|
+
COPY --from=build-env --chown=bun:bun /app/src ./src
|
|
128
|
+
COPY --from=build-env --chown=bun:bun /app/package.json ./package.json
|
|
129
|
+
USER bun
|
|
130
|
+
CMD ["bun", "src/index.js"]
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Для проєктів **без** нативних аддонів standalone-бінарник на alpine лишається каноном (див. розділ вище про компіляцію).
|
|
134
|
+
|
|
135
|
+
## Non-root принцип у фінальному stage
|
|
136
|
+
|
|
137
|
+
Для всіх образів потрібно щоб використовувся non-root принцип. **Спосіб** досягнення non-root залежить від **бази**, а не від зміни ОС — змінювати дистрибутив (Alpine→Debian) заради лише non-root **не треба**. Два шляхи:
|
|
138
|
+
|
|
139
|
+
- **standalone-бінарник на `alpine:latest`** (секція компіляції) — у `alpine` немає готового non-root користувача, тож його створюємо явно: `addgroup -g 1000 app && adduser -D -u 1000 -G app app` + `COPY --chown=app:app` + `USER app` (приклад нижче);
|
|
140
|
+
- **ship `node_modules` на `mirror.gcr.io/oven/bun:alpine`** (виняток: нативний `.node`-аддон) — користувач `bun` (uid/gid 1000) **уже в базі**, тож достатньо `COPY --chown=bun:bun …` + `USER bun`; базу на Debian-slim міняти **не треба** — це той самий антипатерн, що й у переліку фінальних образів.
|
|
141
|
+
|
|
142
|
+
### Приклад для standalone-бінарника на alpine
|
|
143
|
+
|
|
144
|
+
```dockerfile
|
|
145
|
+
# Stage 1
|
|
146
|
+
FROM oven/bun:alpine AS build-env
|
|
147
|
+
WORKDIR /app
|
|
148
|
+
COPY package.json bunfig.toml .
|
|
149
|
+
RUN bun install --production
|
|
150
|
+
COPY ./src ./src
|
|
151
|
+
RUN bun build --compile --outfile app ./src/index.js
|
|
152
|
+
|
|
153
|
+
# Stage 2
|
|
154
|
+
FROM alpine:latest
|
|
155
|
+
RUN apk add --no-cache libstdc++ libgcc tzdata
|
|
156
|
+
|
|
157
|
+
# Додати non-root user
|
|
158
|
+
RUN addgroup -g 1000 app && adduser -D -u 1000 -G app app
|
|
159
|
+
|
|
160
|
+
WORKDIR /app
|
|
161
|
+
|
|
162
|
+
# Змінити власника файлу
|
|
163
|
+
COPY --from=build-env --chown=app:app /app/app ./app
|
|
164
|
+
|
|
165
|
+
# Запускати як non-root
|
|
166
|
+
USER app
|
|
167
|
+
|
|
168
|
+
CMD ["./app"]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Перевіряє `getNonRootRuntimeHint` у **`npm/rules/docker/lint/main.mjs`**.
|
|
172
|
+
|
|
173
|
+
> **Примітка:** для `nginxinc/nginx-unprivileged` канон відрізняється — дивись розділ нижче про nginx non-root.
|
|
174
|
+
|
|
175
|
+
## Мінімальний тег для nginx-unprivileged
|
|
176
|
+
|
|
177
|
+
Якщо використовується nginx, то **використовуй** `mirror.gcr.io/nginxinc/nginx-unprivileged:alpine-slim` (і не `latest` / `alpine`).
|
|
178
|
+
|
|
179
|
+
Тег `alpine-slim` є обов'язковим для всіх `FROM` з образом `nginxinc/nginx-unprivileged` — будь-який інший тег (включаючи без тега) прапорцюється як порушення.
|
|
180
|
+
|
|
181
|
+
Перевіряє `getNginxAlpineSlimTagHint` у **`npm/rules/docker/lint/main.mjs`**.
|
|
182
|
+
|
|
183
|
+
## nginx-unprivileged — без USER, із --chown
|
|
184
|
+
|
|
185
|
+
Окрема гілка для фронтенду на базі **`nginxinc/nginx-unprivileged`** (будь-який тег, з/без `mirror.gcr.io/`-префікса). Цей образ **уже** оголошує `USER 101` і `EXPOSE 8080`, тож у фінальному stage **не потрібні** жодні явні `USER`-інструкції:
|
|
186
|
+
|
|
187
|
+
- **жодного `USER root` / `USER 0`** для білд-кроків: він перезатирає успадкований `USER 101`, і якщо потім не повернути non-root — фінальний образ лишається root, а k8s із `runAsNonRoot: true` падає з `CreateContainerConfigError`;
|
|
188
|
+
- **жодного switch-back** `USER 101` / `USER nginx` наприкінці stage — це лише симптом зайвого `USER root` на початку (повертати треба саме **числовим** UID, бо kubelet не підтверджує non-root за іменем `nginx`). Канон — взагалі не виходити з-під дефолтного 101;
|
|
189
|
+
- **`COPY`/`ADD` лише з `--chown`** (канон — `--chown=nginx:nginx`): без нього файли копіюються власником root і дефолтний non-root користувач (uid=101) не зможе читати статику.
|
|
190
|
+
|
|
191
|
+
Build-stage не чіпаємо — там root і tooling норма. Перевіряє **`npm/rules/docker/lib/docker-nginx-user.mjs`**.
|
|
192
|
+
|
|
193
|
+
### Антипатерн (це правило ловить)
|
|
194
|
+
|
|
195
|
+
```dockerfile
|
|
196
|
+
FROM mirror.gcr.io/nginxinc/nginx-unprivileged:alpine-slim
|
|
197
|
+
USER root
|
|
198
|
+
COPY ./k8s/nginx.conf /etc/nginx/conf.d/default.conf
|
|
199
|
+
COPY --from=build /app/dist ./
|
|
200
|
+
RUN find ./ -type f -name "*.js" -exec gzip -k {} \;
|
|
201
|
+
USER 101 # повернення назад — симптом того, що був зайвий USER root
|
|
202
|
+
EXPOSE 8080
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Канон (привести до цього)
|
|
206
|
+
|
|
207
|
+
```dockerfile
|
|
208
|
+
FROM mirror.gcr.io/nginxinc/nginx-unprivileged:alpine-slim
|
|
209
|
+
|
|
210
|
+
COPY --chown=nginx:nginx ./k8s/nginx.conf /etc/nginx/conf.d/default.conf
|
|
211
|
+
|
|
212
|
+
WORKDIR /usr/share/nginx/html
|
|
213
|
+
|
|
214
|
+
COPY --from=build --chown=nginx:nginx /app/dist ./
|
|
215
|
+
|
|
216
|
+
RUN find ./ -type f -name "*.js" -exec gzip -k {} \;
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
(без жодного `USER`, gzip під дефолтним користувачем 101; `EXPOSE 8080` теж зайвий — база вже його оголошує)
|
|
220
|
+
|
|
221
|
+
## hadolint
|
|
222
|
+
|
|
223
|
+
CLI **`hadolint`** приймає лише **явні шляхи** (`[DOCKERFILE...]` у **`hadolint --help`**); обхід репозиторію робить **`npm/rules/docker/lint/main.mjs`** (правило `npx @7n/rules lint docker`) — файли з іменем **`Dockerfile`**, **`*.Dockerfile`**, **`Containerfile`**, **`*.Containerfile`** (суфікс без урахування регістру), з тими самими пропусками каталогів, що й решта обходу репозиторію.
|
|
224
|
+
|
|
225
|
+
Виклик **`hadolint`** як **нативного бінарника** через **`ensureTool`** (PATH → кеш → авто-install brew/scoop/GitHub Release; **без** `docker run`) — спільна логіка **`npm/rules/docker/lib/docker-hadolint.mjs`**. Для CI-workflow, що встановлює hadolint і викликає `npx @7n/rules lint docker`, дивись `npm/rules/docker/lint_docker_yml/lint_docker_yml.mdc` (там канонічний шаблон і rego-перевірка `.github/workflows/lint-docker.yml`).
|
|
226
|
+
|
|
227
|
+
### Конфігурація hadolint
|
|
228
|
+
|
|
229
|
+
Кореневий **`.hadolint.yaml`**: вимкнення правил, trusted registries — [документація](https://github.com/hadolint/hadolint#configure). Щоб не додавати **`# hadolint ignore=DL3007`** у кожному **`FROM`** з **`:latest`**, у корені проєкту-споживача можна задати глобально:
|
|
230
|
+
|
|
231
|
+
```yaml title=".hadolint.yaml"
|
|
232
|
+
ignored:
|
|
233
|
+
- DL3007
|
|
234
|
+
- DL3008
|
|
235
|
+
- DL3018
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Де DL3007 — «Не використовуй тег latest у FROM»
|
|
239
|
+
Де DL3018 — «Піни версії пакетів у apk add»
|
|
240
|
+
|
|
241
|
+
### Запуск
|
|
242
|
+
|
|
243
|
+
**`npx @7n/rules lint docker`** — виклик hadolint як у **`docker-hadolint.mjs`** (нативний бінарник через **`ensureTool`**; **без** `docker run`), разом з іншими docker-перевірками (mirror/multistage/compile/non-root/nginx) в одному проході по кожному Dockerfile/Containerfile.
|
|
244
|
+
|
|
245
|
+
Якщо немає жодного Dockerfile/Containerfile у репозиторії — перевірка пропускається (exit 0).
|
|
246
|
+
|
|
247
|
+
Винятки: **`# hadolint ignore=DL3008`** (або інший код) у Dockerfile, або **`ignored`** у **`.hadolint.yaml`** (наприклад **DL3007** для **`:latest`** — див. вище).
|
|
@@ -24,3 +24,17 @@ import welcomeImage from './assets/welcome.png.avif'
|
|
|
24
24
|
Якщо raster-посилання у `.vue`/`.html` не вдалось переписати (наприклад, оригіналу немає на диску, тож і `.avif` не згенерувався) — `check image-avif` падає з помилкою на конкретний файл.
|
|
25
25
|
|
|
26
26
|
AVIF-двійники **зберігаємо в git** — це готові артефакти для віддачі браузеру (без них ефект від AVIF втрачається на чистому checkout-і).
|
|
27
|
+
|
|
28
|
+
## Опт-аут для конкретного пакета
|
|
29
|
+
|
|
30
|
+
У workspace-пакеті, де AVIF-імпорти небажані (наприклад, мобільний бандл або публічний сайт без гарантованої AVIF-підтримки), додай у `package.json` цього пакета. Заборонений typo `disabled-avif` (канон — `disable-avif`): [package.json.deny.json](../package_json/template/package.json.deny.json).
|
|
31
|
+
|
|
32
|
+
```json title="apps/site/package.json"
|
|
33
|
+
{
|
|
34
|
+
"@nitra/minify-image": {
|
|
35
|
+
"disable-avif": true
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Тоді перевірка пропускає `.vue` файли цього пакета і не видаляє наявні `.avif` всередині як «сироти». У root-`package.json` опт-аут діє лише для файлів кореня (вкладені workspaces перевіряються незалежно — вмикай прапор у кожному пакеті, де треба).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
## `for...in` заборонено — рефакторити на `for...of`
|
|
2
|
+
|
|
3
|
+
Конструкція `for (const k in obj)` обходить успадковані ключі прототипу, тому майже завжди тягне `Object.hasOwn(obj, k)`-guard. Заборонена у `@nitra/eslint-config` через `no-restricted-syntax` для `ForInStatement` (з версії **3.8.0**). У каноні oxlint (`guard-for-in: "deny"` у [oxlint-canonical.json](../tooling/data/tooling/oxlint-canonical.json)) лишається `guard-for-in` як часткова страховка (oxlint не підтримує `no-restricted-syntax`) — `checkOxlintRc` у `check/main.mjs` звіряє `.oxlintrc.json` проєкту з цим каноном.
|
|
4
|
+
|
|
5
|
+
Замість цього обходь масив напряму, а об'єкт — через `Object.entries` / `Object.keys` / `Object.values`. У такому коді guard за `Object.hasOwn` стає непотрібним і має зникнути разом із `for...in`.
|
|
6
|
+
|
|
7
|
+
```javascript title="❌ до"
|
|
8
|
+
for (const k in obj) {
|
|
9
|
+
if (!Object.hasOwn(obj, k)) continue
|
|
10
|
+
use(k, obj[k])
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
for (const i in arr) {
|
|
14
|
+
use(arr[i])
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```javascript title="✅ після"
|
|
19
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
20
|
+
use(k, v)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const item of arr) {
|
|
24
|
+
use(item)
|
|
25
|
+
}
|
|
26
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
## Розширення нових файлів — `.mjs` / `.cjs`, не `.js`
|
|
2
|
+
|
|
3
|
+
**Нові** JS-файли створюй з явним розширенням модуля:
|
|
4
|
+
|
|
5
|
+
- **`.mjs`** — для ESM (типовий випадок);
|
|
6
|
+
- **`.cjs`** — для CommonJS, де він справді потрібен.
|
|
7
|
+
|
|
8
|
+
Голий **`.js`** для нового файлу **заборонено**. Розширення `.js` інтерпретується як ESM чи CJS лише за полем `package.json#type`, тож той самий файл читається по-різному залежно від пакета. Явне `.mjs`/`.cjs` робить тип модуля однозначним **без читання `package.json`** — навіть якщо `type` зміниться або файл перемістять в інший пакет. Це доповнює вимогу `"type": "module"` у [package_json.mdc](../package_json/package_json.mdc): `type` лишається каноном для всього дерева, а розширення нового файлу прибирає залежність від нього.
|
|
9
|
+
|
|
10
|
+
Стосується **backend і frontend** — будь-який новий вихідний файл: `src/`, тести `*.test.*`, `scripts/`, `src/conn/` тощо.
|
|
11
|
+
|
|
12
|
+
**Існуючі `.js` лишаються як є** — масово перейменовувати не треба; це конвенція для нового коду. Автоматичної перевірки тут немає: stateless-скан не відрізнить новий файл від існуючого, тож `.js` нікого не фейлить.
|
|
@@ -12,3 +12,31 @@ Rego-пакет: `js_lint.jscpd`
|
|
|
12
12
|
- `minLines` — число, значення >= канонічного порогу (`25`); більше дозволено
|
|
13
13
|
|
|
14
14
|
Канон-snippet: [.jscpd.json.snippet.json](./template/.jscpd.json.snippet.json)
|
|
15
|
+
|
|
16
|
+
### Ігнорування `.claude/worktrees/`
|
|
17
|
+
|
|
18
|
+
Каталог `.claude/worktrees/` (робочі копії, які Claude Code створює через **superpowers:using-git-worktrees**) має ігноруватися:
|
|
19
|
+
|
|
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` канон) крос-файловий; запускається як частина `npx @7n/rules lint js`.
|
|
4
|
+
|
|
5
|
+
У корені проєкту має бути **`knip.json`**, який стартує з канонічного baseline з пакета `@7n/rules` — файл [`knip-canonical.json`](../tooling/data/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 @7n/rules lint js` (концерн `js/check`) копіює канон у корінь проєкту як side effect детектора, ще до фази LLM-фіксів. Після створення модифікуй файл під свій проєкт як завгодно: перевіряємо лише наявність, зміст подальших змін не валідується.
|
|
14
|
+
|
|
15
|
+
Пакет `knip` окремо в `devDependencies` не додавай — `bunx knip` тягне його ad-hoc, як oxlint/eslint/jscpd.
|
|
@@ -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 @7n/rules lint js` (концерн `utils_imports`) обходить кожен `utils/`-каталог у воркспейсах і падає, якщо знаходить relative-імпорт з `..` (вихід за межі каталогу) у будь-якому не-тестовому файлі. Це механічне втілення правила «utils не знає про домен»: тільки same-dir (`./X`), bare-пакети та `node:*` дозволені; cross-rule, конфіги проєкту чи sibling-utils — fail.
|
|
14
|
+
|
|
15
|
+
Якщо потрібна domain-залежність — перенеси файл у `lib/`.
|
|
@@ -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` не потрібно. Автоматична перевірка (`lib/bun-sql-scan.mjs`, `findBunSqlPerRequestConnectionInText`, підключена з `safety/main.mjs`) фейлить будь-який `new SQL(...)` усередині функції — це єдина частина цієї сторінки, яку лінтер справді перевіряє; сам singleton-паттерн (файл, іменування `pgWrite`/`pgRead`) — рекомендація без автоматичної перевірки.
|
|
@@ -11,7 +11,21 @@ Rego-пакет: `js_bun_db.package_json`
|
|
|
11
11
|
- `pg-format` — заміни на Bun native SQL (без ручного форматування)
|
|
12
12
|
- `mysql2` — заміни на Bun native SQL
|
|
13
13
|
|
|
14
|
-
`pg` у dependencies **не** є порушенням на рівні Rego — виняток для LISTEN/NOTIFY зважується у JS-сканері (`
|
|
14
|
+
`pg` у dependencies **не** є порушенням на рівні Rego — виняток для LISTEN/NOTIFY зважується у JS-сканері (`safety/main.mjs`, детальніше — `safety/safety.mdc`, секція «pg: виключення для LISTEN/NOTIFY»).
|
|
15
15
|
|
|
16
16
|
✓ `{ "dependencies": { "pg": "^8.0.0" } }` — дозволено
|
|
17
17
|
✗ `{ "dependencies": { "pg-format": "^1.0.0" } }` — `deny: dependencies.pg-format — заміни на Bun native SQL`
|
|
18
|
+
|
|
19
|
+
### Перехід з `pg` / `mysql2` на Bun native SQL
|
|
20
|
+
|
|
21
|
+
PostgreSQL 18+, MariaDB 10.6+ (сумісний з MySQL-протоколом, підключаємось як `mysql://`).
|
|
22
|
+
|
|
23
|
+
Якщо в проєкті використовуються бібліотеки `pg`, `pg-format` або `mysql2`, їх потрібно замінити на Bun native SQL: <https://bun.com/docs/runtime/sql>.
|
|
24
|
+
|
|
25
|
+
- Видалити з `dependencies`: `pg-pool`, `pg-native`, `pg-format`, `mysql`, `mysql2`.
|
|
26
|
+
- Видалити з коду: усі `import` / `require` цих пакетів та власні обгортки над ними.
|
|
27
|
+
- Замінити на `import { sql, SQL } from 'bun'` — Bun має вбудований клієнт із пулом, prepared statements та tagged templates.
|
|
28
|
+
|
|
29
|
+
`pg-format` (unscoped) — це ручне форматування SQL через escape (`format('... %L ...', value)`); такі рядки легко поламати неправильним типом, locale-залежним escape або забутим `%L`. Tagged template Bun SQL параметризує значення нативно (``sql`... ${value} ...` ``) і не лишає простору для injection — окремий «форматер» **для значень** не потрібен.
|
|
30
|
+
|
|
31
|
+
Виключення є **лише** для **динамічних identifiers** (назви схем / таблиць / колонок / індексів / ролей / БД) і whitelist-фрагментів типу `ASC`/`DESC`: Bun SQL їх параметризувати не вміє, тож тут дозволено окремий пакет **`@scaleleap/pg-format`** (scoped форк, не unscoped `pg-format`) — деталі й приклади у `pg_format_identifiers/pg_format_identifiers.mdc`.
|
|
@@ -0,0 +1,104 @@
|
|
|
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-списку](../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: <причина>` (див. `safety/safety.mdc`, секція «sql.unsafe(...) за замовчуванням заборонено»).
|
|
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` сканера (це шим, а не імпорт з бібліотеки); детектор — `findPgFormatShimDefinitionInText` (`lib/bun-sql-scan.mjs`, підключений з `safety/main.mjs`).
|
|
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`.
|
|
103
|
+
|
|
104
|
+
Ніщо з цієї сторінки (правильність використання `%I`/`%s`, вибір `@scaleleap/pg-format` замість unscoped `pg-format` у коді, а не лише у `dependencies`) наразі не має власного AST-детектора — сканер (`lib/bun-sql-scan.mjs`) ловить лише симптоми: shim-реалізації (`findPgFormatShimDefinitionInText`), обгортки над `unsafe` (`findPgFormatLikeQueryWrapperInText`) та відсутність `allow-unsafe`-маркера. Тому ця сторінка лишається docs-only; unscoped `pg-format` як залежність вже заборонено окремо в `package_json/package_json.mdc` (Rego deny-list).
|